AppArmor limite ce qu'un conteneur peut atteindre sur le nœud, fichiers, capacités et réseau, même si son processus s'exécute en root. Là où RBAC contrôle l'accès aux ressources Kubernetes, AppArmor contrôle l'accès au système qui les exécute. Ce guide porte sur son application dans Kubernetes ; pour les bases Linux, consultez le guide AppArmor Linux.
AppArmor a un jumeau qu'il ne remplace pas : seccomp filtre les appels système, quand AppArmor filtre les ressources. Les deux s'appliquent en même temps, et la dernière section de cette page montre comment.
AppArmor relève du domaine System Hardening de la certification CKS, qui pèse 15 % de l'épreuve. Trois compétences y sont attendues et structurent ce guide : appliquer un profil par securityContext, savoir ce que recouvre RuntimeDefault, et distinguer la syntaxe actuelle, valable depuis la 1.30, des annotations qu'elle remplace.
Prérequis
Section intitulée « Prérequis »- Cluster Kubernetes 1.30+ pour
securityContext.appArmorProfile(le champ est stable depuis 1.31) - Nœuds Linux avec AppArmor activé (
aa-enabledrépondYes)
Ce dernier point n'est pas une formalité, et il exclut la plupart des labs. Sur un cluster dont les nœuds sont des conteneurs, kind ou k3d, AppArmor n'est pas disponible, et le kubelet ne se contente pas de l'ignorer : il rejette le Pod.
Status: FailedReason: AppArmorMessage: Pod was rejected: Cannot enforce AppArmor: AppArmor is not enabled on the hostLe message est le même que le profil demandé soit RuntimeDefault ou un profil
nommé, et il apparaît aussi en événement Warning. Si vous le rencontrez, ce
n'est pas votre manifeste qui est en cause : c'est le nœud. Cette page demande
donc une machine virtuelle, là où sa jumelle
seccomp se
pratique sur un cluster local.
- Accès
kubectlavec droits de créer des pods
AppArmor, Profils de contrôle d'accès
Section intitulée « AppArmor, Profils de contrôle d'accès »AppArmor restreint l'accès d'un processus aux fichiers, répertoires, capabilities et appels réseau via des profils déclaratifs chargés dans le noyau.
Vérifier AppArmor sur les nœuds
Section intitulée « Vérifier AppArmor sur les nœuds »AppArmor est une fonctionnalité du noyau du nœud, pas de Kubernetes : si le module n'est pas actif, aucun manifeste ne le fera apparaître. Commencez donc toujours par cette vérification, avant même d'écrire un profil. Le fichier /sys/kernel/security/apparmor/profiles est la source la plus fiable, c'est celle que le kubelet consulte pour décider s'il accepte un Pod demandant un profil Localhost.
# Sur le nœud (via ssh ou node debug)aa-enabled # répond Yes ou Noaa-status # Lister les profils chargésapparmor_parser -V # Version
# Profils disponiblesls /etc/apparmor.d/cat /sys/kernel/security/apparmor/profilesSur une distribution Debian ou Ubuntu, aa-enabled répond Yes d'origine. Sur RHEL, Rocky ou AlmaLinux, la réponse sera No : ces distributions utilisent SELinux, et les profils AppArmor de cette page n'y sont pas transposables.
Structure d'un profil AppArmor
Section intitulée « Structure d'un profil AppArmor »Un profil AppArmor se lit ligne à ligne comme une liste de chemins suivis de permissions : r lecture, w écriture, x exécution, i héritage du profil courant. Ce qui n'est pas explicitement autorisé est refusé. Les deux deny de l'exemple sont donc redondants sur le papier, mais ils rendent l'intention lisible et prennent le dessus sur une règle plus permissive héritée d'un #include.
#include <tunables/global>
profile k8s-app-readonly flags=(attach_disconnected) { #include <abstractions/base>
# Autoriser la lecture/exécution du binaire /usr/bin/myapp rix,
# Lecture seule sur /etc /etc/** r,
# Lecture/écriture sur /tmp uniquement /tmp/** rw,
# Interdire tout accès réseau deny network,
# Interdire ptrace (empêche les outils d'introspection) deny ptrace,}Charger un profil sur le nœud
Section intitulée « Charger un profil sur le nœud »Kubernetes ne charge jamais un profil AppArmor pour vous : il se contente de demander au runtime d'appliquer un profil déjà présent dans le noyau du nœud. Cette étape est donc à répéter sur chaque nœud éligible, et à rejouer après chaque redémarrage si vous n'avez pas déposé le fichier dans /etc/apparmor.d/ (d'où le service apparmor le recharge au boot).
# -r remplace le profil s'il est déjà chargé, -W met le cache à joursudo apparmor_parser -r -W /etc/apparmor.d/k8s-app-readonly
# Vérifier qu'il est chargésudo aa-status | grep k8s-app-readonlyUn profil se charge en mode enforce par défaut. Pour le charger en mode observation, ajoutez flags=(complain) dans sa déclaration ou passez -C à apparmor_parser.
Appliquer AppArmor à un Pod, syntaxe actuelle (v1.30+)
Section intitulée « Appliquer AppArmor à un Pod, syntaxe actuelle (v1.30+) »Depuis Kubernetes 1.30, AppArmor se configure via securityContext.appArmorProfile :
apiVersion: v1kind: Podmetadata: name: app-apparmorspec: securityContext: appArmorProfile: type: Localhost localhostProfile: k8s-app-readonly # Nom du profil chargé sur le nœud containers: - name: app image: nginx:1.31-alpine@sha256:72ba65eb42c10344912a84ff42408db7d34f2feb642204570ab8fc5ffd29f1d3 securityContext: appArmorProfile: # On peut surcharger par conteneur type: RuntimeDefault # Utiliser le profil par défaut du runtimeLes trois types disponibles pour appArmorProfile.type :
| Type | Signification |
|---|---|
Unconfined | Pas de profil AppArmor (déconseillé) |
RuntimeDefault | Profil par défaut du runtime (docker-default) |
Localhost | Profil spécifique chargé sur le nœud |
Le nom exact du profil RuntimeDefault dépend du runtime installé sur le nœud. Avec containerd, la documentation Kubernetes montre cri-containerd.apparmor.d dans la sortie de /proc/1/attr/current ; docker-default est le nom historique côté Docker. Ne codez donc jamais ce nom en dur dans un manifeste : demandez RuntimeDefault et laissez le runtime résoudre.
Priorité Pod vs conteneur
Section intitulée « Priorité Pod vs conteneur »AppArmor et seccomp peuvent être définis au niveau Pod et au niveau conteneur. Le champ conteneur prend toujours le dessus :
| Mécanisme | Niveau Pod | Niveau container | Priorité |
|---|---|---|---|
| Seccomp | spec.securityContext.seccompProfile | spec.containers[].securityContext.seccompProfile | Container > Pod |
| AppArmor | spec.securityContext.appArmorProfile | spec.containers[].securityContext.appArmorProfile | Container > Pod |
Cela permet d'appliquer un profil par défaut au Pod et d'en surcharger un pour un conteneur spécifique (init container, sidecar).
Syntaxe legacy, annotations (avant v1.30)
Section intitulée « Syntaxe legacy, annotations (avant v1.30) »Vous croiserez encore cette syntaxe dans des dépôts existants et dans les questions d'examen. Deux différences comptent : l'annotation se pose sur les métadonnées du Pod et non dans securityContext, et elle nomme explicitement le conteneur visé dans la clé, ce qui interdit toute déclaration au niveau du Pod entier.
apiVersion: v1kind: Podmetadata: name: app-apparmor-legacy annotations: # Format : container.apparmor.security.beta.kubernetes.io/<container-name> container.apparmor.security.beta.kubernetes.io/app: localhost/k8s-app-readonly # Autres valeurs possibles : runtime/default ou unconfinedspec: containers: - name: app image: nginx:1.31-alpine@sha256:4a73073bd557c65b759505da037898b61f1be6cbcc3c2c3aeac22d2a470c1752Combiner AppArmor et seccomp
Section intitulée « Combiner AppArmor et seccomp »Ce sont deux mécanismes complémentaires, ils s'appliquent en même temps, et c'est ainsi qu'ils se posent en production. Le manifeste ci-dessous déclare les deux, plus les trois réglages de securityContext qui les accompagnent toujours :
apiVersion: v1kind: Podmetadata: name: pod-hardenedspec: securityContext: seccompProfile: type: RuntimeDefault # Seccomp : filtrer les syscalls runAsNonRoot: true runAsUser: 1000 containers: - name: app image: nginx:1.31-alpine@sha256:4a73073bd557c65b759505da037898b61f1be6cbcc3c2c3aeac22d2a470c1752 securityContext: appArmorProfile: type: RuntimeDefault # AppArmor : restreindre l'accès fichiers/réseau allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: ["ALL"]Ce pod applique une défense en profondeur. Chaque ligne couvre une surface différente, et aucune ne remplace les autres :
- Seccomp
RuntimeDefault: environ 44 syscalls dangereux bloqués sur plus de 300 - AppArmor
RuntimeDefault: profil du runtime (cri-containerd.apparmor.davec containerd) capabilities: drop: ALL: zéro capability LinuxreadOnlyRootFilesystem: true: système de fichiers immuable- image référencée par digest : le contenu exécuté ne peut plus changer sous un même tag
Cette annulation se constate directement, avec le même runtime que celui du cluster. Un conteneur normal renvoie Seccomp: 2 et un profil AppArmor docker-default (enforce) ; le même conteneur lancé en --privileged renvoie Seccomp: 0 et unconfined. Aucun message d'erreur n'est émis : la protection disparaît en silence.
Vérifier l'application des profils
Section intitulée « Vérifier l'application des profils »Ne vous fiez jamais au manifeste pour conclure qu'un profil est actif. La commande la plus rapide est celle documentée par Kubernetes : lire l'attribut du processus 1 du conteneur depuis le Pod lui-même, sans avoir besoin d'un accès SSH au nœud.
# Le plus simple : lire l'attribut du PID 1 depuis le conteneurkubectl exec pod-hardened -- cat /proc/1/attr/current# Attendu : cri-containerd.apparmor.d (enforce) ou k8s-app-readonly (enforce)Si le Pod ne dispose pas de shell, ou si vous enquêtez sur un conteneur déjà arrêté, il faut passer par le nœud :
# Vérifier les profils AppArmor actifs sur un nœudssh node01sudo aa-status
# Confirmer le profil d'un conteneur (via le PID)# 1. Trouver le PID du conteneur sur le nœudcrictl inspect <container-id> | jq '.info.pid'# 2. Lire le profilcat /proc/<pid>/attr/current
# Tester qu'un profil bloque bien (mode complain → enforce)sudo aa-complain /etc/apparmor.d/k8s-app-readonly # Loggue sans bloquersudo aa-enforce /etc/apparmor.d/k8s-app-readonly # Bloque effectivementLe suffixe entre parenthèses est ce qui compte : (enforce) signifie que les violations sont refusées, (complain) qu'elles sont seulement journalisées. Un profil laissé en complain après une phase de mise au point donne l'illusion d'un durcissement sans en apporter aucun.
Appliquer à grande échelle avec PodSecurityAdmission
Section intitulée « Appliquer à grande échelle avec PodSecurityAdmission »Le profil restricted de la Pod Security Admission ne mutate pas les Pods : il n'injecte aucun profil, il refuse ceux qui déclarent explicitement Unconfined. Le détail de ce mécanisme côté seccomp, ainsi que le réglage seccompDefault du kubelet, vit sur la page seccomp.
apiVersion: v1kind: Namespacemetadata: name: production labels: pod-security.kubernetes.io/enforce: restricted pod-security.kubernetes.io/enforce-version: latest pod-security.kubernetes.io/warn: restricted pod-security.kubernetes.io/warn-version: latest pod-security.kubernetes.io/audit: restricted pod-security.kubernetes.io/audit-version: latestLes Pod Security Standards encadrent aussi AppArmor : avec restricted, seuls RuntimeDefault et Localhost sont autorisés. Utilisez Kyverno ou Gatekeeper si vous voulez aller plus loin, par exemple exiger explicitement un profil Localhost ou gérer des exceptions par namespace :
apiVersion: policies.kyverno.io/v1kind: ValidatingPolicymetadata: name: require-apparmor-profilespec: validationActions: [Deny] matchConstraints: resourceRules: - apiGroups: [""] apiVersions: ["v1"] operations: ["CREATE", "UPDATE"] resources: ["pods"] validations: - expression: >- object.spec.containers.all(c, (has(c.securityContext) && has(c.securityContext.appArmorProfile) && c.securityContext.appArmorProfile.type in ['RuntimeDefault', 'Localhost']) || (has(object.spec.securityContext) && has(object.spec.securityContext.appArmorProfile) && object.spec.securityContext.appArmorProfile.type in ['RuntimeDefault', 'Localhost']) ) message: "Chaque conteneur doit déclarer un appArmorProfile de type RuntimeDefault ou Localhost."L'expression CEL teste d'abord le securityContext du conteneur, puis celui du Pod : c'est exactement l'ordre de priorité appliqué par Kubernetes. Elle ignore volontairement les annotations dépréciées, sinon la politique validerait des manifestes que les clusters récents n'appliquent plus.
Dépannage
Section intitulée « Dépannage »Les deux premières lignes de ce tableau décrivent le même oubli vu de deux endroits différents : le profil n'est pas chargé sur le nœud qui a récolté le Pod. Vérifiez toujours le nœud d'atterrissage (kubectl get pod -o wide) et non un nœud au hasard, car un cluster hétérogène peut n'avoir le profil que sur une partie de la flotte.
| Symptôme | Cause probable | Solution |
|---|---|---|
Pod reste Pending | Profil AppArmor inexistant sur le nœud | aa-status sur le nœud, vérifier le nom du profil |
Error: failed to create containerd task | Profil non chargé | apparmor_parser -r /etc/apparmor.d/<profil> |
| Application refusée sans message clair | Profil trop strict | Passer le profil en mode complain, lire dmesg | grep -i apparmor et journalctl -k |
| Annotation AppArmor ignorée | Kubernetes ≥ 1.30 avec securityContext conflictuel | securityContext prime sur l'annotation deprecated |
aa-enabled retourne no | AppArmor non activé dans le kernel | Vérifier GRUB_CMDLINE_LINUX="apparmor=1 security=apparmor" |
Limitations
Section intitulée « Limitations »Ces cinq points expliquent pourquoi AppArmor est rarement déployé partout dans un cluster réel. Les trois premiers relèvent de l'infrastructure, les deux derniers de vos propres manifestes. Retenez surtout le troisième : dès que vous quittez RuntimeDefault, vous ajoutez un profil à maintenir sur chaque nœud, et cette dette opérationnelle coûte souvent plus que le gain de sécurité obtenu.
- Linux uniquement : AppArmor ne fonctionne pas sur les nœuds Windows, ni sur un nœud dont le noyau ne l'active pas
- Support du runtime : le profil
RuntimeDefaultdépend de l'implémentation containerd ou CRI-O du nœud - Profil nommé = distribution manuelle : il doit être chargé sur tous les nœuds où le Pod peut être schedulé, ce qui coûte cher en cluster hétérogène
- Environnements managés (EKS, GKE, AKS) : l'accès aux nœuds pour charger un profil est souvent limité ou impossible
privileged: trueannule tout : les conteneurs privilégiés ignorent AppArmor, comme ils ignorent seccomp
Testez vos connaissances
Section intitulée « Testez vos connaissances »Les questions portent sur ce qui est réellement discriminant à l'examen CKS : le champ appArmorProfile face aux annotations qu'il remplace, la priorité entre niveau Pod et niveau conteneur, et ce qui arrive à un Pod quand le profil n'est pas chargé sur le nœud. Une réponse ratée désigne la section à relire.
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)Profil de compétences
Quoi faire maintenant
Ressources pour progresser
Des indices pour retenter votre chance ?
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications
À retenir
Section intitulée « À retenir »- AppArmor contrôle l'accès aux fichiers, capacités et réseau via des profils nommés
- Le nœud doit activer AppArmor : sinon le kubelet rejette le Pod, il ne l'exécute pas sans protection
- Le niveau conteneur prend le dessus sur le niveau Pod
- Depuis Kubernetes 1.30 : utiliser
securityContext.appArmorProfile, annotations dépréciées, champ stable depuis 1.31 - Le profil doit être chargé sur chaque nœud où le Pod peut s'exécuter
- Le suffixe
(enforce)est le seul qui protège ;(complain)journalise et laisse passer privileged: trueannule le profil sans le moindre message- Seccomp est complémentaire, pas alternatif : les appliquer ensemble
Mettre en pratique
Section intitulée « Mettre en pratique »Ce confinement ne se démontre pas sur un cluster en conteneurs, qui partage le noyau de l'hôte : le lab tourne sur une machine virtuelle où le profil est réellement chargé. Vous chargez un profil AppArmor sur le nœud, l'appliquez à un conteneur par le champ securityContext, et prouvez que le confinement agit en constatant l'échec d'une écriture interdite.
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Supply Chain Security : Le durcissement en amont, sur l'image elle-même, avant tout profil système.
- Falco : La détection des comportements suspects que votre profil laisse passer.
- Runtime Sandboxes : L'isolation par gVisor ou Kata, un cran au-dessus des profils AppArmor et seccomp.