Docker AppArmor et seccomp : le renforcement minimal qui compte

Cet article vous a aidé ?

Il n’est pas nécessaire de « faire de la sécurité » comme une tournée de conformité théâtrale. Vous avez besoin de conteneurs qui se comportent correctement quand ils sont compromis — car tôt ou tard, l’un le sera. La question n’est pas de savoir si votre application contient des bugs. La question est de savoir si un bug devient un ticket ennuyeux ou une prise d’otage durant le week-end impliquant un exploit du noyau et un attaquant très satisfait de lui.

AppArmor et seccomp sont deux des rares contrôles de durcissement des conteneurs qui réduisent régulièrement le rayon d’impact en production. Ce n’est pas glamoureux et ça n’impressionnera pas votre équipe achats. En revanche, ces contrôles bloquent de vraies techniques : appels système étranges, accès fichiers bizarres et moments « pourquoi ce conteneur d’API a-t-il besoin de monter des systèmes de fichiers ? ».

Ce que signifie réellement le « renforcement minimal »

En sécurité des conteneurs, « renforcement minimal » ne veut pas dire « activer toutes les options et prier ». C’est un petit ensemble de contrôles qui :

  • S’appliquent largement à des applications hétérogènes.
  • Échouent bruyamment (vous pouvez détecter les ruptures) et échouent en sécurité (ils ne suppriment pas silencieusement les protections).
  • Réduisent la surface d’attaque du noyau — car le noyau est la ressource partagée que vous ne pouvez pas patcher en redéployant un conteneur.
  • Sont diagnostiquables opérationnellement sous pression à 02:00.

AppArmor et seccomp remplissent ces critères. Ils ne se substituent pas au fait d’exécuter sans privilèges, de supprimer des capacités Linux, d’utiliser des systèmes de fichiers en lecture seule et d’une gestion appropriée des secrets. Mais si vous me demandiez de choisir deux contrôles qui changent l’issue après une compromission, je choisirais ceux-ci.

Le renforcement minimal qui compte pour la plupart des environnements Docker ressemble à ceci :

  • Utiliser le profil seccomp par défaut de Docker (ne pas le désactiver).
  • Utiliser un profil AppArmor en mode enforce (ne pas exécuter en unconfined).
  • Éviter –privileged et éviter un seccomp=unconfined global.
  • Quand une application a réellement besoin d’exceptions, que celles-ci soient explicites, documentées et testées.

Il y a une raison pour laquelle ces deux contrôles reviennent dans les post-mortems : ils font souvent la différence entre « l’attaquant a obtenu un shell dans un conteneur » et « l’attaquant a obtenu root sur l’hôte ».

Faits et historique importants (court et concret)

Les contrôles de sécurité ont plus de sens quand on sait pourquoi ils existent. Quelques points d’ancrage :

  1. AppArmor est paru au milieu des années 2000 et est devenu un paramètre par défaut courant sur Ubuntu bien avant que les conteneurs ne deviennent populaires ; il a été conçu pour confiner des démons par règles basées sur des chemins.
  2. SELinux vs AppArmor est en grande partie une différence de modèle de politique et d’outillage ; Docker s’intègre aux deux, mais AppArmor est souvent plus simple à exploiter dans des flottes centrées Debian/Ubuntu.
  3. seccomp a commencé comme « secure computing mode » autour de Linux 2.6.12, initialement très restrictif ; seccomp-bpf l’a rendu pratique en permettant des filtres d’appels système basés sur BPF.
  4. Docker a activé seccomp par défaut il y a des années ; de nombreuses équipes le désactivent encore pendant des phases « juste faire fonctionner » et oublient de le réactiver.
  5. L’isolation des conteneurs n’est pas une frontière VM ; les conteneurs partagent le noyau de l’hôte. C’est pourquoi le filtrage d’appels système a une valeur disproportionnée.
  6. Les namespaces et les cgroups ne sont pas des politiques de sécurité ; ce sont des mécanismes d’isolation. AppArmor et seccomp sont des politiques qui peuvent dire « non ».
  7. La plupart des échappements de conteneurs ciblent le noyau ; si vous pouvez réduire la surface d’appels système et bloquer des interfaces noyau étranges, vous augmentez vos chances.
  8. AppArmor est « basé sur les chemins », ce qui est à la fois sa force (lisible) et un piège (astuces de renommage/montage si vous êtes négligent avec les montages et la médiation).
  9. Les filtres seccomp sont par processus ; si un processus est démarré sans le filtre (ou si vous le mettez en unconfined), vous n’avez pas de seconde chance plus tard.

Modèle de menace : ce que ces contrôles arrêtent (et ce qu’ils n’arrêtent pas)

Ce pour quoi AppArmor est utile

AppArmor est un contrôle d’accès obligatoire. Il restreint ce qu’un processus peut faire même si ce processus se croit root. Dans le monde des conteneurs, c’est utile quand une fuite de conteneur commence par « obtenir root à l’intérieur du conteneur » (ce qui est souvent trivial) et continue par « toucher des ressources hôtes ». AppArmor peut bloquer la lecture/écriture de fichiers, les montages, ptrace, les signaux et les interactions avec des interfaces noyau spécifiques.

C’est aussi un bon « fil d’alerte politique ». Quand vous voyez un refus, il correspond généralement à un comportement que vous devriez au moins comprendre.

Ce pour quoi seccomp est utile

seccomp filtre les appels système : il peut autoriser, refuser ou provoquer un trap. Il ne s’agit pas d’empêcher « rm -rf ». Il s’agit d’empêcher des appels système communs aux chaînes d’exploitation : création de namespaces, keyrings, manipulation de paquets bruts, événements perf, montages de systèmes de fichiers, etc. Le profil seccomp par défaut de Docker bloque un certain nombre d’appels système à haut risque que les applications typiques n’utilisent pas.

Ce qu’ils ne font pas

  • Ils ne corrigent pas vos vulnérabilités applicatives. Ils les contiennent.
  • Ils ne remplacent pas le principe du moindre privilège. Si vous donnez au conteneur des montages d’hôte ou le mode privilégié, vous contournez une grande partie de leur valeur.
  • Ils n’empêchent pas l’exfiltration de données via des canaux autorisés (HTTP sortant, DNS, etc.). C’est le rôle des politiques réseau et des contrôles d’egress.
  • Ils n’empêchent pas les abus logiques comme « exporter toutes les données clients » si votre API le permet.

Idée paraphrasée (attribuée) : « Werner Vogels est connu pour promouvoir l’idée que “tout échoue, tout le temps”, donc on conçoit pour la contention et la récupération. » Cet état d’esprit s’applique ici : supposez une compromission, concevez le rayon d’impact.

AppArmor dans Docker : profils, modes et valeurs raisonnables

Comment Docker utilise AppArmor

Sur des hôtes avec AppArmor activé, Docker peut appliquer un profil AppArmor à chaque conteneur. Si vous ne faites rien, vous obtenez souvent un profil par défaut comme docker-default (selon la distribution et le packaging Docker). Si AppArmor est désactivé, ou si Docker ne peut pas charger les profils, les conteneurs peuvent s’exécuter unconfined.

« Unconfined » ne signifie pas « ok ». Cela signifie que vous avez supprimé l’un des rares garde-fous qui fonctionnent même après que le processus soit root.

Enforce vs complain

AppArmor a deux modes opérationnellement pertinents :

  • enforce : refuser les violations. C’est ce que vous voulez en production.
  • complain : autoriser mais journaliser. C’est la façon de collecter des données pour écrire un profil sans casser les charges de travail.

Si vous essayez de déployer AppArmor, commencez par le mode complain dans un environnement de staging qui reçoit du vrai trafic. Puis passez en enforce avec une fenêtre de modification et un plan de rollback.

À quoi ressemble un profil « utile minimal »

Pour Docker, « utile minimal » signifie généralement : conserver le défaut Docker, et n’assouplir que là où vous avez des preuves solides. Les profils personnalisés sont une capacité ; ce sont aussi une obligation de maintenance.

Si vous devez écrire le vôtre, restez strict sur les montages, les périphériques bruts, ptrace et les chemins de fichiers dangereux. Soyez explicite sur les répertoires modifiables. Les conteneurs ne devraient pas pouvoir écrire des parties arbitraires du système de fichiers ; si c’est possible, vous l’apprendrez à vos dépens.

Signes opérationnels que AppArmor ne vous protège pas réellement

  • docker info affiche AppArmor désactivé.
  • Les conteneurs affichent AppArmorProfile: "" ou unconfined.
  • Aucune dénégation n’apparaît nulle part, même lors de tests connus-bad (c’est suspect, pas rassurant).

Blague #1 : AppArmor en mode complain ressemble à un agent de sécurité qui écrit seulement des entrées de journal très véhémentes. Formidable pour la recherche, pas pour arrêter quelqu’un.

seccomp dans Docker : filtrage des appels système sans s’auto-saboter

Le profil seccomp par défaut de Docker

Docker fournit un profil seccomp par défaut qui bloque des appels système historiquement risqués ou rarement nécessaires aux charges de travail conteneurisées classiques. Il n’est pas parfait et pas minimal ; il est pratique. Le profil par défaut a tendance à ne casser que des charges spécialisées (certains outils de tracing, certains runtimes inhabituels, certains outils de tuning de bases de données, astuces de conteneurs imbriqués) et même là vous pouvez généralement résoudre le problème sans passer en unconfined.

Comment se manifestent les échecs seccomp

Quand seccomp bloque quelque chose, vous voyez généralement l’un des cas suivants :

  • Le processus reçoit EPERM ou EACCES et journalise une erreur du type « operation not permitted ».
  • Le processus se fait tuer avec SIGSYS (« bad system call »). Cela arrive quand le filtre est configuré pour tuer.
  • Les logs d’audit montrent des événements seccomp si l’audit est configuré.

L’astuce de débogage est de corréler les erreurs applicatives avec les logs noyau/audit et la configuration du conteneur. Si vous commencez par désactiver seccomp au hasard, vous « corrigerez » le symptôme tout en créant une régression de sécurité que personne ne se souviendra dans six semaines.

Quand personnaliser seccomp

Ne personnalisez seccomp que lorsque vous avez une charge stable et une raison que vous pouvez formuler en une phrase : « Ce conteneur requiert l’appel système X parce que la fonctionnalité Y l’utilise. » Si vous ne pouvez pas l’expliquer, vous devinez. Deviner mène à finir avec unconfined partout.

Blague #2 : Lancer des conteneurs avec seccomp=unconfined revient à retirer les ceintures de sécurité parce qu’elles froissent la chemise.

Tâches pratiques : commandes, sorties et décisions (12+)

Voici les tâches de base que vous exécutez sur de vrais hôtes. Chaque tâche inclut : une commande, une sortie représentative, ce que cela signifie et la décision suivante.

Tâche 1 — Vérifier si Docker voit AppArmor et seccomp activés

cr0x@server:~$ docker info --format 'apparmor={{.SecurityOptions}}'
apparmor=[name=apparmor] seccomp=[name=seccomp,profile=default] cgroupns

Ce que cela signifie : Docker voit AppArmor et seccomp, et seccomp utilise le profil par défaut.

Décision : Bon état de base. Si AppArmor ou seccomp manque ici, arrêtez et corrigez la configuration de l’hôte avant de « durcir les conteneurs ».

Tâche 2 — Confirmer le support noyau AppArmor et son état

cr0x@server:~$ sudo aa-status
apparmor module is loaded.
55 profiles are loaded.
52 profiles are in enforce mode.
3 profiles are in complain mode.
0 processes are unconfined but have a profile defined.

Ce que cela signifie : Le module noyau est chargé et la plupart des profils sont en enforce.

Décision : Si le module n’est pas chargé ou si les profils ne sont pas appliqués, corrigez cela en priorité. Les « flags Docker » n’aideront pas si AppArmor n’est pas actif.

Tâche 3 — Vérifier si des conteneurs sont unconfined (AppArmor)

cr0x@server:~$ docker ps -q | xargs -r docker inspect --format '{{.Name}} AppArmor={{.AppArmorProfile}}'
/api AppArmor=docker-default
/worker AppArmor=docker-default
/metrics AppArmor=unconfined

Ce que cela signifie : Un conteneur tourne sans confinement AppArmor.

Décision : Traitez cela comme un défaut. Trouvez qui a mis --security-opt apparmor=unconfined (ou pourquoi Docker n’a pas pu appliquer un profil) et remédiez au problème.

Tâche 4 — Vérifier le mode seccomp par conteneur

cr0x@server:~$ docker ps -q | xargs -r docker inspect --format '{{.Name}} Seccomp={{.HostConfig.SecurityOpt}}'
/api Seccomp=[]
/worker Seccomp=[]
/metrics Seccomp=[seccomp=unconfined]

Ce que cela signifie : Deux conteneurs utilisent les valeurs par défaut ; un a seccomp explicitement en unconfined.

Décision : Supprimez l’exception sauf si elle est justifiée. Si vous devez la conserver, remplacez-la par un profil seccomp personnalisé qui n’ajoute que les appels système requis.

Tâche 5 — Trouver comment le conteneur a été démarré (pour tracer la source du changement)

cr0x@server:~$ docker inspect metrics --format 'Image={{.Config.Image}} SecurityOpt={{.HostConfig.SecurityOpt}} Privileged={{.HostConfig.Privileged}}'
Image=corp/metrics-exporter:2.7.1 SecurityOpt=[seccomp=unconfined apparmor=unconfined] Privileged=false

Ce que cela signifie : Quelqu’un a explicitement désactivé les deux mécanismes.

Décision : Traitez cela comme la désactivation de la vérification TLS. Ouvrez un ticket contre le manifeste de déploiement et exigez une justification et un propriétaire.

Tâche 6 — Valider que les capabilities du conteneur n’ont pas été « élargies utilement »

cr0x@server:~$ docker inspect api --format 'CapAdd={{.HostConfig.CapAdd}} CapDrop={{.HostConfig.CapDrop}}'
CapAdd=[] CapDrop=[ALL]

