Zero Trust TLS Everywhere JWT + RSA-4096 Ansible natif Open Source · Go

Des agents
pour Ansible

Exécutez vos playbooks sur des hôtes derrière NAT, firewall ou DMZ. Les agents initient eux-mêmes la connexion — vous n'ouvrez aucun port entrant.

8 risques que vous prenez à chaque playbook

Ansible est un outil remarquable. Mais son modèle SSH classique crée des angles morts sécurité et des dettes opérationnelles que la plupart des équipes découvrent trop tard.

💥

Une machine compromise, toute votre infra exposée

Votre Control Node centralise les accès SSH à tous vos hôtes. Un token CI/CD leaké, une dépendance malveillante, un accès mal révoqué — et c'est l'ensemble de votre parc qui est à portée d'un attaquant. En une seule étape. Sans alarme.

La surface d'attaque croît linéairement avec la taille de votre infra.

🔑

Révoquer une clef SSH : une urgence à 3h du matin

Une clef volée. L'horloge tourne. Vous devez la supprimer sur 300 hôtes, maintenant. Avec SSH classique, c'est une opération manuelle, hôte par hôte, qui prend des heures — sans blacklist centrale, sans expiration automatique.

La rotation de clefs n'est pas un événement planifié. C'est une urgence.

📋

Qui a lancé quoi, sur quoi, et quand ?

Ansible n'a pas de log d'audit centralisé. Les traces restent sur le Control Node — modifiables, supprimables. En cas d'incident ou de contrôle ISO 27001, NIS2 ou SOC2, vous ne pouvez pas prouver qu'une configuration a été appliquée, ni par qui.

L'auditeur pose la question. Vous cherchez dans des logs dispersés sur plusieurs machines.

🚪

Tout le monde peut tout faire

Ansible n'a pas de RBAC natif. Sur un Control Node standard, quiconque a accès peut lancer n'importe quel playbook sur n'importe quel hôte. AWX apporte du contrôle d'accès — au prix d'une stack lourde, d'une base de données et d'une interface à maintenir.

Vos droits sont gérés dans Azure AD. Votre automation n'a aucun contrôle d'accès.

📡

Votre infra dérive. Vous ne le savez pas.

Ansible pousse une configuration, puis rien. Entre deux runs, un admin fait un changement manuel, un paquet se met à jour, une règle firewall est modifiée. Votre infra dérive silencieusement de l'état décrit dans vos playbooks — et vous ne le saurez qu'au prochain incident.

Push-and-forget n'est pas de la gestion de configuration. C'est de l'espoir.

Un hôte offline ? La tâche disparaît.

Un redémarrage inattendu, une coupure réseau — si un hôte est indisponible au moment du playbook, la tâche échoue et n'est jamais rejouée. En rolling deploy sur 50 machines, il suffit d'une qui redémarre pour avoir un parc partiellement patché, sans alerte.

Votre fenêtre de maintenance a 4 heures. 3 hôtes n'ont pas reçu le patch. Vous ne le savez pas.

🔀

Un hôte compromis peut attaquer votre Control Node

C'est votre Control Node qui initie les connexions SSH vers les hôtes. Un hôte compromis connaît son adresse IP — et cette machine détient les clefs SSH de tout votre parc. Compromettre un hôte → cibler le Control Node → accéder à l'intégralité de l'infrastructure.

Vous avez segmenté vos hôtes. Mais le Control Node est le dénominateur commun de tous vos segments.

🌐

Multi-cloud, multi-site : une dette réseau permanente

Production, DMZ, disaster recovery, cloud A, cloud B, sites edge — chaque segment doit avoir une route vers le Control Node. VPN site-à-site, peering cloud, tunnels SSH : chaque connexion est une règle firewall de plus, une surface d'attaque de plus, une configuration à maintenir.

Votre infra est multi-cloud. Votre automation ne devrait pas exiger un réseau plat entre tous vos environnements.

Ansible Control Node ──SSH──▶ Hôte cible ← BLOQUÉ derrière NAT/firewall

Le modèle inversé Ansible-SecAgent

Ansible-SecAgent inverse le flux de contrôle. L'agent sur l'hôte cible initie une connexion WebSocket persistante vers le serveur relay. Ansible n'a besoin d'atteindre que le relay server — jamais les hôtes directement.

