Aller au contenu
English
Compte : non connecté, synchronisation possible Mon apprentissage
Selectionner le thème
English

Confiner par profils : seccomp et AppArmor

Par mis à jour le

Améliorer cette page

Un conteneur lancé sans option reçoit deux filtres que personne ne lui a demandés. Le premier, seccomp, trie les appels système : ce sont les demandes qu'un programme adresse au noyau pour ouvrir un fichier, créer un processus ou monter un disque. Le second, AppArmor, trie surtout les accès aux fichiers, au réseau et aux montages. Chacun suit un profil, un fichier de règles qui dit ce qui est permis. Quand une commande échoue avec « Operation not permitted », vous ne savez pas lequel des deux profils a refusé, ni si une capability manquante est plutôt en cause. Cette leçon vous apprend à lire les profils actifs, à attribuer un refus à sa couche, puis à écrire vos propres profils sans retirer les autres protections. Les mesures viennent d'une VM Debian 13 avec Docker Engine 29.9.2 et Compose 5.6.1. Un profil AppArmor se charge dans le noyau de l'hôte : rejouez les gestes sur une machine jetable, jamais sur un hôte partagé.

Chaque geste se vérifie dans le noyau, pas seulement dans une option de configuration.

  • Vous lisez les profils actifs d'un conteneur dans docker info, /proc/1/status et /proc/1/attr/current.
  • Vous attribuez un refus à sa couche (capability, seccomp ou AppArmor) en retirant une couche à la fois.
  • Vous reconnaissez les réglages qui désactivent ces protections, pour les refuser.
  • Vous écrivez un profil seccomp dérivé du profil par défaut de votre version du moteur, et vous l'attachez avec docker run ou Compose.
  • Vous chargez un profil AppArmor dédié, vous l'attachez en mode enforce (le noyau applique les refus) et vous faites apparaître son refus dans le journal du noyau.
  • Vous donnez à ce profil ce qu'un service exige encore (recevoir le signal d'arrêt, survivre au redémarrage de l'hôte).

Vous devez savoir lancer un conteneur et connaître le rôle des capabilities Linux. La leçon Sécuriser Docker en production les met en oeuvre sur le moteur, et la page Comprendre les Linux Capabilities explique le mécanisme du noyau. Vous avez besoin d'un accès sudo sur un hôte Linux qui exécute AppArmor, ici Debian 13, avec curl et python3 pour télécharger et modifier le profil seccomp.

Toutes les mesures de la leçon utilisent une image Alpine épinglée et coupent le réseau (--network none). Tous les objets créés portent le préfixe c38b-m- et vivent dans un dossier de travail dédié :

Fenêtre de terminal
ALP="alpine:3.24.3@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a"
W=/tmp/c38b-m-work
mkdir -p "$W" && cd "$W"

Jouez les blocs dans l'ordre et dans un même terminal : ils réutilisent ALP, W et le répertoire courant. Les deux labs travaillent sur d'autres appels, d'autres chemins et d'autres valeurs : refaites-y le raisonnement plutôt que de recopier les blocs.

Que filtre chaque mécanisme, et en quoi diffère-t-il des capabilities ?

Section intitulée « Que filtre chaque mécanisme, et en quoi diffère-t-il des capabilities ? »

Les trois mécanismes se prononcent sur un même appel du processus, chacun avec sa propre question. Les capabilities décident si le processus a le droit de faire une opération privilégiée, par exemple changer le propriétaire d'un fichier. seccomp décide si l'appel système lui-même est autorisé, quel que soit le processus qui l'émet. AppArmor décide si le profil attaché au processus permet l'accès visé : un chemin, une opération réseau, un montage, mais aussi une capability ou un signal.

Arbre de décision à trois contrôles successifs pour un appel système : seccomp d'abord, puis AppArmor, puis les capabilities, dans l'ordre mesuré pour mount. Un oui passe au contrôle suivant, et l'appel s'exécute après le dernier oui. Un non à une étape donne un refus : EPERM par défaut pour seccomp, EACCES par défaut pour AppArmor, EPERM pour les capabilities.Appel systèmedu processusseccomp :appel autorisé ?AppArmor :accès autorisé ?ouiRefus seccompEPERM par défautnonCapabilities :opération permise ?ouiRefus AppArmorEACCES par défautnonAppel exécutéouiRefus capabilityEPERMnon
Source · SVG
Échap pour fermer, + et − pour zoomer, défilement pour se déplacer. 100 %

Suivez le schéma de haut en bas : un refus à n'importe quelle étape arrête l'appel. Retenez surtout qu'un accès doit être permis par les trois couches à la fois.

Le message d'erreur donne une indication, pas une preuve. Par défaut, seccomp et les capabilities répondent EPERM (« Operation not permitted »), et AppArmor répond EACCES (« Permission denied ») quand il refuse un fichier ou un montage. Mais un profil seccomp peut renvoyer EACCES, et un profil AppArmor qui refuse une capability renvoie EPERM, comme le montrent les mesures plus bas. Seul le retrait d'une couche à la fois tranche.

L'ordre des contrôles est fixe pour seccomp : il se prononce avant que le noyau exécute l'appel système, donc avant les contrôles faits pendant cette exécution. Entre AppArmor et les capabilities, l'ordre dépend de l'appel. Le schéma montre celui de mount, que le tableau de la section « Quelle couche a refusé mon appel ? » met en évidence. Le tableau ci-dessous résume ce que chaque couche examine et où elle se règle.

CoucheCe qu'elle examineOption Docker qui la règle
CapabilitiesL'opération privilégiée demandée--cap-add, --cap-drop
seccompLe nom de l'appel système et ses arguments numériques--security-opt seccomp=
AppArmorLes chemins, le réseau, les montages, les capabilities et les signaux d'après le profil--security-opt apparmor=

La différence avec les capabilities tient à la granularité. Une capability est un droit attribué au processus, alors qu'un profil seccomp liste des appels système et un profil AppArmor des règles d'accès. Les deux profils peuvent donc refuser une action que le processus a pourtant le droit d'accomplir.

Quels profils le moteur applique-t-il sans que vous demandiez rien ?

Section intitulée « Quels profils le moteur applique-t-il sans que vous demandiez rien ? »

Docker attache à chaque conteneur non privilégié un profil seccomp intégré et, sur un hôte qui exécute AppArmor, un profil nommé docker-default, sans que vous le demandiez (voir le profil seccomp par défaut et le profil AppArmor par défaut). La commande docker info annonce les mécanismes actifs, et le noyau dit ce qu'un conteneur précis applique réellement.