Ce que cela signifie : Ce conteneur supprime toutes les capabilities (bon). Il peut encore avoir certains comportements par défaut selon son exécution, mais c’est une posture explicite.

Décision : Quand vous limitez les capabilities, AppArmor/seccomp deviennent encore plus efficaces. Conserver ce modèle.

Tâche 7 — Détecter les conteneurs privilégiés (parce qu’ils percent tout)

cr0x@server:~$ docker ps -q | xargs -r docker inspect --format '{{.Name}} Privileged={{.HostConfig.Privileged}}'
/api Privileged=false
/worker Privileged=false
/buildkit Privileged=true

Ce que cela signifie : Un conteneur est privilégié. Cela peut être intentionnel (par ex. outils de build) mais c’est à haut risque.

Décision : Si c’est un système de build, isolez-le : nœuds dédiés, pas de secrets de production, surveillance agressive, et idéalement utilisez rootless ou une approche de build plus sûre.

Tâche 8 — Vérifier les logs noyau pour des dénis AppArmor

cr0x@server:~$ sudo dmesg -T | grep -i apparmor | tail -n 5
[Sat Jan  3 10:11:14 2026] audit: type=1400 audit(1704276674.123:781): apparmor="DENIED" operation="open" profile="docker-default" name="/proc/kcore" pid=29144 comm="cat" requested_mask="r" denied_mask="r" fsuid=0 ouid=0

Ce que cela signifie : Un processus à l’intérieur d’un conteneur confiné docker-default a tenté de lire /proc/kcore et a été refusé.

Décision : C’est généralement une bonne nouvelle : cela indique que le confinement est actif. Vérifiez si l’accès était attendu (rare) ou suspect (souvent).

Tâche 9 — Vérifier les logs d’audit pour les violations seccomp

cr0x@server:~$ sudo ausearch -m SECCOMP -ts recent | tail -n 3
time->Sat Jan  3 10:12:01 2026
type=SECCOMP msg=audit(1704276721.443:812): auid=4294967295 uid=0 gid=0 ses=4294967295 subj==unconfined pid=29210 comm="node" exe="/usr/local/bin/node" sig=31 arch=c000003e syscall=319 compat=0 ip=0x7f2c8e2f1a9d code=0x0

Ce que cela signifie : Un processus a déclenché un événement seccomp sur l’appel système 319 (exemple). Vous pourrez mapper les numéros d’appels système aux noms si nécessaire, mais l’événement est l’indice.

Décision : Corrélez avec le conteneur et le déploiement. Si c’est votre application qui plante, il vous faut un ajustement seccomp ciblé, pas une désactivation globale.

Tâche 10 — Vérifier quel profil seccomp Docker utilise par défaut (niveau démon)

cr0x@server:~$ sudo cat /etc/docker/daemon.json
{
  "log-driver": "journald",
  "live-restore": true
}

Ce que cela signifie : Pas d’override ; Docker utilisera son seccomp par défaut intégré sauf si un conteneur demande le contraire.

Décision : Bien. Si vous voyez "seccomp-profile": "unconfined" ou des motifs similaires (selon l’outillage), traitez cela comme un rollback d’urgence de la posture de sécurité.

Tâche 11 — Lancer un conteneur canari pour vérifier que l’application des règles n’est pas cassée silencieusement

cr0x@server:~$ docker run --rm --name aa-canary alpine:3.19 sh -c 'cat /proc/kcore; echo ok'
cat: can't open '/proc/kcore': Permission denied
ok

Ce que cela signifie : Le refus est attendu sous les défauts Docker/AppArmor habituels ; le conteneur continue de fonctionner.

Décision : Conservez ce test rapide lors du bootstrap d’un nœud ou après des mises à jour du noyau. S’il réussit soudainement, vous avez probablement perdu le confinement AppArmor.

Tâche 12 — Prouver que seccomp est actif avec un appel système généralement bloqué

cr0x@server:~$ docker run --rm alpine:3.19 sh -c 'apk add --no-cache strace >/dev/null; strace -e trace=bpf true'
strace: syscall bpf denied

Ce que cela signifie : L’appel système bpf est couramment bloqué par le profil seccomp par défaut ; strace signale le refus.

Décision : Si cela réussit de manière inattendue, quelqu’un a désactivé seccomp ou changé le profil. Si votre application légitime a besoin de bpf, vous êtes dans une niche — documentez et isolez.

Tâche 13 — Identifier le profil AppArmor appliqué au processus d’un conteneur en cours d’exécution

cr0x@server:~$ pid=$(docker inspect -f '{{.State.Pid}}' api); sudo cat /proc/$pid/attr/current
docker-default (enforce)

Ce que cela signifie : Le noyau montre que le processus est confiné sous docker-default en mode enforce.

Décision : C’est une vérification forte quand les métadonnées Docker sont trompeuses ou si vous suspectez des différences à l’exécution.

Tâche 14 — Détecter les conteneurs avec des montages risqués qui sapent la politique

cr0x@server:~$ docker inspect api --format '{{json .Mounts}}'
[{"Type":"bind","Source":"/srv/api/config","Destination":"/app/config","Mode":"ro","RW":false,"Propagation":"rprivate"}]

Ce que cela signifie : Un montage bind en lecture seule est utilisé pour la configuration. C’est raisonnable.

Décision : Si vous voyez des binds vers /, /var/run/docker.sock, /proc ou des chemins hôtes inscriptibles avec un large périmètre, corrigez cela en priorité ; aucun réglage de profil ne rendra cela sûr.

Tâche 15 — Repérer le piège « docker.sock dans un conteneur »

cr0x@server:~$ docker ps -q | xargs -r docker inspect --format '{{.Name}} {{range .Mounts}}{{.Destination}} {{end}}' | grep -F '/var/run/docker.sock'
/ci-runner /var/run/docker.sock

Ce que cela signifie : Un conteneur a accès au socket Docker, ce qui équivaut généralement à root sur l’hôte dans la plupart des configurations.

Décision : Traitez cela comme une charge de travail privilégiée. Isolez-la, restreignez qui peut la déployer et évitez de la mélanger avec des chemins de données de production.

Manuel de diagnostic rapide

Quand quelque chose casse après avoir activé AppArmor/seccomp, votre objectif est de trouver le goulot d’étranglement en minutes, pas en heures. Voici l’ordre qui fait gagner du temps.

Premier : confirmer que c’est bien AppArmor/seccomp (pas le réseau, pas le DNS, pas des permissions)

  • Recherchez « Operation not permitted » ou « Bad system call » dans les logs du conteneur.
  • Vérifiez les options de sécurité et profils du conteneur (docker inspect).
  • Vérifiez les logs noyau/audit pour des dénis (dmesg, ausearch).

Second : identifier quelle action est bloquée

  • Les dénis AppArmor mentionnent généralement une opération et un chemin (ex. operation="open" name="/proc/kcore").
  • Les événements seccomp mentionnent un numéro d’appel système (et parfois le nom selon l’outillage) et souvent SIGSYS.

Troisième : décider de la bonne catégorie de correction

  • Corriger l’application si elle fait quelque chose d’inutile (fréquent).
  • Corriger la configuration du conteneur (montages, capabilities, utilisateur) si le comportement de l’app est normal mais le conteneur est surprotégé au mauvais endroit.
  • Corriger le profil uniquement si c’est un besoin stable et justifié.
  • Isoler la charge si elle nécessite des pouvoirs dangereux (systèmes de build, outils eBPF, runtimes imbriqués).

Quatrième : tester avec la plus petite exception possible

Ne passez jamais du défaut à unconfined. Pour seccomp, autorisez un seul appel système. Pour AppArmor, autorisez un seul chemin ou capability. Puis redéployez et vérifiez que le refus disparaît et qu’aucune autre régression n’apparaît.

Trois mini-récits d’entreprise tirés du terrain

1) Incident causé par une fausse hypothèse : « C’est dans un conteneur, donc ça ne peut pas toucher l’hôte »

L’entreprise exécutait un ensemble de services « internes uniquement » dans Docker sur quelques hôtes Linux puissants. L’un de ces services était une API de conversion PDF. Vous voyez déjà la suite : documents fournis par les utilisateurs, un parseur avec un historique et un modèle de menace décrit comme « non exposé à Internet ». En réalité, il était joignable depuis plusieurs endroits car les réseaux internes ne sont que l’Internet avec un meilleur café.

L’équipe supposait que la containerisation suffisait. AppArmor était installé sur la distribution mais pas réellement appliqué aux conteneurs ; seccomp avait été désactivé des mois plus tôt parce qu’un autre service avait déclenché des problèmes lors d’une migration précipitée. Personne n’avait documenté la raison. La désactivation est devenue la valeur par défaut.

Un attaquant a exploité une faille de parsing pour obtenir une exécution de code dans le conteneur. De là, il a énuméré les montages et trouvé un bind monté en écriture vers un répertoire hôte utilisé pour des uploads partagés. Ce répertoire contenait une clé SSH oubliée par un ancien script d’automatisation. La clé appartenait à un compte qui, bien que non root, faisait partie du groupe pouvant parler au socket Docker sur certaines machines.

De « RCE in a container », ils sont passés à « contrôle du démon Docker », et ensuite la partie était finie : lancer un conteneur privilégié, monter le système de fichiers hôte, déposer un backdoor. L’analyse post-incident revenait sans cesse à une ligne : « Nous avons supposé que les conteneurs étaient une frontière. »

La correction n’a pas été héroïque. Elle a été ennuyeuse : réactiver seccomp par défaut, appliquer AppArmor, enlever les montages docker.sock et imposer une politique exigeant qu’une exception ait un propriétaire et une date d’expiration. L’exploit suivant a encore obtenu un shell dans un conteneur, mais il n’a pas pu faire le second saut. C’est la victoire.

2) Optimisation qui s’est retournée contre eux : « Notre profil seccomp personnalisé est plus rapide »

Un groupe focalisé sur la performance a estimé que le profil seccomp par défaut de Docker était « trop gros » et donc « forcément lent ». Ils ont écrit un profil personnalisé destiné à n’autoriser que les appels système utilisés par leurs services Go. Les tests initiaux semblaient bons : les benchmarks de latence étaient légèrement meilleurs sur une charge synthétique, et personne n’a vu d’erreurs en staging.

Puis il y a eu la production. Le premier symptôme n’était pas lié à la sécurité ; c’était la disponibilité. Un sous-ensemble de conteneurs commença à planter sous de fortes charges, mais seulement sur des nœuds avec un noyau plus récent. La signature du plantage mélangeait « bad system call » et erreurs runtime cryptiques. L’ingénieur d’astreinte a fait ce que font les ingénieurs sous stress : il a désactivé seccomp pour les services en panne afin de restaurer la disponibilité. Les incidents ont cessé. La « correction » est restée.

La cause racine était ennuyeuse : le runtime Go et libc faisaient des appels système qui n’étaient pas apparus dans leurs tests restreints, y compris des appels liés à la gestion de threads et à des interfaces noyau plus récentes. Leur profil avait été « optimisé » sur un instantané de comportement, pas sur le comportement réel à travers des versions de noyau, des changements de libc et des modes opérationnels comme DNS et TLS variables.

La leçon à long terme n’a pas été « ne jamais personnaliser ». C’était « ne personnalisez pas à moins de pouvoir le maintenir ». Ils sont revenus au profil seccomp par défaut de Docker pour les charges générales et ont conservé un profil personnalisé soigneusement maintenu pour un service spécialisé, détenu par l’équipe concernée et testé en CI contre plusieurs noyaux. Le léger gain de benchmark ne valait pas la fragilité opérationnelle.

3) Pratique ennuyeuse mais correcte qui a sauvé la mise : tests canary et défauts appliqués

Une autre organisation exploitait des dizaines de clusters Kubernetes mais disposait aussi d’une petite flotte d’hôtes Docker pour des jobs batch legacy. Ces hôtes étaient fréquemment mis à jour — patchs noyau, mises à jour Docker, churn habituel. L’équipe SRE avait une habitude simple : chaque bootstrap de nœud lançait un conteneur canary qui essayait quelques actions « qui devraient être refusées » et confirmait que les refus attendus apparaissaient dans les logs.

Une semaine, après une mise à jour système apparemment routinière, le canary a commencé à réussir des actions qui auraient dû être bloquées. Aucun signal d’alarme n’avait encore été déclenché, car les jobs de production tournaient toujours. Mais le test canary a échoué à l’admission du nœud, donc de nouvelles charges n’y ont pas été planifiées. L’hôte a été mis en quarantaine automatiquement.

Il s’est avéré qu’AppArmor avait été désactivé au démarrage à cause d’une incohérence de paramètres noyau lors de la mise à jour. Docker continuait de tourner, les conteneurs aussi, et tout le monde aurait supposé que « tout va bien ». Mais « tout va bien » l’était seulement jusqu’à la première compromission. Le canary a détecté une régression silencieuse qui aurait pu durer des mois.

L’équipe a corrigé la configuration de boot, réactivé AppArmor et retiré la quarantaine du nœud. Aucun rapport d’incident, aucun impact client. Juste une de ces victoires invisibles qui n’existent que parce que quelqu’un a insisté sur une boucle de contrôle ennuyeuse et a refusé de se convaincre que « ça marche probablement ».

Erreurs courantes : symptômes → cause racine → correctif

1) Symptom : le conteneur sort immédiatement avec « Bad system call »

Cause racine : seccomp a bloqué un appel système et l’action est configurée pour tuer ou le processus ne gère pas EPERM proprement.

Correctif : Confirmer que seccomp est actif et inspecter les logs d’audit (ausearch -m SECCOMP). Ajouter l’appel système spécifique à un profil seccomp personnalisé pour cette charge, ou mettre à jour l’app/runtime. Ne pas définir seccomp=unconfined comme « correctif temporaire » à moins d’en planifier la suppression.

2) Symptom : l’app ne peut plus lire un fichier qu’elle lisait auparavant (alors que les permissions Unix semblent correctes)

Cause racine : AppArmor a refusé un accès basé sur le chemin même si les permissions Unix l’autorisent.