Ansible Control Node │ HTTPS (REST) ┌─────────────────────────────────────────┐ │ Relay Server │ │ Go · NATS JetStream · SQLite │ │ Auth JWT · Blacklist JTI · CLI │ └──────────────▲──────────────────────────┘ │ WSS persistant (TLS) │ ↑ initié par l'agent ┌──────────────┴──────────────────────────┐ │ Agents (secagent-minion · systemd) │ │ host-A host-B host-C host-N │ │ NAT · DMZ · Cloud privé · Edge │ └─────────────────────────────────────────┘
Mononode

Simple & rapide

Un seul serveur relay pour démarrer. Idéal pour les petites infrastructures ou pour valider la solution en quelques minutes.

  • 1 secagent-server (Go, 4.65 MiB)
  • 1 NATS JetStream (embarqué)
  • 1 Caddy (TLS automatique)
  • SQLite inclus — zéro config DB
  • docker compose up -d → prêt en 30s
Multi-nodes · HA

Scalable & haute dispo

Plusieurs nodes relay derrière un load balancer. Les agents se connectent à n'importe quel node, le routage est transparent via cluster NATS.

  • N secagent-server en parallèle
  • Cluster NATS JetStream (3 nodes)
  • Load balancer (Nginx / Ingress K8s)
  • PostgreSQL externe (RDS/CloudSQL)
  • Helm chart Kubernetes inclus

Votre Ansible existant, inchangé

Ansible-SecAgent s'intègre comme un plugin Ansible standard. Vos playbooks, rôles et collections fonctionnent sans modification. La transition est progressive : SSH reste disponible en parallèle.

Playbooks inchangés

Tous vos playbooks, rôles et collections Ansible Galaxy fonctionnent sans modification. Changez juste connection: relay dans ansible.cfg.

SSH toujours disponible

Ansible-SecAgent ne supprime pas SSH. Les deux coexistent. Idéal pour une migration progressive : passez hôte par hôte sans rupture de service.

Inventaire dynamique

Le plugin inventory liste automatiquement tous les agents connectés avec leurs facts. Vos inventaires statiques restent compatibles en parallèle.

become / sudo supporté

become: true et become_user fonctionnent comme avec SSH. Le become_pass est transmis de façon sécurisée, masqué dans tous les logs.

# ansible.cfg — seul changement requis [defaults] connection = relay # was: ssh inventory = relay_inventory.py # dynamic, auto-populated [relay_connection] server_url = https://relay.example.com token = ${ANSIBLE_RELAY_TOKEN}

Sécurité by design

Ansible-SecAgent est conçu autour d'un modèle Zero Trust. Chaque décision d'architecture a une justification sécurité explicite, documentée dans les specs techniques.

RSA-4096 par agent

Chaque agent génère sa paire de clefs RSA-4096 au premier démarrage. La clef privée ne quitte jamais l'hôte.

Enrôlement contrôlé

La clef publique de l'agent doit être pré-autorisée en base de données avant tout enrôlement. Zéro TOFU (Trust On First Use).

JWT chiffré RSAES-OAEP

Le token JWT retourné à l'agent est chiffré avec sa clef publique RSA. Illisible sans la clef privée, même en cas d'interception réseau.

Blacklist JTI

Révocation immédiate par JTI (JWT ID unique). La connexion WebSocket active est fermée avec le code 4001 — l'agent ne reconnecte pas automatiquement.

Dual-key JWT

Rotation des secrets JWT sans interruption de service. Grace period configurable : les anciens tokens restent valides pendant la migration.

TLS obligatoire

WSS (WebSocket over TLS) et HTTPS sur toutes les connexions. Terminaison TLS via Caddy. Aucun plain HTTP accepté.

become_pass masqué

Le mot de passe sudo/su est transmis via stdin chiffré et masqué dans tous les logs — agent, serveur et plugins Ansible.

AES-256-GCM

Les secrets serveur (RSA keypair, JWT secrets) sont chiffrés en base de données via AES-256-GCM dérivé de RSA_MASTER_KEY.

Vecteur d'attaque SSH classique Ansible-SecAgent
Port entrant exposé Port 22 ouvert Aucun port entrant
Interception du token d'auth Clef SSH en clair sur disque JWT chiffré RSA-OAEP
Agent compromis → propagation Clef SSH donne accès direct JTI révocable immédiatement
Agent non autorisé TOFU implicite possible Pre-authorize obligatoire
Rotation des secrets Manuelle, downtime Dual-key, grace period, zero downtime
Fuite become_pass dans logs Risque selon configuration Masqué dans tous les logs

