Aperta Scientia Crest
← Retour au Tech Radar
📡 Aperta Intelligence Lab
Sécurité des conteneurs : pourquoi votre conteneur non-root perd ses capabilities (SCC, CRI-O, kernel)

Sécurité des conteneurs : pourquoi votre conteneur non-root perd ses capabilities (SCC, CRI-O, kernel)

📅 6 octobre 2026
Tech Radar Sécurité Conteneurs & Linux

Sommaire de l'analyse

Un sidecar de supervision tourne en non-root et doit exécuter des sondes ICMP : il lui faut CAP_NET_RAW. La SecurityContextConstraint (SCC) autorise la capability, le PodSpec la demande (capabilities.add: [NET_RAW]), CRI-O démarre le conteneur sans erreur — et pourtant ping échoue :

$ ping -c 1 10.0.0.1
ping: Operation not permitted
$ cat /proc/1/status | grep Cap
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000000000002000

Rien à voir avec un bug. Ce cas, documenté par le Red Hat Developer Blog le 5 octobre 2026, est la rencontre de trois systèmes qui ne se parlent pas : l’admission SCC côté API server, la traduction de la requête CRI par CRI-O en spec OCI, et la sémantique des capabilities dans le noyau Linux. Comprendre où s’arrête la SCC et où commence le kernel est la clé de tout débogage de privilèges conteneurisés.


1. La SCC est un portail d’admission, pas un moteur d’exécution

Une SCC détermine uniquement si une configuration de sécurité est autorisée à entrer dans le cluster (capabilities autorisées, plages d’UID, types de volumes, escalade de privilèges, contexte SELinux). Elle est évaluée par le contrôleur d’admission de l’API server OpenShift au moment de la création du Pod. Une fois le Pod admis, le travail de la SCC est terminé : elle n’a aucune composante runtime, ne parle pas à CRI-O, n’écrit aucune règle iptables et ne configure aucune capability de processus.

  • À l’échelle du cluster : la SCC est plus granulaire que Pod Security Admission (PSA) — PSA raisonne en 3 niveaux grossiers (privileged, baseline, restricted) là où la SCC contrôle champ par champ.

2. CRI-O : de la SCC au spec OCI, là où tout se joue

CRI-O n’a aucune notion de SCC : il reçoit un CreateContainerRequest et le traduit en spec OCI. Les champs simples (hostNetwork, hostPID, hostIPC) se mappent directement en modes de namespaces. Les capabilities, elles, passent par SpecSetPrivileges() et divergent selon le mode :

  • Conteneurs privilégiés : SetupPrivileged(true) remplit les cinq jeux de capabilities (41 capabilities connues). À noter : le champ dropCapabilities du PodSpec est alors ignoré — la logique add/drop est entièrement court-circuitée.
  • Conteneurs non privilégiés : CRI-O part d’une liste puis fusionne 9 capabilities par défaut (CHOWN, DAC_OVERRIDE, FSETID, FOWNER, SETGID, SETUID, SETPCAP, NET_BIND_SERVICE, KILL), sauf si le PodSpec déclare drop: ALL. C’est précisément pourquoi le motif drop: ALL + add: [une capability] est la bonne pratique de production : on repart d’un ensemble minimal maîtrisé.

CRI-O écrit ensuite la capability finale dans Bounding, Effective et Permitted — de façon identique que le conteneur tourne en root ou non. Le résultat runtime, lui, sera radicalement différent.


3. Kernel : cinq jeux de capabilities et une transition UID qui efface tout

Les capabilities décomposent le modèle superuser en unités distinctes, réparties en cinq jeux évalués différemment par le noyau :

  • Bounding (CapBnd) — plafond absolu de ce qu’un processus peut acquérir.
  • Permitted (CapPrm) — réservoir depuis lequel les capabilities effectives sont puisées.
  • Effective (CapEff) — ce que le noyau vérifie réellement à l’exécution ; sans CAP_NET_RAW ici, la création d’un socket brut est refusée quel que soit le contenu des autres jeux.
  • Inheritable (CapInh) — capabilities transmises au travers d’execve(), uniquement si le binaire porte les file capabilities correspondantes.
  • Ambient (CapAmb) — introduites avec Linux 4.3 pour résoudre exactement le problème du non-root : elles repeuplent automatiquement Permitted et Effective après execve().

Le mécanisme fatal : le runtime OCI démarre le processus en root, applique les jeux, puis bascule vers l’UID cible (ex. 1000). Or, lors de cette transition UID 0 → non-zéro, le noyau efface CapPrm et CapEff. Seuls Bounding subsiste. Et CRI-O ne positionne jamais CapInh ni CapAmb.


4. Pourquoi CRI-O n’active pas les capabilities ambiantes — décision assumée

Ce n’est pas un oubli : c’est un choix de sécurité documenté dans le code de CRI-O. Kubernetes suppose que runAsNonRoot entraîne la perte des capabilities ; activer les capabilities ambiantes casserait ce contrat implicite. Une KEP (2763 – ambient capabilities) existe pourtant côté SIG Security pour les rendre supportables de façon sélective — à suivre, car elle changerait la donne pour tous les sidecars non-root à besoin de privilège fin.


5. Les remèdes, et leur coût réel

  • File capabilities (setcap) — l’approche la plus ciblée : setcap 'cap_net_raw=+ep' /usr/bin/ping lors du build. Le noyau promeut la capability dans Permitted et Effective à l’exécution, même en non-root, tant qu’elle reste dans le Bounding set. Coût : modifier l’image ; les file capabilities sont stockées en attributs étendus, donc le pipeline de build et le driver de stockage doivent les préserver — et la confiance se déplace vers la provenance de l’image.
  • Exécution en root — évite la transition UID. Coût : la SCC de production la plus courante (restricted-v2) l’interdit.
  • Conteneur privilégié — remplit les cinq jeux, mais désactive SELinux, AppArmor et seccomp et court-circuite tout le add/drop : « une masse pour écraser une punaise ». À proscrire hors debug.

La leçon d’ingénierie : la SCC, CRI-O et le noyau sont trois couches successives, chacune avec son propre modèle mental. Devant un CapEff: 0000000000000000 en non-root, on ne cherche pas un bug : on relit la transition UID et la sémantique des capabilities.


6. Sources & Références Techniques

Appliquer ces technologies en production

Découvrez nos cursus intensifs de 3 mois AS200 (DevOps) et AS300 (AI Platform Engineer).

Découvrir les cursus →