Correctif : Vérifier dmesg pour des dénis AppArmor, identifier le profil, puis soit ajuster le profil soit déplacer le fichier dans un chemin autorisé (souvent préférable). Vérifier les montages : un chemin différent dans le conteneur peut causer un décalage de politique.

3) Symptom : « mount: permission denied » à l’intérieur du conteneur

Cause racine : AppArmor et/ou seccomp ont bloqué des opérations liées aux montages ; il manque aussi probablement des capabilities comme CAP_SYS_ADMIN.

Correctif : Demander pourquoi le conteneur doit monter quelque chose. La plupart ne devraient pas. Pour des cas légitimes (rares), isolez la charge et accordez explicitement ce dont elle a besoin avec un périmètre strict.

4) Symptom : les outils de debug ne fonctionnent pas (strace, perf, ptrace)

Cause racine : C’est souvent le point. AppArmor et seccomp restreignent couramment les interfaces de traçage car elles sont puissantes dans les exploits.

Correctif : Utiliser des images de debug dédiées dans des environnements isolés, ou exécuter les outils de debug sur l’hôte avec l’accès approprié. Si vous devez activer le tracing en production, le faire pour un conteneur/pod de debug court avec un accès audité et un plan clair de suppression.

5) Symptom : « fonctionne sur un nœud, échoue sur un autre » après durcissement

Cause racine : Les versions du noyau diffèrent, les versions de Docker diffèrent, ou les ensembles de profils AppArmor diffèrent. Les profils seccomp personnalisés sont particulièrement sensibles aux variantes d’exécution.

Correctif : Standardiser les images de nœuds, pinner et tester les profils, et ajouter des tests canary. Considérez la dérive des hôtes comme un problème de sécurité et de fiabilité, pas seulement d’hygiène.

6) Symptom : tout tourne, mais vous ne voyez aucun refus nulle part

Cause racine : Le logging/l’audit n’est pas activé ou AppArmor/seccomp ne sont pas réellement appliqués (conteneurs unconfined).

Correctif : Vérifier via /proc/<pid>/attr/current, docker info et tests canary. Le silence n’est pas une preuve de sécurité.

7) Symptom : quelqu’un « corrige » un incident en ajoutant –privileged

Cause racine : Manque de discipline opérationnelle et voie d’escalade manquante pour l’ajustement des profils. Le mode privilégié devient la solution de facilité.

Correctif : Faire en sorte que les déploiements privilégiés requièrent une approbation explicite, isoler les charges privilégiées et construire une voie rapide pour des changements de profil ciblés avec des tests.

Listes de contrôle / plan pas à pas

Pas à pas : déploiement du renforcement minimal qui ne vous ruinera pas la semaine

  1. Inventorier la posture actuelle. Identifier les conteneurs avec apparmor=unconfined, seccomp=unconfined, le mode privilégié et les montages docker.sock.
  2. Corriger le pire en premier. Retirer les montages docker.sock des charges non-build. Éliminer les conteneurs privilégiés dans le plan de données de production.
  3. Activer les contrôles hôtes. S’assurer qu’AppArmor est chargé et en enforce. S’assurer que Docker rapporte seccomp par défaut activé.
  4. Test canary sur chaque nœud. Utiliser des vérifications rapides de refus (comme /proc/kcore et un appel système bloqué) pendant le bootstrap et après les mises à jour.
  5. Déployer les défauts seccomp largement. Le seccomp par défaut doit être activé partout sauf exception documentée.
  6. Déployer l’enforce AppArmor. S’assurer que les conteneurs ne sont pas unconfined. Préférer docker-default sauf forte raison.
  7. Gérer les exceptions avec ownership. Chaque unconfined ou profil personnalisé doit avoir un propriétaire, une raison et une revue périodique.
  8. Automatiser la vérification. Vérifications CI pour les manifests de déploiement : échouer si ils définissent unconfined sans allowlist.
  9. Opérationnaliser le débogage. Former l’astreinte à trouver les dénis et événements seccomp ; écrire un runbook qui commence par les logs, pas par des conjectures.

Checklist : options de lancement de conteneur à surveiller

  • N’utiliser --privileged que si le nœud est dédié et que la charge est explicitement de confiance élevée.
  • N’utiliser --security-opt seccomp=unconfined que comme mitigation temporaire avec ticket et expiration.
  • N’utiliser --security-opt apparmor=unconfined en production. Corriger le profil à la place.
  • Préférer --read-only avec des montages écriture explicites pour l’état.
  • Retirer les capabilities par défaut ; les ajouter seulement si vous pouvez l’expliquer.
  • Éviter les namespaces PID/réseau de l’hôte sauf si vous construisez des agents nœud et comprenez les implications.

