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é.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »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/statuset/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 runou 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).
Prérequis
Section intitulée « Prérequis »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é :
ALP="alpine:3.24.3@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a"W=/tmp/c38b-m-workmkdir -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.
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.
| Couche | Ce qu'elle examine | Option Docker qui la règle |
|---|---|---|
| Capabilities | L'opération privilégiée demandée | --cap-add, --cap-drop |
| seccomp | Le nom de l'appel système et ses arguments numériques | --security-opt seccomp= |
| AppArmor | Les 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.
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 :
docker run --rm --network none "$ALP" sh -c 'grep -E "^(Seccomp|Seccomp_filters|NoNewPrivs):" /proc/1/status; cat /proc/1/attr/current'NoNewPrivs: 0Seccomp: 2Seccomp_filters: 1docker-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 :
docker run -d --network none --name c38b-m-ins "$ALP" sleep 120docker inspect c38b-m-ins --format 'SecurityOpt={{json .HostConfig.SecurityOpt}} AppArmorProfile={{json .AppArmorProfile}} Privileged={{.HostConfig.Privileged}}'docker rm -f c38b-m-insSecurityOpt=null AppArmorProfile="docker-default" Privileged=falseSecurityOpt 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.
Quelle couche a refusé mon appel ?
Section intitulée « Quelle couche a refusé mon appel ? »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 :
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=1unshare: unshare(0x10000000): Operation not permittedunshare_rc=0Le 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.
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 permittedchown_rc=1chown: /tmp/f: Operation not permittedchown_rc=1Le 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 conteneur | Résultat de mount | Couche qui refuse |
|---|---|---|
| aucune | permission denied (are you root?), code 1 | seccomp, qui n'autorise mount qu'avec SYS_ADMIN |
seccomp=unconfined | mounting none on /mnt failed: Permission denied, code 255 | AppArmor (docker-default) |
--cap-add SYS_ADMIN | mounting none on /mnt failed: Permission denied, code 255 | AppArmor (docker-default) |
--cap-add SYS_ADMIN et apparmor=unconfined | réussi, code 0 | aucune |
seccomp=unconfined et apparmor=unconfined | permission denied (are you root?), code 1 | la 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.
Que retirent unconfined et privileged ?
Section intitulée « Que retirent unconfined et privileged ? »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.
docker run --rm --network none --privileged "$ALP" sh -c 'grep -E "^(Seccomp|Seccomp_filters|NoNewPrivs):" /proc/1/status; cat /proc/1/attr/current'NoNewPrivs: 0Seccomp: 0Seccomp_filters: 0unconfinedAvec --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.
| Option | Champ Seccomp de /proc/1/status | /proc/1/attr/current |
|---|---|---|
| aucune (défaut) | 2 | docker-default (enforce) |
--security-opt seccomp=unconfined | 0 | docker-default (enforce) |
--security-opt apparmor=unconfined | 2 | unconfined |
--privileged | 0 | unconfined |
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.
Comment écrire un profil seccomp personnalisé ?
Section intitulée « Comment écrire un profil seccomp personnalisé ? »Un profil seccomp est un fichier JSON qui repose sur trois éléments :
defaultActiondit ce que devient un appel absent de la liste ;syscallsest la liste des règles ;- chaque règle porte
names(les appels visés),actionet, 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.
D'où partir ?
Section intitulée « D'où partir ? »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.
curl -fsSL "https://raw.githubusercontent.com/moby/moby/docker-v29.9.2/vendor/github.com/moby/profiles/seccomp/default.json" -o defaut.jsonsha256sum defaut.jsonpython3 -c "import json;d=json.load(open('defaut.json'));print('defaultAction=',d['defaultAction'],' defaultErrnoRet=',d.get('defaultErrnoRet'),' groupes=',len(d['syscalls']))"785b2429264afba4d594320337cb17f144f3c7d51585f9805eef72e28f4f9334 defaut.jsondefaultAction= SCMP_ACT_ERRNO defaultErrnoRet= 1 groupes= 37Le 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 :
cat > derive-ajout.py <<'EOF'import jsond = 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']))EOFpython3 derive-ajout.pydocker 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 : 38chmod rc=0unshare: unshare(0x10000000): Operation not permittedunshare=refuseRegardez 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.
Comment refuser un appel qui était autorisé ?
Section intitulée « Comment refuser un appel qui était autorisé ? »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.
cat > derive-retrait.py <<'EOF'import jsonVISES = {"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)EOFpython3 derive-retrait.pydocker 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 30docker exec c38b-m-d2 grep -E '^Seccomp' /proc/1/statusdocker rm -f c38b-m-d2règles qui autorisent la famille chmod : [(0, 'SCMP_ACT_ALLOW', ['chmod', 'fchmod', 'fchmodat', 'fchmodat2'])]chmod rc=1chmod: /tmp/f: Operation not permittedunshare=refuseunshare: unshare(0x10000000): Operation not permittedls=okSeccomp: 2Seccomp_filters: 1Trois 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.
Que peut-on régler d'autre dans une règle ?
Section intitulée « Que peut-on régler d'autre dans une règle ? »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.
cat > c38b-m-chmod-eperm.json <<'EOF'{ "defaultAction": "SCMP_ACT_ALLOW", "syscalls": [ { "names": ["chmod", "fchmod", "fchmodat", "fchmodat2"], "action": "SCMP_ACT_ERRNO" } ]}EOFdocker 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 permittedchmod600_rc=1mkdir_rc=0-rw-r--r-- 1 root root 0 Nov 4 18:39 /tmp/funshare_rc=0Seccomp: 2Seccomp_filters: 1Sans 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": 13renvoieEACCES, etchmodaffichechmod: /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 quechmodest un processus séparé, et la sortie a affichéBad system call (core dumped).- Une clé
argsconditionne 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).
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"}] } ]}EOFdocker 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=0chmod: /tmp/f: Operation not permittedchmod777_rc=1-rw------- 1 root root 0 Nov 4 18:39 /tmp/fchmod 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 inputdocker: 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 directoryEn 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.
Comment l'attacher dans un fichier Compose ?
Section intitulée « Comment l'attacher dans un fichier Compose ? »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.
mkdir -p "$W/compose" && cd "$W/compose"cp "$W/c38b-m-chmod-eperm.json" .cat > compose.yaml <<EOFname: c38b-m-projservices: app: image: $ALP command: ["sleep", "120"] network_mode: none security_opt: - seccomp=./c38b-m-chmod-eperm.jsonEOFdocker compose up -ddocker compose exec -T app sh -c 'touch /tmp/f; chmod 600 /tmp/f; echo rc=$?; grep -E "^Seccomp" /proc/1/status'docker compose downcd "$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 Startedchmod: /tmp/f: Operation not permittedrc=1Seccomp: 2Seccomp_filters: 1La 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.
Comment écrire et charger un profil AppArmor ?
Section intitulée « Comment écrire et charger un profil AppArmor ? »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.
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,}EOFsudo 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-cplxsudo apparmor_parser -a c38b-m-nowrite-cplxsudo 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.txt107 profiles are loaded. docker-defaultparser_rc=0109 profiles are loaded. c38b-m-nowrite docker-default c38b-m-nowrite-cplxLe 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 :
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 existsparser_a_rc=239parser_r_rc=0Une erreur de syntaxe se signale au chargement, avec le numéro de ligne et un code de retour 1 :
printf 'profile c38b-m-bad {\n file,\n deny /x/** wz,\n}\n' > c38b-m-badsudo 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_MODEbad_rc=1Attachez ensuite le profil à un conteneur, avec le dossier de test monté sur
/srv/vault et un tmpfs sur /srv/open pour comparer.
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-vaultcat_rc=0sh: can't create /srv/vault/secret.txt: Permission deniedwrite_rc=1touch: /srv/vault/new: Permission deniedtouch_rc=1mkdir: can't create directory '/srv/vault/d': Permission deniedmkdir_rc=1open_rc=0-rw-rw-rw- 1 1001 1001 14 Nov 4 18:40 /srv/vault/secret.txtuid=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 :
sed -e 's/c38b-m-nowrite/c38b-m-nochown/' -e 's# deny /srv/vault/\*\* w,# deny capability chown,#' c38b-m-nowrite > c38b-m-nochownsudo apparmor_parser -a c38b-m-nochowndocker 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 permittedchown_rc=1chown: /tmp/f: Operation not permittedchown_sans_seccomp_rc=1Le 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 deniedwrite_rc=1Une 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 directoryLe 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 :
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-audsudo apparmor_parser -a c38b-m-nowrite-auddocker 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-vaultsh: can't create /srv/vault/secret.txt: Permission deniedwrite_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=1001La 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 :
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/nullsleep 1docker exec c38b-m-svc cat /proc/1/attr/currentt0=$(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/nullsudo 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 :
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-servicecat c38b-m-servicesudo 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/nullsleep 1docker exec c38b-m-svc cat /proc/1/attr/currentt0=$(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=0c38b-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 :
sudo install -m 0644 -o root -g root c38b-m-service /etc/apparmor.d/c38b-m-servicesudo apparmor_parser -r /etc/apparmor.d/c38b-m-service; echo "parser_install_rc=$?"ls -l /etc/apparmor.d/c38b-m-servicesudo 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-service110 profiles are loaded. c38b-m-nochown c38b-m-nowrite c38b-m-serviceCette 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é.
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-servicec38b-m-service (enforce)service_rc=0docker: 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 informationnowrite_rc=127Seul 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.
Quelles sont les limites de ces profils ?
Section intitulée « Quelles sont les limites de ces profils ? »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.
Comment nettoyer ce que la leçon a créé ?
Section intitulée « Comment nettoyer ce que la leçon a créé ? »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.
sudo apparmor_parser -R /etc/apparmor.d/c38b-m-service; echo "unload c38b-m-service rc=$?"sudo rm /etc/apparmor.d/c38b-m-servicefor 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=$?"; donesudo aa-status | grep -c c38bsudo aa-status | grep -E 'profiles are loaded|docker-default'cd / && find /tmp/c38b-m-work -deletedocker ps -a --filter name=c38b-m- --format '{{.Names}}'unload c38b-m-service rc=0unload c38b-m-nowrite rc=0unload c38b-m-nowrite-cplx rc=0unload c38b-m-nowrite-aud rc=0unload c38b-m-nochown rc=00107 profiles are loaded. docker-defaultLe 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.
Mettre en pratique
Section intitulée « Mettre en pratique »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.
À retenir
Section intitulée « À retenir »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)), carSecurityOptest 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=unconfinedet--privilegedcomme 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érifiezenforce, et pour journaliser un refusdenyvous ajoutezaudit. - Vous savez qu'un profil dédié remplace
docker-defaulten entier : sanssignal (receive) peer=runc,docker stopn'arrête plus le service proprement. - Vous installez le profil d'un service sous
/etc/apparmor.d/, car un profil chargé par-adisparaît au redémarrage et le conteneur ne démarre plus. - Vous gardez en tête que
complainn'arrête pas une règledeny, et qu'un profil seccomp tronqué ou absent arrêtedocker run(code 125) alors qu'un nom d'appel inconnu passe sans erreur. - Vous déchargez vos profils avec
apparmor_parser -Rà la fin.
Contrôle de connaissances
Section intitulée « Contrôle de connaissances »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
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
Vérification
(0/0)Score par domaine
Un domaine regroupe plusieurs guides et reçoit plusieurs questions : son score est plus fiable qu'un score par thème, qui porte souvent sur une seule question.
Bilan
Points forts
Aucun sujet réussi à 80 % sur au moins deux questions pour l’instant.
Points faibles
Aucun sujet sous le seuil de réussite.
À faire ensuite
Des indices pour retenter votre chance ?
Continuer le cours : Racine en lecture seule, et arrêt sans perte
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications