L’agent IA que vous avez bâti dans les tutoriels précédents marche, propose des verdicts utiles et tient en moins de cinq fichiers Python. Mais tel quel, il n’est pas prêt pour la production. Le bulletin CERTFR-2026-ACT-016 de l’ANSSI rappelle que les greffons d’un agent s’exécutent avec les privilèges de l’application hôte et qu’un seul prompt mal intentionné peut déclencher une cascade d’actions système. Ce tutoriel referme la boucle : isoler le processus de l’agent, restreindre les outils MCP qu’il peut appeler, journaliser chaque prompt et chaque réponse dans un stockage append-only externe, et automatiser le rollback en cas de comportement anormal.
L’objectif n’est pas la perfection mais une défense en profondeur réaliste : si l’agent est compromis (injection de prompt réussie, fuite de clé LLM, modèle manipulé), le périmètre de dégâts reste borné, l’incident est détectable, et la restauration prend des minutes plutôt que des jours. Toutes les recettes sont implémentables sur Linux moderne avec systemd, Docker et un coffre de secrets standard.
Pour situer cet article
Ce tutoriel suppose que vous avez un agent fonctionnel — typiquement celui construit dans Triage SIEM par un agent IA — workflow LangChain et Wazuh pas à pas. Pour le contexte général sur la cybersécurité agentique en 2026, référez-vous au guide principal Cybersécurité agentique en 2026 : SOC IA, triage SIEM, riposte automatisée.
Prérequis
- Une machine Linux moderne (Debian 12, Ubuntu 24.04 ou RHEL 9) avec systemd, Docker 24+ et accès root.
- L’agent Python opérationnel et son code dans un dépôt git.
- Un coffre de secrets : HashiCorp Vault open source, ou à défaut
systemd-credspour les déploiements modestes. - Un stockage append-only accessible : MinIO en mode WORM, S3 avec Object Lock, ou un serveur syslog dédié avec disque en lecture seule.
- Niveau attendu : avancé. Vous êtes à l’aise avec systemd, les capacités Linux, les seccomp profiles et Docker.
- Temps estimé : 4 à 5 heures pour un déploiement complet et testé.
Étape 1 — Créer un compte de service dédié
Premier réflexe : l’agent ne tourne jamais sous root, ni sous votre compte personnel, ni sous un compte applicatif partagé. On crée un utilisateur système nommé aiagent, sans shell de connexion, avec un répertoire de travail propre dont les permissions interdisent tout accès en lecture aux autres utilisateurs. C’est la règle minimale du moindre privilège côté UNIX.
sudo useradd --system --home /opt/aiagent --shell /usr/sbin/nologin aiagent
sudo install -d -o aiagent -g aiagent -m 0750 /opt/aiagent
sudo install -d -o aiagent -g aiagent -m 0700 /opt/aiagent/state
sudo install -d -o aiagent -g aiagent -m 0700 /opt/aiagent/logs
sudo cp -r ~/wazuh-triage-agent/* /opt/aiagent/
sudo chown -R aiagent:aiagent /opt/aiagent
sudo find /opt/aiagent -type d -exec chmod 0750 {} \;
sudo find /opt/aiagent -type f -exec chmod 0640 {} \;
Le mode 0700 sur state et logs garantit qu’aucun autre utilisateur, même administrateur d’application non root, ne peut lire les journaux d’audit ou les curseurs de traitement. Vérifiez avec sudo -u aiagent ls -la /opt/aiagent que tout est lisible par l’agent et seulement par lui. Ce nettoyage de permissions paraît trivial mais c’est ce qui empêche un autre service compromis sur la même machine d’aller lire vos clés ou votre historique de prompts.
Étape 2 — Sortir les secrets du fichier .env
Le fichier .env du tutoriel précédent était pratique pour démarrer, dangereux en production : un attaquant qui obtient la lecture du répertoire récupère toutes les clés. La parade est systemd-creds ou Vault. Pour rester simple et autonome, on utilise systemd-creds qui chiffre le secret au repos avec une clé dérivée du TPM ou d’un fichier de clé séparé.
sudo systemd-ask-password "Wazuh password:" | \
sudo systemd-creds encrypt --name=wazuh_pass - /etc/credstore.encrypted/wazuh_pass.cred
sudo systemd-ask-password "LLM API key:" | \
sudo systemd-creds encrypt --name=llm_api_key - /etc/credstore.encrypted/llm_api_key.cred
Les fichiers .cred ne sont déchiffrables qu’à l’exécution par systemd, dans l’environnement du service ciblé. Vous référencerez ces credentials dans l’unité systemd à l’étape suivante. Côté Python, vous lirez le contenu via la variable d’environnement CREDENTIALS_DIRECTORY que systemd injecte. Cette mécanique élimine totalement les secrets du dépôt git et du système de fichiers en clair.
Étape 3 — Conteneuriser l’agent
Conteneuriser l’agent ne remplace pas les autres garde-fous mais ajoute une couche d’isolation utile : système de fichiers en lecture seule, pas de capacités Linux superflues, image immuable signée à chaque release. On part d’une image Python officielle minimale et on n’installe que le strict nécessaire.
# Dockerfile
FROM python:3.12-slim AS base
RUN useradd --system --home /app --shell /usr/sbin/nologin aiagent
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt && \
pip check
COPY --chown=aiagent:aiagent . .
USER aiagent
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
ENTRYPOINT ["python", "poll.py"]
Trois principes ici. L’image part d’une distribution slim, sans compilateur, sans curl/wget. L’utilisateur aiagent existe aussi dans le conteneur, et tout le code lui appartient. PYTHONDONTWRITEBYTECODE évite la création de fichiers .pyc qui briseraient l’immuabilité du système de fichiers. Compilez avec docker build -t aiagent:1.0.0 . et signez l’image avec cosign si vous utilisez un registry sérieux ; la signature sera vérifiée par votre orchestrateur avant déploiement.
Étape 4 — Restreindre les capacités du conteneur
Un conteneur par défaut a plus de capacités qu’il n’en faut. On lance l’agent avec un système de fichiers racine en lecture seule, sans accès au réseau hôte, sans capacités Linux étendues, et sans la possibilité d’élever ses privilèges. C’est l’application directe des recommandations Docker hardening de l’ANSSI et des CIS Benchmarks Docker.
docker run -d \
--name aiagent \
--read-only \
--tmpfs /tmp:size=64M,mode=1777 \
--cap-drop=ALL \
--security-opt=no-new-privileges:true \
--security-opt=seccomp=/etc/aiagent/seccomp.json \
--network=aiagent-net \
--memory=512m --cpus=1 \
--user 999:999 \
-v /opt/aiagent/state:/app/state:rw \
-v /opt/aiagent/logs:/app/logs:rw \
-e WAZUH_API_URL=https://wazuh-manager.local:55000 \
-e LLM_PROVIDER=anthropic \
-e WAZUH_API_PASS_FILE=/run/secrets/wazuh_pass \
-e ANTHROPIC_API_KEY_FILE=/run/secrets/llm_api_key \
--secret source=wazuh_pass,target=wazuh_pass \
--secret source=llm_api_key,target=llm_api_key \
aiagent:1.0.0
Le réseau aiagent-net est un réseau Docker dédié dont les règles iptables n’autorisent que les flux sortants vers le manager Wazuh et l’API LLM choisie. Tout le reste est interdit, y compris les requêtes DNS hors du résolveur interne — c’est ce qui empêche un agent compromis d’exfiltrer des données vers un serveur externe quelconque. Vérifiez avec docker exec aiagent curl -m 3 https://example.com qui doit échouer en timeout, alors que curl https://wazuh-manager.local:55000 doit aboutir.
Étape 5 — Écrire un profil seccomp ciblé
Le profil seccomp par défaut de Docker bloque déjà une cinquantaine d’appels système dangereux. On peut aller plus loin pour un agent Python qui n’a besoin que d’un sous-ensemble très réduit. Au lieu de réinventer le profil, on part du profil par défaut et on retire explicitement les appels qui pourraient servir à une escalade : ptrace, perf_event_open, setns, unshare.
# /etc/aiagent/seccomp.json (extrait — fusionné avec default Docker)
{
"defaultAction": "SCMP_ACT_ERRNO",
"syscalls": [
{
"names": ["ptrace", "perf_event_open", "setns", "unshare", "kexec_load",
"init_module", "delete_module", "reboot", "swapon", "swapoff",
"mount", "umount2", "pivot_root"],
"action": "SCMP_ACT_ERRNO"
}
]
}
Pour la base, copiez le profil par défaut depuis le dépôt Docker (https://github.com/moby/moby/blob/master/profiles/seccomp/default.json) et fusionnez-y la section ci-dessus. Testez l’agent en exécutant ses cas nominaux : si vous voyez des erreurs EPERM sur des appels légitimes, ajoutez-les explicitement avec SCMP_ACT_ALLOW. C’est itératif mais le résultat est un conteneur qui ne peut littéralement pas exécuter une grande partie des techniques d’escalade locales.
Étape 6 — Restreindre les outils MCP par allowlist
Si votre agent expose des outils via le Model Context Protocol, vous avez probablement plusieurs serveurs MCP installés (Wazuh, VirusTotal, MISP, etc.). En production, l’agent ne doit voir que les serveurs qui lui sont explicitement autorisés, et chaque serveur ne doit exposer que les méthodes dont l’agent a besoin. C’est l’équivalent d’un RBAC pour les outils.
# mcp_config.yml — chargé par le bootstrap de l'agent
mcp_servers:
wazuh:
url: stdio:///opt/mcp/wazuh-server
allowed_tools:
- get_alert
- list_alerts_for_agent
denied_tools:
- delete_alert
- update_rule
- admin_action
virustotal:
url: https://mcp.virustotal.local
allowed_tools:
- lookup_hash
- lookup_ip
rate_limit_per_min: 30
policies:
default_action: deny
log_every_call: true
forbid_dynamic_load: true
La règle default_action: deny est non négociable : tout outil non explicitement listé est refusé, et la tentative est journalisée. forbid_dynamic_load empêche un agent compromis d’enregistrer un nouveau serveur MCP à la volée — c’est exactement le scénario d’attaque évoqué dans le bulletin ANSSI. Côté code Python, votre wrapper LangChain doit lire ce fichier au démarrage et refuser tout appel qui n’est pas dans la liste, sans même envoyer la requête au modèle.
Étape 7 — Pousser l’audit vers un stockage append-only
Le fichier audit.jsonl local est utile mais perdable : un agent compromis pourrait l’écraser, et un disque saturé pourrait le tronquer. La parade est d’envoyer chaque entrée d’audit, en temps quasi réel, vers un stockage externe en mode WORM (Write Once Read Many). MinIO open source supporte le mode Object Lock compatible S3 — c’est l’option la plus simple pour un déploiement autonome.
# shipping.py — daemon léger lancé en sidecar
import time, json, pathlib, os, datetime
import boto3
LOG = pathlib.Path("/app/logs/audit.jsonl")
CURSOR = pathlib.Path("/app/state/audit.cursor")
BUCKET = "aiagent-audit"
s3 = boto3.client("s3",
endpoint_url=os.environ["MINIO_URL"],
aws_access_key_id=os.environ["MINIO_KEY"],
aws_secret_access_key=open(os.environ["MINIO_SECRET_FILE"]).read().strip(),
)
def cursor() -> int:
return int(CURSOR.read_text().strip()) if CURSOR.exists() else 0
while True:
with LOG.open() as f:
f.seek(cursor())
chunk = f.read()
if chunk:
day = datetime.datetime.utcnow().strftime("%Y-%m-%d")
key = f"{day}/{int(time.time()*1000)}.jsonl"
s3.put_object(
Bucket=BUCKET, Key=key, Body=chunk.encode("utf-8"),
ObjectLockMode="COMPLIANCE",
ObjectLockRetainUntilDate=datetime.datetime.utcnow() + datetime.timedelta(days=365),
)
CURSOR.write_text(str(f.tell()))
time.sleep(10)
Le mode COMPLIANCE de MinIO garantit qu’aucun objet ne peut être effacé ou modifié pendant la durée de rétention, même par un compte administrateur. Si un attaquant compromet l’agent, ses traces dans MinIO restent intactes et exploitables pour l’investigation. Couplez cela à une politique IAM stricte : le compte utilisé par shipping.py a uniquement le droit PutObject, jamais DeleteObject ni BypassGovernanceRetention.
Étape 8 — Mettre en place un kill switch et un rollback
Quand un agent dérive — pic d’appels, taux de fermeture anormal, prompt système modifié hors versionning — il faut pouvoir l’arrêter en quelques secondes et revenir à un état connu. Un service systemd dédié, qu’un opérateur peut activer à distance via Ansible ou un simple SSH, suffit.
# /etc/systemd/system/aiagent.service
[Unit]
Description=AI Triage Agent
After=docker.service network.target
Requires=docker.service
[Service]
Type=simple
ExecStart=/usr/bin/docker start -a aiagent
ExecStop=/usr/bin/docker stop -t 5 aiagent
LoadCredentialEncrypted=wazuh_pass:/etc/credstore.encrypted/wazuh_pass.cred
LoadCredentialEncrypted=llm_api_key:/etc/credstore.encrypted/llm_api_key.cred
Restart=on-failure
RestartSec=10s
StartLimitIntervalSec=300
StartLimitBurst=3
[Install]
WantedBy=multi-user.target
Le kill switch est un simple systemctl stop aiagent. Le rollback consiste à redéployer l’image taguée précédente après un cosign verify : docker rm aiagent && docker run ... aiagent:0.9.5 ... avec exactement les mêmes options de sécurité que pour la version actuelle. Documentez ces deux commandes dans le runbook du SOC, testez-les en simulation tous les trimestres, et chronométrez : un opérateur formé doit pouvoir effectuer un rollback complet en moins de cinq minutes.
Étape 9 — Auditer la configuration finale
Une dernière vérification globale s’impose avant de déclarer l’agent prêt. La checklist ci-dessous résume les contrôles à passer une à une — chacun doit être vert, sinon vous avez une régression à corriger.
- L’agent ne tourne pas sous root (
docker inspect aiagent --format '{{.Config.User}}'renvoieaiagentou999:999). - Le système de fichiers racine est en lecture seule (
--read-only). - Aucune capacité Linux n’est ajoutée au-delà du strict minimum (
--cap-drop=ALL). - L’option
no-new-privilegesest active. - Le profil seccomp custom est chargé.
- Le réseau Docker dédié n’autorise que les flux vers Wazuh et l’API LLM.
- Aucun secret n’apparaît en clair dans
docker inspectni dans le dépôt git. - L’audit est poussé vers MinIO avec ObjectLock actif.
- Le service systemd est activé et le kill switch testé.
- L’image Docker est signée et la signature vérifiée à chaque déploiement.
Pour automatiser cette checklist, écrivez un petit script bash que vous lancez après chaque déploiement et dont la sortie est ingérée par votre SIEM. Toute régression devient ainsi détectée le jour même, pas trois mois plus tard pendant un audit.
Étape 10 — Tester un scénario d’attaque réaliste
La validation finale est un exercice red team ciblé sur l’agent. Soumettez à votre passerelle un payload d’injection de prompt qui tente d’invoquer un outil non listé dans mcp_config.yml. Le comportement attendu : l’outil est refusé, l’événement est journalisé, votre meta-supervision (étape 5 du tutoriel précédent) lève une alerte. Si tout cela se produit comme prévu, vous avez l’élément central d’un agent prêt pour la production.
Recommencez avec d’autres scénarios : exfiltration tentée vers un domaine externe (doit être bloquée par le réseau Docker), tentative d’écriture dans /etc depuis le conteneur (doit échouer en read-only), tentative de modification de l’audit local (peut réussir mais l’attaque est trop tardive — l’enregistrement est déjà parti vers MinIO). Documentez chaque test et son résultat dans le dépôt git du projet.
Erreurs fréquentes
| Erreur | Cause | Solution |
|---|---|---|
| Agent root dans le conteneur | Image Python officielle par défaut | Créer explicitement un user et le déclarer avec USER |
Secrets visibles dans docker inspect | Variables d’environnement classiques | Utiliser --secret ou LoadCredential systemd |
| Audit perdu après crash | Buffer Python non flush | PYTHONUNBUFFERED=1 et flush=True sur chaque write |
| MinIO accepte la suppression | ObjectLock pas activé à la création du bucket | Recréer le bucket avec --with-lock, vérifier mc retention info |
| Restrictions réseau trop larges | Réseau bridge par défaut | Réseau Docker dédié avec règles iptables explicites |
| seccomp casse l’agent | Profil trop restrictif | Démarrer avec le profil par défaut, ajouter les blocages un à un |
| Rollback non testé | Procédure dans la doc, jamais exécutée | Exercice trimestriel obligatoire avec chronomètre |
Tutoriels liés
- Triage SIEM par un agent IA — workflow LangChain et Wazuh pas à pas
- Détecter une attaque pilotée par IA — prompt injection, deepfake vocal, malware LLM-aware
- Créer un serveur MCP : architecture, primitives, premier déploiement
Questions fréquentes
Faut-il vraiment Docker ou un conteneur Podman fait l’affaire ?
Podman est même préférable dans plusieurs scénarios : il fonctionne en mode rootless par défaut, ce qui supprime un vecteur d’escalade dès la base. Toutes les options listées ici (--cap-drop, --security-opt, --read-only) ont leur équivalent direct sous Podman. Le choix dépend surtout de votre orchestrateur et de l’écosystème en place.
Pourquoi ne pas utiliser Kubernetes avec NetworkPolicy et PodSecurity ?
Si vous opérez déjà Kubernetes, c’est l’option recommandée — vous gagnez les NetworkPolicies pour la segmentation réseau, le PodSecurity Admission pour l’application des contrôles, et les ServiceAccounts pour l’identité. Toute la logique de ce tutoriel se transpose : readOnlyRootFilesystem: true, allowPrivilegeEscalation: false, capabilities.drop: [ALL], et un seccomp profile via seccompProfile.type: Localhost.
Combien de temps de rétention pour les audits MinIO ?
L’AI Act européen impose, à l’article 26(6), une rétention minimale de six mois des journaux pour les déployeurs de systèmes à haut risque. NIS2 prévoit pour les entités concernées une rétention typique de six à dix-huit mois selon la catégorie (importante ou essentielle) et l’analyse de risque retenue. Une rétention de 365 jours par défaut couvre la plupart des cas, à ajuster selon votre cadre réglementaire spécifique et l’analyse forensique nécessaire à votre métier.
L’agent peut-il quand même être compromis avec toutes ces protections ?
Oui, aucune défense n’est absolue. L’objectif de la défense en profondeur est qu’une seule faille ne suffise pas, que l’incident soit détectable rapidement, et que la remise en état soit rapide. Avec ce dispositif, un attaquant doit cumuler plusieurs vulnérabilités pour produire un dommage significatif, et chacune de ses tentatives laisse une trace immuable.