Implémentées par version

Ansible-SecAgent est développé par phases incrémentales, chacune validée par tests automatisés et audit sécurité avant déploiement.

v0.1 — Agent Python MVP
  • Enrollment RSA-4096 : génération clef, POST /api/register, JWT OAEP
  • Connexion WSS persistante avec reconnexion backoff exponentiel (1s → 60s)
  • Exécution de commandes via subprocess (buffer stdout 5 MB, timeout, kill)
  • Transfert de fichiers : put_file et fetch_file (base64, limite 500 KB)
  • Élévation de privilèges : become via stdin, become_pass masqué dans logs
  • Registre async persisté JSON : reprise après redémarrage agent
  • Déploiement systemd : Restart=on-failure, User dédié, NoNewPrivileges
v0.2 — Server Python (FastAPI + NATS)
  • API REST FastAPI : /api/register, /api/exec, /api/upload, /api/fetch, /api/inventory
  • NATS JetStream : streams RELAY_TASKS + RELAY_RESULTS, routing inter-nodes HA
  • Auth JWT : rôles agent/plugin/admin, blacklist JTI, révocation WS code 4001
  • SQLite : tables agents, authorized_keys, blacklist avec schéma versionné
  • Inventaire dynamique : format JSON Ansible standard, filtre only_connected
v0.3 — Plugins Ansible
  • Connection plugin : remplace SSH, exec_command / put_file / fetch_file natifs
  • become support : transmission become_pass au serveur relay
  • Inventory plugin : GET /api/inventory → hostvars + groupes Ansible
  • Configuration via ansible.cfg ou variables d'hôte (ansible_connection: relay)
  • Mapping HTTP → AnsibleConnectionError : 503 UNREACHABLE, 504 timeout
v1.0 — Réécriture Go
  • Serveur Go : binaire 4.65 MiB, 0 restart en production, goroutines
  • Agent Go : 94 tests PASS, 3 agents qualif connectés en continu
  • secagent-inventory : binaire Go standalone, 19 tests, format Ansible natif
  • SQLite via modernc (CGO-free), Gorilla WebSocket, JWT Go natif
  • Dockerfile multi-stage golang:alpine → alpine : image minimale
v1.1 — CLI Management
  • CLI cobra intégrée dans le binaire secagent-server (zéro binaire supplémentaire)
  • minions : list / get / suspend / resume / revoke / authorize / vars
  • security : keys status / rotate --grace Nh · tokens list · blacklist list
  • inventory list --only-connected · server status / stats
  • --format table|json|yaml sur toutes les commandes
  • Dual-key JWT : rotation sans interruption, grace period configurable
  • Message WS rekey : agents migrent vers le nouveau secret sans déconnexion
  • AES-256-GCM : chiffrement RSA keypair serveur et JWT secrets en DB
  • 441 tests PASS, 0 fail · Audit sécurité : 0 CRITIQUE, 0 HAUT
v1.2 — Ansible Container
  • Container relay-ansible : multi-stage build (Go builder + Python runtime)
  • secagent-inventory : binaire Go standalone intégré dans le container
  • Connection plugin relay.py : chargé depuis ansible_plugins/connection_plugins/
  • ExecCommand handler : relaie commandes Ansible vers agents via WebSocket bloquant
  • Inventory filtering : par défaut retourne tous les minions, relay_status : "connected" | "disconnected"
  • End-to-end validation : ansible all -m ping, ansible-playbook, dynamic inventory ✅
  • Network : relay-ansible sur ansible_server_default (connecté à relay-api)
  • Dockerfile : golang:1.25-alpine (Stage 1) → python:3.11-slim (Stage 2)
  • Phase 11 COMPLETE : tous les composants Ansible natif + relay intégrés et testés ✅

Administrez depuis le terminal

La CLI cobra est intégrée directement dans le binaire secagent-server. Accessible depuis un container Docker ou un pod Kubernetes — sans binaire supplémentaire.