Checklist : que capturer dans une demande d’exception

  • Image du conteneur exacte et version.
  • Logs de refus exacts (AppArmor) ou numéros/événements d’appels système (seccomp).
  • Justification métier pour le comportement.
  • Proposition de la plus petite exception possible (un chemin, un appel système, une capability).
  • Plan d’isolation (nœuds dédiés, moins de secrets, réseau restreint).
  • Plan de test et plan de rollback.

FAQ

1) Ai-je besoin des deux AppArmor et seccomp ? Un seul ne suffit-il pas ?

Utilisez les deux. Ils couvrent des couches différentes : AppArmor porte sur le contrôle d’accès (fichiers, capabilities, opérations), seccomp sur la surface des appels système. Le chevauchement est acceptable ; la redondance est l’objectif.

2) Si mon conteneur s’exécute en non-root, ai-je encore besoin de ces contrôles ?

Oui. Non-root aide beaucoup, mais l’exploitation du noyau et les échappements de conteneur ne nécessitent pas que votre processus démarre en root. La défense en profondeur compte parce que les attaquants chaînent les faiblesses.

3) Pourquoi ne pas tout exécuter en mode privilégié et compter sur les contrôles réseau ?

Parce que les contrôles réseau n’empêchent pas l’escalade locale de privilèges, les bugs du noyau ou les dommages accidentels à l’hôte. Les conteneurs privilégiés sont essentiellement des « processus avec un département marketing ».

4) Quelle est la posture seccomp par défaut la plus sûre dans Docker ?

Utiliser le profil seccomp par défaut de Docker et n’en dévier que pour des charges spécifiques avec des besoins documentés. Le défaut est un équilibre pragmatique pour de nombreuses charges.

5) Comment savoir si un conteneur est réellement confiné (et pas seulement configuré) ?

Vérifier l’attribut du processus : /proc/<pid>/attr/current pour AppArmor. Pour seccomp, rechercher des événements d’audit SECCOMP lors d’une action connue bloquée, ou inspecter la configuration runtime et tester avec un canary.

6) Mon appli a besoin de ptrace/strace en production. Que faire ?

Première question : en a-t-elle vraiment besoin ? Généralement, vous avez besoin d’une meilleure observabilité, pas de ptrace. Si c’est indispensable, isolez la charge, limitez fortement son périmètre et acceptez le risque accru. N’élargissez pas la sécurité du parc sans précautions.

7) L’activation de ces contrôles va-t-elle nuire aux performances ?

Dans la plupart des charges réelles, la surcharge est négligeable comparée au réseau, au disque et à la logique applicative. Le coût opérationnel vient des exceptions et du débogage, pas des cycles CPU.

8) Pourquoi des refus n’apparaissent-ils qu’après une mise à jour de bibliothèque ?

Parce que les runtimes évoluent. Une libc, une JVM, Go ou Node plus récent peut utiliser des appels système différents ou accéder à d’autres interfaces noyau. C’est pourquoi les profils seccomp « minimaux » demandent maintenance et tests across noyaux.

9) AppArmor suffit-il si je suis sur Ubuntu, et SELinux si je suis sur RHEL ?

Utilisez ce que votre plateforme supporte bien. AppArmor est courant sur Ubuntu/Debian ; SELinux sur les systèmes dérivés de RHEL. L’essentiel est l’enforcement et la maturité opérationnelle, pas l’idéologie.

10) Quel est le gain rapide si je ne peux corriger qu’une chose cette semaine ?

Cessez d’exécuter des conteneurs unconfined et d’utiliser le mode privilégié. Appliquez le seccomp par défaut partout. Ces changements ferment beaucoup de portes d’évasion « faciles ».

Prochaines étapes qui réduisent réellement le risque

Si vous voulez un renforcement minimal qui compte, ne commencez pas par écrire une politique de 500 lignes. Commencez par vous assurer que les valeurs par défaut sont réelles et appliquées :

  • Vérifier qu’AppArmor est chargé et en enforce sur chaque nœud.
  • Vérifier que Docker utilise seccomp par défaut et que les conteneurs ne sont pas unconfined.
  • Retirer les conteneurs privilégiés et les montages docker.sock du plan de données de production.
  • Ajouter un test canary simple au bootstrap des nœuds qui prouve que des refus se produisent.
  • Opérationnaliser le diagnostic : former l’astreinte à lire les dénis et événements seccomp avant d’opter pour l’unconfined.

Faites ces cinq choses et vous convertirez la « sécurité des conteneurs » d’un diaporama en une boucle de contrôle. C’est ce genre d’ennuyeux qui garde les systèmes — et les week-ends — intacts.

← Précédent
ZFS Special Small Blocks : comment turbocharger les charges de petits fichiers
Suivant →
Planification de capacité ZFS : concevoir la croissance sans reconstruction

Laisser un commentaire