Fenêtre de terminal
docker info --format '{{.SecurityOptions}}'
[name=apparmor,profile=default name=seccomp,profile=builtin name=cgroupns]

Cette sortie dit que Docker sait appliquer AppArmor (profil default) et seccomp (profil builtin, intégré à Docker). Elle ne dit rien d'un conteneur précis. Pour en examiner un, lisez deux fichiers de son processus principal, le PID 1 :

Fenêtre de terminal
docker run --rm --network none "$ALP" sh -c 'grep -E "^(Seccomp|Seccomp_filters|NoNewPrivs):" /proc/1/status; cat /proc/1/attr/current'
NoNewPrivs: 0
Seccomp: 2
Seccomp_filters: 1
docker-default (enforce)

Regardez la ligne Seccomp: 2. Elle vaut 0 quand aucun filtre n'est chargé et 2 quand un filtre est actif, et Seccomp_filters: 1 compte les filtres chargés. La ligne NoNewPrivs ne sert pas dans cette leçon. La dernière ligne, lue dans /proc/1/attr/current, donne le profil AppArmor et son mode. Le mode enforce veut dire que le noyau applique les refus ; l'autre mode, complain, se contente de journaliser les accès que le profil refuserait. Ces deux fichiers viennent du noyau : ils font foi, contrairement à une option affichée par Docker.

Pour finir, docker inspect n'est pas la bonne source pour seccomp :

Fenêtre de terminal
docker run -d --network none --name c38b-m-ins "$ALP" sleep 120
docker inspect c38b-m-ins --format 'SecurityOpt={{json .HostConfig.SecurityOpt}} AppArmorProfile={{json .AppArmorProfile}} Privileged={{.HostConfig.Privileged}}'
docker rm -f c38b-m-ins
SecurityOpt=null AppArmorProfile="docker-default" Privileged=false

SecurityOpt vaut null : ce champ ne liste que les options que vous avez demandées. AppArmorProfile nomme bien le profil appliqué, mais aucun champ ne nomme le profil seccomp. Une liste d'options vide ne prouve donc pas l'absence de filtre : la preuve reste /proc/1/status.

Une capability absente et un filtre seccomp renvoient la même erreur, EPERM (« Operation not permitted »). Le message ne désigne donc pas la couche coupable. La méthode consiste à retirer une couche à la fois sur un conteneur jetable : si l'appel passe quand une couche est retirée, c'est elle qui refusait. Ce retrait sert à mesurer, jamais à corriger : la correction ne touche que la couche en cause.

Le premier cas est unshare -U, qui demande au noyau un espace de noms utilisateur séparé. Sous le profil par défaut, il échoue. L'option seccomp=unconfined désactive le profil seccomp pour un conteneur : si l'appel repasse avec elle, seccomp était seul en cause :

Fenêtre de terminal
docker run --rm --network none "$ALP" sh -c 'unshare -U true; echo unshare_rc=$?'
docker run --rm --network none --security-opt seccomp=unconfined "$ALP" sh -c 'unshare -U true; echo unshare_rc=$?'
unshare_rc=1
unshare: unshare(0x10000000): Operation not permitted
unshare_rc=0

Le premier conteneur échoue avec EPERM et le second réussit. Aucune capability n'a changé entre les deux : le filtre seccomp est responsable. Notez aussi que --cap-add SYS_ADMIN fait passer unshare, car le profil par défaut autorise cet appel quand la capability est présente. Ici, seccomp et la capability sont liés.

Le deuxième cas va dans l'autre sens. chown passe par défaut, et échoue quand on retire la capability CHOWN. L'erreur persiste même quand seccomp et AppArmor sont désactivés, ce qui innocente ces deux couches.

Fenêtre de terminal
docker run --rm --network none --cap-drop CHOWN "$ALP" sh -c 'touch /tmp/f; chown 1000 /tmp/f; echo chown_rc=$?'
docker run --rm --network none --cap-drop CHOWN --security-opt seccomp=unconfined --security-opt apparmor=unconfined "$ALP" sh -c 'touch /tmp/f; chown 1000 /tmp/f; echo chown_rc=$?'
chown: /tmp/f: Operation not permitted
chown_rc=1
chown: /tmp/f: Operation not permitted
chown_rc=1

Le troisième cas, mount -t tmpfs none /mnt, traverse les trois couches. Le tableau donne ce que chaque combinaison a produit : le message change selon la couche qui refuse, et une seule combinaison réussit.

Options ajoutées au conteneurRésultat de mountCouche qui refuse
aucunepermission denied (are you root?), code 1seccomp, qui n'autorise mount qu'avec SYS_ADMIN
seccomp=unconfinedmounting none on /mnt failed: Permission denied, code 255AppArmor (docker-default)
--cap-add SYS_ADMINmounting none on /mnt failed: Permission denied, code 255AppArmor (docker-default)
--cap-add SYS_ADMIN et apparmor=unconfinedréussi, code 0aucune
seccomp=unconfined et apparmor=unconfinedpermission denied (are you root?), code 1la capability SYS_ADMIN absente

Lisez le tableau de haut en bas. Donner la capability ne suffit pas (ligne 3), parce que docker-default interdit mount. À l'inverse, retirer les deux profils ne suffit pas sans la capability (ligne 5). Les lignes 1 et 5 affichent le même message, parce que seccomp et une capability absente renvoient toutes deux EPERM : le message seul ne départage rien.

Ce qui départage, c'est le résultat quand on retire une couche. Dès que seccomp est retiré (ligne 2), le message et le code changent : le profil seccomp par défaut refusait donc en premier, puisqu'il n'autorise mount que pour un processus qui détient SYS_ADMIN. Puis AppArmor refuse alors que la capability manque encore, ce qui fixe l'ordre du schéma pour cet appel.

Pour attribuer un refus, répétez donc la démarche : relevez l'erreur, retirez une couche, relancez, et recommencez jusqu'à ce que l'appel passe. La correction vise uniquement la couche trouvée, et les autres protections restent actives.

Trois réglages désactivent ces protections : seccomp=unconfined, apparmor=unconfined et --privileged. Reconnaissez-les dans un docker run, un fichier Compose ou un script pour les refuser : aucun ne se justifie pour corriger un refus.

Fenêtre de terminal
docker run --rm --network none --privileged "$ALP" sh -c 'grep -E "^(Seccomp|Seccomp_filters|NoNewPrivs):" /proc/1/status; cat /proc/1/attr/current'
NoNewPrivs: 0
Seccomp: 0
Seccomp_filters: 0
unconfined

Avec --privileged, le filtre seccomp tombe à Seccomp: 0 et AppArmor passe à unconfined : les deux couches disparaissent d'un coup. Les deux options unconfined agissent chacune sur une seule couche, comme le résume le tableau.

OptionChamp Seccomp de /proc/1/status/proc/1/attr/current
aucune (défaut)2docker-default (enforce)
--security-opt seccomp=unconfined0docker-default (enforce)
--security-opt apparmor=unconfined2unconfined
--privileged0unconfined

Quand vous posez les deux unconfined, docker inspect affiche SecurityOpt=["seccomp=unconfined","apparmor=unconfined"] et AppArmorProfile="unconfined", ce qui se repère facilement dans une revue. Un service dont le contrôle /proc/1/status donne Seccomp: 0, ou dont /proc/1/attr/current donne unconfined, tourne sans l'une de ces protections.

Un profil seccomp est un fichier JSON qui repose sur trois éléments :

  • defaultAction dit ce que devient un appel absent de la liste ;
  • syscalls est la liste des règles ;
  • chaque règle porte names (les appels visés), action et, au besoin, args (des conditions sur les arguments de l'appel).

Vous le donnez à Docker avec --security-opt seccomp=<fichier>. Le client lit le fichier, pas le démon : le chemin se résout donc sur la machine où vous tapez la commande.

Partez du profil par défaut de la version exacte de votre moteur, pas d'un profil qui autorise tout. Pour Docker 29.9.2, le fichier se trouve dans le dépôt moby/moby, au tag de la version. Le chemin profiles/seccomp/default.json n'existe pas à ce tag : utilisez le chemin vendor/ du bloc ci-dessous. Sur la VM de mesure, le fichier avait la somme de contrôle de la première ligne de la sortie.

Fenêtre de terminal
curl -fsSL "https://raw.githubusercontent.com/moby/moby/docker-v29.9.2/vendor/github.com/moby/profiles/seccomp/default.json" -o defaut.json
sha256sum defaut.json
python3 -c "import json;d=json.load(open('defaut.json'));print('defaultAction=',d['defaultAction'],' defaultErrnoRet=',d.get('defaultErrnoRet'),' groupes=',len(d['syscalls']))"
785b2429264afba4d594320337cb17f144f3c7d51585f9805eef72e28f4f9334 defaut.json
defaultAction= SCMP_ACT_ERRNO defaultErrnoRet= 1 groupes= 37

Le profil refuse par défaut (SCMP_ACT_ERRNO, avec l'erreur 1, EPERM) et énumère ses autorisations dans 37 règles. Attaché sans changement, le fichier téléchargé se comporte comme le profil intégré : chmod passe et unshare reste refusé. Pour bloquer chmod, deux gestes se présentent : ajouter un refus, ou retirer l'autorisation. Les deux essais donnent des résultats différents.

Que se passe-t-il si vous ajoutez un refus en fin de liste ?

Section intitulée « Que se passe-t-il si vous ajoutez un refus en fin de liste ? »

Le premier réflexe est d'ajouter, à la fin de la liste, une règle qui refuse chmod et ses variantes. Le script derive-ajout.py fait cet essai avec l'image Alpine :

Fenêtre de terminal
cat > derive-ajout.py <<'EOF'
import json
d = json.load(open('defaut.json'))
d['syscalls'].append({"names": ["chmod", "fchmod", "fchmodat", "fchmodat2"], "action": "SCMP_ACT_ERRNO", "errnoRet": 1})
json.dump(d, open('derive.json', 'w'), indent=1)
print('règles :', len(d['syscalls']))
EOF
python3 derive-ajout.py
docker run --rm --network none --security-opt seccomp="$W/derive.json" "$ALP" sh -c 'touch /tmp/f; chmod 600 /tmp/f; echo "chmod rc=$?"; unshare -U true && echo unshare=ok || echo unshare=refuse'
règles : 38
chmod rc=0
unshare: unshare(0x10000000): Operation not permitted
unshare=refuse

Regardez chmod rc=0 : le profil compte 38 règles et il ne refuse rien de nouveau. Il a pourtant bien été chargé, puisqu'unshare reste refusé comme avant. Un refus ajouté en fin de liste n'a rien bloqué ici. Le profil par défaut autorise déjà la famille chmod dans sa première règle, une règle SCMP_ACT_ALLOW, et c'est elle qui l'a emporté. Cet essai porte sur un refus sans condition visant un appel déjà autorisé : il ne vous dit pas comment deux règles contradictoires se départagent en général. Ne comptez donc pas sur un refus ajouté, et durcissez par retrait.

Puisque defaultAction refuse, il suffit de retirer les noms de l'appel de la règle qui l'autorise : l'appel tombe alors sous le refus par défaut. Vous durcissez le profil en lui retirant des autorisations, pas en lui ajoutant des refus. Le script derive-retrait.py cherche d'abord les règles qui citent la famille chmod (la première ligne de la sortie n'en trouve qu'une, la règle 0), puis retire ces noms de chaque règle SCMP_ACT_ALLOW.

Fenêtre de terminal
cat > derive-retrait.py <<'EOF'
import json
VISES = {"chmod", "fchmod", "fchmodat", "fchmodat2"}
d = json.load(open('defaut.json'))
ou = [(i, sorted(VISES & set(r.get('names', [])))) for i, r in enumerate(d['syscalls']) if VISES & set(r.get('names', []))]
print('règles qui autorisent la famille chmod :', [(i, d['syscalls'][i]['action'], n) for i, n in ou])
for r in d['syscalls']:
if r.get('action') == 'SCMP_ACT_ALLOW' and 'names' in r:
r['names'] = [n for n in r['names'] if n not in VISES]
json.dump(d, open('derive.json', 'w'), indent=1)
EOF
python3 derive-retrait.py
docker run --rm --network none --security-opt seccomp="$W/derive.json" "$ALP" sh -c 'touch /tmp/f; chmod 600 /tmp/f; echo "chmod rc=$?"; unshare -U true && echo unshare=ok || echo unshare=refuse; ls /tmp >/dev/null && echo ls=ok'
docker run -d --name c38b-m-d2 --network none --security-opt seccomp="$W/derive.json" "$ALP" sleep 30
docker exec c38b-m-d2 grep -E '^Seccomp' /proc/1/status
docker rm -f c38b-m-d2
règles qui autorisent la famille chmod : [(0, 'SCMP_ACT_ALLOW', ['chmod', 'fchmod', 'fchmodat', 'fchmodat2'])]
chmod rc=1
chmod: /tmp/f: Operation not permitted
unshare=refuse
unshare: unshare(0x10000000): Operation not permitted
ls=ok
Seccomp: 2
Seccomp_filters: 1

Trois lignes de la sortie établissent que le profil est correct. chmod rc=1 prouve que l'appel visé est refusé. unshare=refuse et ls=ok montrent que le reste du profil par défaut reste en place et que les appels ordinaires passent. Seccomp: 2 confirme que le noyau applique un filtre. Vous avez donc retiré une autorisation sans toucher au reste.

Les variantes qui suivent isolent chaque champ d'une règle. Elles ont été mesurées sur un profil minimal dont defaultAction vaut SCMP_ACT_ALLOW, à la place du profil par défaut. Un tel profil perd les protections du défaut : unshare -U true y passe, alors que le profil par défaut le refuse. Ne le gardez que pour comprendre les champs, jamais pour un service.

Fenêtre de terminal
cat > c38b-m-chmod-eperm.json <<'EOF'
{
"defaultAction": "SCMP_ACT_ALLOW",
"syscalls": [
{
"names": ["chmod", "fchmod", "fchmodat", "fchmodat2"],
"action": "SCMP_ACT_ERRNO"
}
]
}
EOF
docker run --rm --network none --security-opt seccomp="$W/c38b-m-chmod-eperm.json" "$ALP" sh -c 'touch /tmp/f; chmod 600 /tmp/f; echo chmod600_rc=$?; mkdir /tmp/d; echo mkdir_rc=$?; ls -l /tmp/f; unshare -U true; echo unshare_rc=$?; grep -E "^Seccomp" /proc/1/status'
chmod: /tmp/f: Operation not permitted
chmod600_rc=1
mkdir_rc=0
-rw-r--r-- 1 root root 0 Nov 4 18:39 /tmp/f
unshare_rc=0
Seccomp: 2
Seccomp_filters: 1

Sans errnoRet, l'erreur renvoyée est EPERM. Le fichier garde ses droits d'origine (-rw-r--r--), mkdir passe, et unshare_rc=0 confirme la perte de protection annoncée. Trois autres champs se règlent en modifiant la règle :

  • "errnoRet": 13 renvoie EACCES, et chmod affiche chmod: /tmp/f: Permission denied.
  • "action": "SCMP_ACT_KILL_PROCESS" tue le processus fautif au lieu de lui renvoyer une erreur. Le shell parent a survécu, parce que chmod est un processus séparé, et la sortie a affiché Bad system call (core dumped).
  • Une clé args conditionne la règle à une valeur d'argument, comme le montre l'exemple ci-dessous.

La règle args refuse chmod seulement pour le mode 0777, écrit 511 en décimal. Pour chmod et fchmod, le mode est le deuxième argument (index 1). Pour fchmodat et fchmodat2, c'est le troisième (index 2).

Fenêtre de terminal
cat > c38b-m-chmod-args.json <<'EOF'
{
"defaultAction": "SCMP_ACT_ALLOW",
"syscalls": [
{
"names": ["chmod", "fchmod"],
"action": "SCMP_ACT_ERRNO",
"args": [{"index": 1, "value": 511, "op": "SCMP_CMP_EQ"}]
},
{
"names": ["fchmodat", "fchmodat2"],
"action": "SCMP_ACT_ERRNO",
"args": [{"index": 2, "value": 511, "op": "SCMP_CMP_EQ"}]
}
]
}
EOF
docker run --rm --network none --security-opt seccomp="$W/c38b-m-chmod-args.json" "$ALP" sh -c 'touch /tmp/f; chmod 600 /tmp/f; echo chmod600_rc=$?; chmod 777 /tmp/f; echo chmod777_rc=$?; ls -l /tmp/f'
chmod600_rc=0
chmod: /tmp/f: Operation not permitted
chmod777_rc=1
-rw------- 1 root root 0 Nov 4 18:39 /tmp/f

chmod 600 passe et chmod 777 est refusé : la règle ne vise que la valeur choisie.

Un profil mal écrit ne se signale pas toujours. Sur Docker 29.9.2, deux erreurs arrêtent docker run avant le démarrage du conteneur, avec le code de retour 125 : un fichier JSON tronqué, puis un fichier absent. Voici les deux messages, dans cet ordre :

docker: compacting json for seccomp profile (/tmp/c38b-m-work/c38b-m-broken.json) failed: unexpected end of JSON input
docker: opening seccomp profile (/tmp/c38b-m-work/c38b-m-absent.json) failed: open /tmp/c38b-m-work/c38b-m-absent.json: no such file or directory

En revanche, un nom d'appel système inexistant est accepté en silence : le conteneur a démarré normalement avec un profil visant c38b_m_inexistant. Une faute de frappe dans names ne produit donc aucune erreur, et la règle ne s'applique à rien. Vérifiez toujours l'effet par un essai qui doit échouer.

Compose reprend l'option sous security_opt. Un chemin relatif se lit depuis le dossier du fichier Compose, pas depuis le dossier où vous lancez la commande. Les formes seccomp= et seccomp: ont fonctionné toutes les deux avec Compose 5.6.1.

Pour le mesurer, docker compose -f "$W/compose/compose.yaml" up -d a été lancé depuis un autre dossier, qui ne contenait aucun profil : le profil a été appliqué (Seccomp: 2, chmod refusé). Avec un faux profil permissif du même nom dans ce dossier de lancement, chmod restait refusé, donc c'est bien le fichier voisin du compose.yaml qui est lu. Le bloc ci-dessous lance Compose depuis le dossier du fichier, ce qui revient au même.

Fenêtre de terminal
mkdir -p "$W/compose" && cd "$W/compose"
cp "$W/c38b-m-chmod-eperm.json" .
cat > compose.yaml <<EOF
name: c38b-m-proj
services:
app:
image: $ALP
command: ["sleep", "120"]
network_mode: none
security_opt:
- seccomp=./c38b-m-chmod-eperm.json
EOF
docker compose up -d
docker compose exec -T app sh -c 'touch /tmp/f; chmod 600 /tmp/f; echo rc=$?; grep -E "^Seccomp" /proc/1/status'
docker compose down
cd "$W"
Container c38b-m-proj-app-1 Creating
Container c38b-m-proj-app-1 Created
Container c38b-m-proj-app-1 Starting
Container c38b-m-proj-app-1 Started
chmod: /tmp/f: Operation not permitted
rc=1
Seccomp: 2
Seccomp_filters: 1

La ligne Seccomp: 2 et le refus de chmod prouvent que le profil est appliqué au service. Avec seccomp=unconfined à la place, le même conteneur affichait Seccomp: 0 : un contrôle automatique de ce champ détecte donc ce réglage.

Un profil AppArmor est un fichier de règles qu'apparmor_parser charge dans le noyau de l'hôte. Il porte un nom, et c'est ce nom que vous donnez à --security-opt apparmor=. Un accès que le profil n'autorise nulle part est refusé sans règle particulière : c'est un refus implicite. La règle deny crée un refus explicite, qui s'impose même quand une règle plus large autorise l'accès. Le profil ci-dessous autorise largement le réseau (network), les capabilities que le conteneur détient déjà (capability), les fichiers (file) et le démontage (umount), puis refuse seulement l'écriture sous /srv/vault/. Il ne reprend pas les autres règles de docker-default : la section « Que perd un profil dédié par rapport à docker-default ? » montre ce que ce manque coûte à un service. Les deux flags ne servent pas à l'exercice : recopiez-les tels quels.

Fenêtre de terminal
mkdir -p "$W/vault"
echo "contenu-vault" > "$W/vault/secret.txt"
chmod 666 "$W/vault/secret.txt"
ls -l "$W/vault/secret.txt"
cat > c38b-m-nowrite <<'EOF'
#include <tunables/global>
profile c38b-m-nowrite flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
network,
capability,
file,
umount,
deny /srv/vault/** w,
}
EOF
sudo aa-status | grep -E 'c38b|docker-default|profiles are loaded'
sudo apparmor_parser -a c38b-m-nowrite; echo "parser_rc=$?"
sed -e 's/profile c38b-m-nowrite flags=(/profile c38b-m-nowrite-cplx flags=(complain,/' c38b-m-nowrite > c38b-m-nowrite-cplx
sudo apparmor_parser -a c38b-m-nowrite-cplx
sudo aa-status | grep -E 'c38b|docker-default|profiles are loaded'
-rw-rw-rw- 1 ansible ansible 14 Nov 4 18:40 /tmp/c38b-m-work/vault/secret.txt
107 profiles are loaded.
docker-default
parser_rc=0
109 profiles are loaded.
c38b-m-nowrite
docker-default
c38b-m-nowrite-cplx

Le fichier de test a le mode Unix 666 : les permissions autorisent donc l'écriture, et seule la politique AppArmor peut la refuser. Le chargement avec -a ajoute le profil, sans toucher à docker-default ni aux autres profils. La commande sed fabrique aussi une copie du profil en mode complain, pour comparer ensuite les deux modes. Dans la sortie, aa-status passe de 107 à 109 profils : le profil de la leçon et cette copie, listés à côté de docker-default.

Si vous rejouez la leçon alors que le profil est encore chargé, -a refuse de l'ajouter une seconde fois. L'option -r remplace le profil chargé par le contenu du fichier :

Fenêtre de terminal
sudo apparmor_parser -a c38b-m-nowrite; echo "parser_a_rc=$?"
sudo apparmor_parser -r c38b-m-nowrite; echo "parser_r_rc=$?"
apparmor_parser: Unable to add "c38b-m-nowrite". Profile already exists
parser_a_rc=239
parser_r_rc=0

Une erreur de syntaxe se signale au chargement, avec le numéro de ligne et un code de retour 1 :

Fenêtre de terminal
printf 'profile c38b-m-bad {\n file,\n deny /x/** wz,\n}\n' > c38b-m-bad
sudo apparmor_parser -a c38b-m-bad; echo "bad_rc=$?"
AppArmor parser error for c38b-m-bad in profile c38b-m-bad at line 3: syntax error, unexpected TOK_ID, expecting TOK_MODE
bad_rc=1

Attachez ensuite le profil à un conteneur, avec le dossier de test monté sur /srv/vault et un tmpfs sur /srv/open pour comparer.

Fenêtre de terminal
S='cat /proc/1/attr/current; echo --- ; cat /srv/vault/secret.txt; echo "cat_rc=$?"; echo nouveau > /srv/vault/secret.txt; echo "write_rc=$?"; touch /srv/vault/new; echo "touch_rc=$?"; mkdir /srv/vault/d; echo "mkdir_rc=$?"; echo ok > /srv/open/f; echo "open_rc=$?"; ls -l /srv/vault/secret.txt; id'
docker run --rm --network none --security-opt apparmor=c38b-m-nowrite -v "$W/vault:/srv/vault" --tmpfs /srv/open "$ALP" sh -c "$S"
c38b-m-nowrite (enforce)
---
contenu-vault
cat_rc=0
sh: can't create /srv/vault/secret.txt: Permission denied
write_rc=1
touch: /srv/vault/new: Permission denied
touch_rc=1
mkdir: can't create directory '/srv/vault/d': Permission denied
mkdir_rc=1
open_rc=0
-rw-rw-rw- 1 1001 1001 14 Nov 4 18:40 /srv/vault/secret.txt
uid=0(root) gid=0(root) groups=0(root),0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)

Lisez la sortie dans cet ordre. La première ligne confirme le mode enforce. La lecture réussit ; l'écriture, touch et mkdir dans le dossier protégé sont refusés ; l'écriture dans /srv/open passe. Le profil n'arrête donc que l'accès visé. Le fichier garde le mode 666 et l'utilisateur du conteneur est root : seule la politique refuse, pas les permissions Unix. Avec docker-default ou avec apparmor=unconfined, les mêmes écritures ont toutes réussi.

AppArmor filtre aussi les capabilities. La règle capability du profil laisse au conteneur celles qu'il détient ; une règle deny capability chown lui en retire une, même si Docker la lui a donnée. Le refus ressemble alors à celui d'une capability absente :

Fenêtre de terminal
sed -e 's/c38b-m-nowrite/c38b-m-nochown/' -e 's# deny /srv/vault/\*\* w,# deny capability chown,#' c38b-m-nowrite > c38b-m-nochown
sudo apparmor_parser -a c38b-m-nochown
docker run --rm --network none --security-opt apparmor=c38b-m-nochown "$ALP" sh -c 'cat /proc/1/attr/current; touch /tmp/f; chown 1000 /tmp/f 2>&1; echo chown_rc=$?'
docker run --rm --network none --security-opt apparmor=c38b-m-nochown --security-opt seccomp=unconfined "$ALP" sh -c 'touch /tmp/f; chown 1000 /tmp/f 2>&1; echo chown_sans_seccomp_rc=$?'
c38b-m-nochown (enforce)
chown: /tmp/f: Operation not permitted
chown_rc=1
chown: /tmp/f: Operation not permitted
chown_sans_seccomp_rc=1

Le message est Operation not permitted, donc EPERM, exactement celui de --cap-drop CHOWN : un refus AppArmor ne rend pas toujours EACCES. Le refus persiste sans seccomp, alors que Docker donne la capability CHOWN par défaut et que le même chown passe sous docker-default : c'est donc bien le profil AppArmor qui refuse.

Le mode complain journalise les accès que le profil refuserait implicitement, sans les bloquer, mais il applique quand même une règle deny. Relancez le docker run qui monte vault, avec la copie chargée plus haut sous flags=(complain,...), c'est-à-dire avec --security-opt apparmor=c38b-m-nowrite-cplx. Le conteneur affiche c38b-m-nowrite-cplx (complain), et l'écriture dans le fichier reste refusée :

c38b-m-nowrite-cplx (complain)
[...]
sh: can't create /srv/vault/secret.txt: Permission denied
write_rc=1

Une règle deny s'applique donc même en complain, ce que confirme le manuel aa-complain(8). Ne comptez pas sur ce mode pour tester sans risque une règle deny, et ne déduisez pas le mode d'un refus observé : un refus deny apparaît dans les deux modes. Seul /proc/1/attr/current dit si le profil est en enforce.

Si vous nommez un profil qui n'est pas chargé, le conteneur ne démarre pas, et docker run rend le code 127 avec une erreur du runtime runc :

docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: unable to apply apparmor profile: apparmor failed to apply profile: write fsmount:fscontext:proc/thread-self/attr/apparmor/exec: no such file or directory

Le message ne dit pas « profil absent » en toutes lettres, mais unable to apply apparmor profile suivi de no such file or directory le laisse entendre. Chargez le profil avant de lancer le conteneur. C'est aussi l'erreur que rencontre un service après un redémarrage de l'hôte, quand son profil n'a été chargé qu'avec -a : la section « Comment un profil AppArmor survit-il au redémarrage ? » montre comment l'installer.

Pourquoi un refus AppArmor n'apparaît-il pas dans le journal du noyau ?

Section intitulée « Pourquoi un refus AppArmor n'apparaît-il pas dans le journal du noyau ? »

Le refus de l'écriture est bien appliqué, et pourtant la commande journalctl -k n'affiche aucune ligne apparmor="DENIED" pour /srv/vault. Sur la VM de mesure, le paramètre /sys/module/apparmor/parameters/audit valait normal (lisible avec sudo cat) et auditd était inactif. Une règle deny simple refuse sans journaliser : le manuel apparmor.d(5) indique qu'il faut la combiner avec audit pour obtenir une trace.

La règle audit deny fait les deux. Chargez une copie du profil où seule cette règle change, puis relancez l'écriture :

Fenêtre de terminal
sed -e 's/c38b-m-nowrite/c38b-m-nowrite-aud/' -e 's#deny /srv/vault#audit deny /srv/vault#' c38b-m-nowrite > c38b-m-nowrite-aud
sudo apparmor_parser -a c38b-m-nowrite-aud
docker run --rm --network none --security-opt apparmor=c38b-m-nowrite-aud -v "$W/vault:/srv/vault" --tmpfs /srv/open "$ALP" sh -c 'cat /proc/1/attr/current; cat /srv/vault/secret.txt; echo x > /srv/vault/secret.txt; echo write_rc=$?'
sudo journalctl -k --no-pager -o short-iso --since "-5min" | grep 'apparmor='
c38b-m-nowrite-aud (enforce)
contenu-vault
sh: can't create /srv/vault/secret.txt: Permission denied
write_rc=1
[...]
2026-11-04T18:40:19+00:00 c45-debian.lab kernel: audit: type=1400 audit(1793817619.758:118): apparmor="STATUS" operation="profile_load" profile="unconfined" name="c38b-m-nowrite-aud" pid=29909 comm="apparmor_parser"
2026-11-04T18:40:19+00:00 c45-debian.lab kernel: audit: type=1400 audit(1793817619.966:119): apparmor="DENIED" operation="open" class="file" profile="c38b-m-nowrite-aud" name="/srv/vault/secret.txt" pid=29951 comm="sh" requested_mask="wc" denied_mask="wc" fsuid=0 ouid=1001

La ligne DENIED suffit pour attribuer le refus à AppArmor : profile= nomme le profil, name= le chemin, operation="open" l'opération, et requested_mask="wc" l'écriture et la création demandées. Un refus implicite, lui, se journalise de lui-même : un profil qui n'autorise pas l'écriture sur ce chemin, sans règle deny ni audit, a produit la même ligne.

Le filtre grep 'apparmor=' ramène aussi du bruit sans rapport avec votre refus : des lignes operation="profile_load" quand vous chargez un profil, et, si vous nommez un profil qui n'est pas chargé, une ligne operation="change_onexec" émise par runc avec info="label not found". Cherchez le profile= et le name= de votre cas. Sur cette VM, le refus de mount par docker-default ne laissait pas non plus de trace sans règle d'audit.

Que perd un profil dédié par rapport à docker-default ?

Section intitulée « Que perd un profil dédié par rapport à docker-default ? »

Un profil passé par --security-opt apparmor= remplace docker-default en entier : il n'hérite d'aucune de ses règles. Or docker-default ne se contente pas d'autoriser. Le gabarit du profil embarqué dans Docker 29.9.2 refuse aussi mount, l'écriture dans la plupart des fichiers de /proc et de /sys, la lecture de /proc/kcore, et les sockets réseau alg et vsock. Il autorise enfin des signaux précis : ceux que runc et le démon envoient au conteneur, et ceux que les processus du conteneur s'échangent. Le profil c38b-m-nowrite n'a aucune de ces règles.

Le manque le plus visible concerne les signaux. Le service ci-dessous attend SIGTERM pour s'arrêter proprement : c'est le signal que docker stop lui fait envoyer par runc. Lancez-le sous le profil de la leçon, puis arrêtez-le :

Fenêtre de terminal
APP='trap "echo arret-propre; exit 0" TERM; sleep 300 & wait'
docker run -d --name c38b-m-svc --network none --security-opt apparmor=c38b-m-nowrite "$ALP" sh -c "$APP" >/dev/null
sleep 1
docker exec c38b-m-svc cat /proc/1/attr/current
t0=$(date +%s); docker stop c38b-m-svc >/dev/null; t1=$(date +%s)
echo "duree_stop=$((t1-t0))s code=$(docker inspect -f '{{.State.ExitCode}}' c38b-m-svc) journal=[$(docker logs c38b-m-svc 2>&1)]"
docker rm c38b-m-svc >/dev/null
sudo journalctl -k --no-pager -o short-iso --since "-1min" | grep 'apparmor="DENIED"' | grep 'c38b-m-nowrite'
c38b-m-nowrite (enforce)
duree_stop=12s code=137 journal=[]
2026-11-04T19:09:44+00:00 c45-debian.lab kernel: audit: type=1400 audit(1793819384.532:124): apparmor="DENIED" operation="signal" class="signal" profile="c38b-m-nowrite" pid=2327 comm="runc" requested_mask="receive" denied_mask="receive" signal=term peer="runc"
2026-11-04T19:09:46+00:00 c45-debian.lab kernel: audit: type=1400 audit(1793819386.560:125): apparmor="DENIED" operation="signal" class="signal" profile="c38b-m-nowrite" pid=2334 comm="runc" requested_mask="receive" denied_mask="receive" signal=kill peer="runc"

L'arrêt a pris 12 secondes et s'est terminé en code 137, sans la ligne arret-propre. Le journal dit pourquoi : AppArmor a refusé au conteneur de recevoir (requested_mask="receive") le SIGTERM puis le SIGKILL envoyés par runc (peer="runc"). Le conteneur a tout de même été tué, comme l'indique le code 137, par une voie que ces deux lignes ne montrent pas : le profil n'empêche pas l'arrêt, il retire au service sa seule chance de s'arrêter lui-même. Le service n'a jamais su qu'on lui demandait de s'arrêter : c'est l'arrêt brutal qu'un service avec des données en cours d'écriture doit éviter. La leçon Cycle de vie, PID 1 et arrêt propre explique le délai de docker stop et le code 137.

Le profil d'un service autorise donc au moins la réception des signaux de runc. La copie c38b-m-service ne diffère de c38b-m-nowrite que par cette règle :

Fenêtre de terminal
sed -e 's/c38b-m-nowrite/c38b-m-service/' -e 's# deny /srv/vault/\*\* w,# signal (receive) peer=runc,\n deny /srv/vault/** w,#' c38b-m-nowrite > c38b-m-service
cat c38b-m-service
sudo apparmor_parser -a c38b-m-service; echo "parser_service_rc=$?"
docker run -d --name c38b-m-svc --network none --security-opt apparmor=c38b-m-service "$ALP" sh -c "$APP" >/dev/null
sleep 1
docker exec c38b-m-svc cat /proc/1/attr/current
t0=$(date +%s); docker stop c38b-m-svc >/dev/null; t1=$(date +%s)
echo "duree_stop=$((t1-t0))s code=$(docker inspect -f '{{.State.ExitCode}}' c38b-m-svc) journal=[$(docker logs c38b-m-svc 2>&1)]"
docker rm c38b-m-svc >/dev/null
#include <tunables/global>
profile c38b-m-service flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
network,
capability,
file,
umount,
signal (receive) peer=runc,
deny /srv/vault/** w,
}
parser_service_rc=0
c38b-m-service (enforce)
duree_stop=0s code=0 journal=[arret-propre]

Avec cette règle, le service reçoit SIGTERM, écrit arret-propre et sort en code 0 sans attendre. Cette seule règle ne couvre que les signaux de runc : un service dont les processus s'envoient des signaux entre eux, ou en reçoivent d'un processus de l'hôte, a besoin des autres règles signal du gabarit. Pour un vrai service, ne partez donc pas d'un profil vide : partez du gabarit de votre version du moteur, gardez ses refus et ses règles de signaux, puis ajoutez votre restriction. Vérifiez ensuite les trois gestes d'exploitation : l'accès visé est refusé, docker stop aboutit en code 0, et le journal ne montre aucun refus inattendu.

Comment un profil AppArmor survit-il au redémarrage ?

Section intitulée « Comment un profil AppArmor survit-il au redémarrage ? »

apparmor_parser -a charge le profil dans le noyau et n'écrit rien sur le disque : au redémarrage de l'hôte, le profil disparaît, et le conteneur qui le nomme ne démarre plus. Pour qu'un profil soit rechargé au démarrage, installez son fichier sous /etc/apparmor.d/, que le service apparmor lit à chaque démarrage, puis chargez-le depuis cet emplacement avec -r :

Fenêtre de terminal
sudo install -m 0644 -o root -g root c38b-m-service /etc/apparmor.d/c38b-m-service
sudo apparmor_parser -r /etc/apparmor.d/c38b-m-service; echo "parser_install_rc=$?"
ls -l /etc/apparmor.d/c38b-m-service
sudo aa-status | grep -E 'c38b|profiles are loaded'
parser_install_rc=0
-rw-r--r-- 1 root root 229 Nov 4 19:09 /etc/apparmor.d/c38b-m-service
110 profiles are loaded.
c38b-m-nochown
c38b-m-nowrite
c38b-m-service

Cette sortie vient d'un passage limité aux trois profils qu'elle liste. Si vous avez joué tous les blocs de la leçon, aa-status liste aussi les copies -cplx et -aud (112 profils sur la VM de mesure). La VM de mesure a ensuite été redémarrée. Un redémarrage ouvre un nouveau terminal et vide /tmp : les contrôles ci-dessous redéfinissent donc ALP, et le dossier de travail a disparu. Ce bloc décrit l'état après redémarrage : joué sans redémarrer, il rend nowrite_rc=0, puisque c38b-m-nowrite est encore chargé.

Fenêtre de terminal
ALP="alpine:3.24.3@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a"
sudo aa-status | grep -E 'c38b|profiles are loaded'
docker run --rm --network none --security-opt apparmor=c38b-m-service "$ALP" cat /proc/1/attr/current; echo "service_rc=$?"
docker run --rm --network none --security-opt apparmor=c38b-m-nowrite "$ALP" cat /proc/1/attr/current; echo "nowrite_rc=$?"
108 profiles are loaded.
c38b-m-service
c38b-m-service (enforce)
service_rc=0
docker: Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: unable to apply apparmor profile: apparmor failed to apply profile: write fsmount:fscontext:proc/thread-self/attr/apparmor/exec: no such file or directory
Run 'docker run --help' for more information
nowrite_rc=127

Seul le profil installé est revenu : le conteneur qui le nomme démarre, alors que celui qui nomme c38b-m-nowrite, chargé par -a, échoue en code 127. Vous n'avez pas besoin de redémarrer pour l'anticiper : la présence du fichier sous /etc/apparmor.d/ et un service apparmor activé (systemctl is-enabled apparmor) indiquent que le profil sera rechargé. Si vous redémarrez quand même une machine jetable, seul c38b-m-service reste à nettoyer, par son chemin, comme le font les deux premières lignes du bloc de nettoyage. La page Comprendre et utiliser AppArmor détaille la gestion des profils côté hôte.

Chaque mécanisme laisse des cas hors de sa portée, et il vaut mieux les connaître que les découvrir en production.

  • seccomp ne lit pas le contenu des chemins passés par pointeur : il compare le nom de l'appel et ses arguments numériques, donc il ne peut pas refuser « ouvrir ce fichier ». La documentation du noyau sur seccomp l'explique : les filtres BPF ne déréférencent pas les pointeurs. Le refus d'un chemin relève d'AppArmor.
  • AppArmor dépend de l'hôte. Les distributions qui s'appuient sur SELinux, à la place d'AppArmor, n'ont pas ce mécanisme : leurs conteneurs se confinent par des étiquettes SELinux, qui suivent d'autres règles.
  • Un profil personnalisé est lié à la machine. Un profil seccomp est un fichier que le client lit, un profil AppArmor est un état du noyau de l'hôte : chaque hôte qui exécute le service doit porter le profil installé sous /etc/apparmor.d/.
  • Une règle seccomp mal écrite se tait : un nom d'appel inconnu est accepté sans erreur. Seul un essai qui doit échouer prouve qu'une règle s'applique.

Ne désactivez jamais une protection pour corriger un refus : un unconfined accordé pour « tester vite » risque de rester en place. Écrivez la sonde minimale qui reproduit le refus, trouvez la couche en retirant une couche à la fois, puis modifiez seulement le profil de cette couche. Pour Kubernetes, les mêmes profils s'attachent par le securityContext : seccomp dans Kubernetes et AppArmor dans Kubernetes.

Un profil AppArmor chargé reste dans le noyau tant que vous ne le déchargez pas : supprimer le fichier ne suffit pas. Déchargez d'abord le profil installé par son chemin sous /etc/apparmor.d/, puis retirez ce fichier pour qu'il ne revienne pas au prochain démarrage. Déchargez ensuite les autres profils de la leçon avec -R, qui lit leur fichier dans le dossier de travail, vérifiez qu'il ne reste que les profils d'origine, puis supprimez le dossier.

Fenêtre de terminal
sudo apparmor_parser -R /etc/apparmor.d/c38b-m-service; echo "unload c38b-m-service rc=$?"
sudo rm /etc/apparmor.d/c38b-m-service
for f in c38b-m-nowrite c38b-m-nowrite-cplx c38b-m-nowrite-aud c38b-m-nochown; do sudo apparmor_parser -R "$f"; echo "unload $f rc=$?"; done
sudo aa-status | grep -c c38b
sudo aa-status | grep -E 'profiles are loaded|docker-default'
cd / && find /tmp/c38b-m-work -delete
docker ps -a --filter name=c38b-m- --format '{{.Names}}'
unload c38b-m-service rc=0
unload c38b-m-nowrite rc=0
unload c38b-m-nowrite-cplx rc=0
unload c38b-m-nowrite-aud rc=0
unload c38b-m-nochown rc=0
0
107 profiles are loaded.
docker-default

Le compte 0 après grep -c c38b prouve qu'aucun profil de la leçon ne reste, et le total retombe au nombre d'avant la leçon (107 sur la VM de mesure), docker-default compris. La dernière commande ne doit rien afficher. Ne déchargez jamais docker-default ni un autre profil que ceux de la leçon.

Le premier lab part d'un service dont le filtrage seccomp est désactivé. Vous y dérivez le profil par défaut, fourni en copie épinglée, pour qu'il refuse un appel précis avec une valeur d'argument précise, tout en laissant passer le même appel avec les autres valeurs. Il combine les deux gestes de la leçon : retirer l'appel de la règle qui l'autorise, puis le réautoriser avec une condition sur son argument. Ses quatre tests contrôlent que le profil explicite est déclaré sans aucun unconfined, que le noyau annonce un filtre actif dans /proc/1/status, qu'une sonde fournie voit l'appel visé refusé et les autres passer, et que le profil refuse encore ce que le défaut refusait, ce qui écarte un profil permissif.

Le second lab vous fait écrire, charger et attacher vous-même un profil AppArmor dédié, en mode enforce. Il attend l'inverse du profil de démonstration de cette leçon : au lieu d'autoriser largement puis de refuser une écriture par deny, le profil nomme ce dont le service a besoin et refuse le reste par omission. C'est le refus implicite vu plus haut, le seul que le mode complain laisse passer, donc le seul qui prouve le mode appliqué. Ses tests lisent le profil déclaré sur le conteneur, vérifient enforce dans /proc/1/attr/current, essaient deux lectures dont une doit être refusée par la politique et non par les permissions Unix, puis tentent de lire un fichier système dont le service n'a aucun besoin, pour écarter un profil à règle file, nue. Retenez aussi que la méthode « charger en complain, relever les refus, ajouter des règles » ne montre que ce que le service a tenté : elle ne révèle pas la règle signal (receive) peer=runc d'un service qui ignore SIGTERM.

Un refus se juge à sa couche avant de se corriger, et une correction ne retire jamais les autres protections.

  • Vous lisez les profils actifs dans /proc/1/status (Seccomp: 2) et /proc/1/attr/current (docker-default (enforce)), car SecurityOpt est vide par défaut.
  • Vous attribuez un refus en retirant une couche à la fois, parce que le message ne départage pas : seccomp, les capabilities et une capability refusée par AppArmor renvoient tous EPERM.
  • Vous repérez seccomp=unconfined, apparmor=unconfined et --privileged comme des réglages à refuser.
  • Vous durcissez un profil seccomp en retirant l'appel de la règle SCMP_ACT_ALLOW : un refus ajouté en fin de liste n'a rien bloqué à la mesure.
  • Vous attachez un profil AppArmor chargé avec apparmor_parser, vous vérifiez enforce, et pour journaliser un refus deny vous ajoutez audit.
  • Vous savez qu'un profil dédié remplace docker-default en entier : sans signal (receive) peer=runc, docker stop n'arrête plus le service proprement.
  • Vous installez le profil d'un service sous /etc/apparmor.d/, car un profil chargé par -a disparaît au redémarrage et le conteneur ne démarre plus.
  • Vous gardez en tête que complain n'arrête pas une règle deny, et qu'un profil seccomp tronqué ou absent arrête docker run (code 125) alors qu'un nom d'appel inconnu passe sans erreur.
  • Vous déchargez vos profils avec apparmor_parser -R à la fin.

Vérifiez que l'essentiel de cette leçon est acquis. Les questions portent sur la lecture des profils actifs, l'attribution d'un refus à sa couche, l'écriture des deux profils et leur tenue en exploitation (arrêt du service, redémarrage).

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

6 questions
6 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

Ce projet vous est utile ?

Les guides, formations, quiz et labs restent gratuits. Si vous les utilisez régulièrement, vous pouvez contribuer à leur maintenance et à leur développement.

Devenir membre de soutien (nouvel onglet) Soutenir une fois (nouvel onglet) En savoir plus

Suivez aussi le projet sur LinkedIn