secagent-server — management CLI
$ docker exec relay-api secagent-server minions list --format table HOSTNAME STATUS LAST SEEN VERSION IP qualif-host-01 connected 2026-03-06 14:32 v1.1.0 192.168.1.101 qualif-host-02 connected 2026-03-06 14:32 v1.1.0 192.168.1.102 qualif-host-03 connected 2026-03-06 14:31 v1.1.0 192.168.1.103 prod-web-01 suspended 2026-03-06 09:15 v1.1.0 10.0.1.10 prod-db-01 connected 2026-03-06 14:32 v1.1.0 10.0.1.20 5 agents (4 connected, 1 suspended) $ docker exec relay-api secagent-server security keys status --format table KEY SECRET STATUS EXPIRES current jwt_secret_current active 2026-04-05 14:00 previous jwt_secret_previous grace 2026-03-07 14:00 (grace 24h) RSA keypair rsa-4096 encrypted (AES-256-GCM in DB)
🤖

Gestion des agents

Listez, suspendez, révoquez et autorisez les agents. Gérez leurs variables d'inventaire.

secagent-server minions list
secagent-server minions get my-host
secagent-server minions suspend my-host
secagent-server minions resume  my-host
secagent-server minions revoke  my-host
secagent-server minions authorize new-host \
  --pubkey "$(cat pubkey.pem)"
secagent-server minions vars set my-host \
  env=prod region=eu-west
🔐

Sécurité & clefs

Rotation des secrets JWT zero downtime, gestion des tokens actifs et de la blacklist JTI.

# Rotation JWT (grace period configurable)
secagent-server security keys rotate --grace 24h

# État des clefs
secagent-server security keys status

# Tokens actifs et blacklist
secagent-server security tokens list
secagent-server security blacklist list
secagent-server security blacklist purge
📋

Inventaire

Visualisez les agents connectés et leurs facts directement depuis la CLI.

# Agents connectés uniquement
secagent-server inventory list --only-connected

# Format JSON (compatible Ansible)
secagent-server inventory list --format json

# Format YAML
secagent-server inventory list --format yaml
📊

Statut serveur

Consultez l'état du serveur relay : uptime, connexions actives, tâches exécutées, mémoire.

# État général
secagent-server server status --format table

# Statistiques détaillées
secagent-server server stats --format json

# Exemple de sortie :
# UPTIME    AGENTS  TASKS 24H  MEMORY
# 3d 14h    4 / 5   127        12 MB
Accès : La CLI lit les variables d'environnement du container (ADMIN_TOKEN, JWT_SECRET_KEY). Aucune configuration supplémentaire requise.
# Docker
docker exec relay-api secagent-server <commande>

# Kubernetes
kubectl exec -n ansible-relay deploy/relay-api -- secagent-server <commande>

À venir

Les phases suivantes sont spécifiées dans ARCHITECTURE.md et prêtes à démarrer.

Production Kubernetes

Helm chart complet : Deployment relay-api (replicas 3), StatefulSet NATS JetStream (cluster, PVC 20Gi fast-ssd), Ingress nginx avec TLS cert-manager, Secrets K8s pour JWT_SECRET_KEY et ADMIN_TOKEN. PostgreSQL externe (RDS/CloudSQL) en remplacement de SQLite.

Documentation & Hardening

Rate limiting sur les endpoints sensibles (/api/register, /api/exec), audit logs structurés (JSON, syslog), RBAC granulaire par hostgroup, métriques Prometheus / Grafana dashboard, runbook opérationnel.

Plugin FreeIPA

Intégration enterprise : enrollment via ipa-client-install, authentification mTLS avec certificats signés par l'AC FreeIPA (Dogtag), révocation via CRL/OCSP, inventory plugin LDAP (hostgroups IPA → groupes Ansible natifs).

Démarrage rapide

Choisissez votre méthode de déploiement.

Cycle d'enrollment : l'agent génère sa paire RSA-4096 au premier démarrage et la stocke dans /etc/secagent-minion/id_rsa (mode 0600). Il tente immédiatement de s'enroller auprès du serveur — mais le serveur refuse tant que la clef publique n'est pas pré-autorisée. L'agent retente automatiquement avec un backoff exponentiel.
1

Cloner le dépôt et configurer

git clone https://github.com/CCoupel/Ansible-SecAgent.git
cd Ansible-SecAgent/GO

export JWT_SECRET_KEY="$(openssl rand -hex 32)"
export ADMIN_TOKEN="$(openssl rand -hex 16)"
export RSA_MASTER_KEY="$(openssl rand -hex 32)"
2

Démarrer le serveur (relay-api + NATS + Caddy)

docker compose up -d
3

Déployer l'agent sur l'hôte cible

# === Sur l'hôte cible (accès initial : SSH, USB, provisioning existant…) ===
cp secagent-minion /usr/local/bin/secagent-minion

# Variables d'environnement de l'agent
export RELAY_SERVER_URL=wss://relay.example.com
export RELAY_HOSTNAME=my-host
# Clef privée : /etc/secagent-minion/id_rsa  (créée au 1er démarrage, mode 0600)
# JWT         : /etc/secagent-minion/token.jwt (créé après enrollment réussi)

./secagent-minion
# → Génère RSA-4096 dans /etc/secagent-minion/id_rsa
# → POST /api/register → 403 (pas encore autorisé) → retente en backoff
4

Autoriser l'agent (pre-authorize)

# === Toujours sur l'hôte cible : extraire la clef publique ===
openssl rsa -in /etc/secagent-minion/id_rsa -pubout 2>/dev/null
# → -----BEGIN PUBLIC KEY-----
#    MIICIjANBgkqhkiG9w0BAQ...
#    -----END PUBLIC KEY-----

# === Sur le serveur relay : enregistrer la clef publique ===
docker exec relay-api secagent-server minions authorize my-host \
  --pubkey "$(ssh user@my-host openssl rsa -in /etc/secagent-minion/id_rsa -pubout 2>/dev/null)"

# → L'agent retente automatiquement et se connecte
5

Lancer un playbook Ansible

ansible-playbook site.yml   # Aucune modification du playbook requise !

# Vérifier la connexion
docker exec relay-api secagent-server minions list --format table
Cycle d'enrollment : l'agent génère sa paire RSA-4096 au premier démarrage et la stocke dans /etc/secagent-minion/id_rsa (mode 0600). Il tente immédiatement de s'enroller auprès du serveur — mais le serveur refuse tant que la clef publique n'est pas pré-autorisée. L'agent retente automatiquement avec un backoff exponentiel.
1

Compiler depuis les sources

git clone https://github.com/CCoupel/Ansible-SecAgent.git
cd Ansible-SecAgent/GO
go build -o secagent-server    ./cmd/server
go build -o secagent-minion     ./cmd/agent
go build -o secagent-inventory ./cmd/inventory
2

Démarrer NATS JetStream

nats-server -js -p 4222 &
# ou : docker run -d --rm -p 4222:4222 nats:latest -js
3

Démarrer le serveur relay

export JWT_SECRET_KEY="$(openssl rand -hex 32)"
export ADMIN_TOKEN="$(openssl rand -hex 16)"
export RSA_MASTER_KEY="$(openssl rand -hex 32)"
export NATS_URL="nats://localhost:4222"

./secagent-server serve --addr :8080
4

Déployer l'agent sur l'hôte cible

# === Sur l'hôte cible ===
cp secagent-minion /usr/local/bin/
cp secagent-minion.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now secagent-minion
# → Génère /etc/secagent-minion/id_rsa (RSA-4096, mode 0600)
# → Tente l'enrollment → refusé → retente en backoff
5

Autoriser l'agent (pre-authorize)

# === Sur l'hôte cible : extraire la clef publique depuis la clef privée ===
openssl rsa -in /etc/secagent-minion/id_rsa -pubout 2>/dev/null

# === Sur le serveur relay ===
./secagent-server minions authorize my-host \
  --pubkey "$(ssh user@my-host openssl rsa -in /etc/secagent-minion/id_rsa -pubout 2>/dev/null)"

# → L'agent retente et se connecte. JWT stocké dans /etc/secagent-minion/token.jwt
1

Créer le namespace et les secrets

kubectl create namespace ansible-relay
kubectl create secret generic relay-secrets -n ansible-relay \
  --from-literal=JWT_SECRET_KEY="$(openssl rand -hex 32)" \
  --from-literal=ADMIN_TOKEN="$(openssl rand -hex 16)" \
  --from-literal=RSA_MASTER_KEY="$(openssl rand -hex 32)"
2

Déployer via Helm

helm upgrade --install ansible-relay ./helm/ansible-relay \
  -n ansible-relay \
  --set ingress.host=relay.example.com \
  --set replicaCount=3
3

Vérifier le déploiement

kubectl get pods -n ansible-relay
kubectl exec -n ansible-relay deploy/relay-api -- \
  secagent-server server status --format table
4

Gérer les agents et les clefs

kubectl exec -n ansible-relay deploy/relay-api -- \
  secagent-server minions list --format table

# Rotation JWT zero downtime
kubectl exec -n ansible-relay deploy/relay-api -- \
  secagent-server security keys rotate --grace 24h