
Atténuation Docker pour CVE-2026-31431 (« Copy Fail »). Inclut également des modèles Kubernetes.
Script idempotent pour bloquer la création de sockets AF_ALG pour tous les conteneurs Docker
sur un hôte, en tant qu'atténuation de CVE-2026-31431 (« Copy Fail »).
CVE-2026-31431 est une élévation de privilèges locale dans le modèle cryptographique
authencesn du noyau Linux, présente dans les noyaux compilés entre 2017 et
la disponibilité du correctif en amont (commit mainline a664bf3d603d). Un
utilisateur non privilégié peut enchaîner une opération de socket AF_ALG avec splice() pour
effectuer une écriture contrôlée de 4 octets dans le cache de pages de n'importe quel fichier lisible,
ciblant un binaire setuid pour obtenir un shell root. Une preuve de concept Python de 732 octets
exploite cela de manière fiable, sans courses ni offsets spécifiques aux distributions, sur chaque
grande distribution Linux livrant un noyau affecté.
La première étape obligatoire de l'exploit est l'ouverture d'un socket AF_ALG
(socket(AF_ALG, SOCK_SEQPACKET, 0)). Bloquer cet appel système via seccomp
empêche l'exploitation même sur des noyaux non corrigés. Le profil seccomp par défaut intégré de Docker
ne bloque pas , et n'est pas
suffisant — les clusters testés ont montré que les pods admis sous PSS Restricted
pouvaient toujours ouvrir des sockets .
AF_ALGRuntimeDefaultAF_ALGVoir l'avis du chercheur original sur https://copy.fail et l'avis CERT-EU sur https://cert.europa.eu/publications/security-advisories/2026-005/ pour tous les détails techniques et la disponibilité des correctifs par distribution.
Ce script couvre la configuration globale du démon Docker Engine. Pour
Kubernetes, voir la section Kubernetes ci-dessous. Pour les charges de travail
bare-metal ou VM (non conteneurisées), désactivez plutôt le module noyau algif_aead :
echo "install algif_aead /bin/false" > /etc/modprobe.d/disable-algif.conf
rmmod algif_aead 2>/dev/null || true
Cette approche n'a aucun impact sur dm-crypt/LUKS, kTLS, IPsec/XFRM, OpenSSL, GnuTLS, NSS ou SSH.
Extrait le profil seccomp intégré actif de Docker en inspectant le
HostConfig.SecurityOpt d'un conteneur éphémère. Cela évite toute
dépendance à une URL distante et garantit que le profil de base correspond à la
version de Docker réellement installée. Une récupération GitHub depuis
moby/profiles n'est utilisée comme solution de repli
que si l'inspection du conteneur ne donne rien.
Corrige le profil en supprimant socket de l'entrée de liste blanche de Docker
et en le ré-ajoutant avec un filtre d'arguments qui autorise toutes les familles d'adresses
sauf AF_ALG (valeur 38) en utilisant SCMP_CMP_NE. Tout le reste du comportement
seccomp par défaut de Docker est préservé.
Écrit le profil corrigé de manière atomique dans
/etc/seccomp/docker-block-af-alg.json (fichier temporaire + renommage). Ignoré si
le contenu sur disque est déjà identique.
Met à jour /etc/docker/daemon.json pour définir "seccomp-profile" sur le
chemin du profil corrigé. Le fichier original est sauvegardé dans daemon.json.bak
lors de la première modification. Ignoré si déjà configuré correctement.
Recharge dockerd via systemctl reload docker (SIGHUP — aucun redémarrage
requis). Ignoré si aucun des deux fichiers n'a changé.
Vérifie que le blocage est actif en exécutant une sonde dans un conteneur, indépendamment du fait que des modifications aient été apportées ou non aux étapes ci-dessus.
Le script est idempotent : l'exécuter plusieurs fois produit le même résultat et ne recharge Docker que lorsque quelque chose a réellement changé.
Remarque : les conteneurs
--privilegedcontournent tous les profils seccomp, quelle que soit cette configuration. Auditez vos fichiers Compose et vos commandes d'exécution pour les conteneurs privilégiés séparément.
docker dans le PATHcurl dans le PATH (solution de repli uniquement)systemctl (hôte systemd)/etc/seccomp et /etc/docker, et pour
systemctl reload docker# Appliquer l'atténuation et vérifier (utilisation normale)
sudo python3 harden-docker-seccomp.py
# Afficher ce qui changerait sans rien écrire ni recharger Docker
sudo python3 harden-docker-seccomp.py --dry-run
# Ré-exécuter uniquement la vérification du conteneur (aucune modification de configuration)
python3 harden-docker-seccomp.py --verify-only
# Sortie verbeuse
sudo python3 harden-docker-seccomp.py --verbose
INFO Written: /etc/seccomp/docker-block-af-alg.json
INFO Updated: /etc/docker/daemon.json
INFO Reloading Docker daemon (SIGHUP)…
INFO Docker daemon reloaded.
INFO Verifying AF_ALG is blocked inside a container…
OK: AF_ALG blocked — [Errno 1] Operation not permitted
INFO All done. CVE-2026-31431 (Copy Fail) mitigation is active.
INFO Profile unchanged: /etc/seccomp/docker-block-af-alg.json
INFO daemon.json already configured correctly.
INFO No changes — Docker daemon reload not required.
INFO Verifying AF_ALG is blocked inside a container…
OK: AF_ALG blocked — [Errno 1] Operation not permitted
INFO All done. CVE-2026-31431 (Copy Fail) mitigation is active.
| Code | Signification |
|---|---|
0 | Succès — l'atténuation est active |
1 | Script non exécuté en root (lors du patch), ou erreur irrécupérable |
2 | Échec de la vérification — AF_ALG n'est pas bloqué |
Les pods Kubernetes partagent le noyau de l'hôte, donc la même primitive de socket AF_ALG
est accessible depuis n'importe quel pod sur un nœud affecté. Le seccomp RuntimeDefault n'est pas
suffisant — les clusters testés ont montré que les pods admis sous PSS Restricted
pouvaient toujours ouvrir des sockets AF_ALG. Un profil Localhost avec une règle de refus explicite
est requis.
L'atténuation nécessite deux choses : le JSON du profil présent sur le système de fichiers de chaque nœud, et chaque spécification de pod le référençant. Les sections ci-dessous couvrent les deux, y compris comment injecter le profil globalement sans modifier les spécifications de pods individuelles.
Le kubelet résout les profils seccomp Localhost par rapport à sa racine seccomp,
qui par défaut est /var/lib/kubelet/seccomp. Le profil doit exister à ce
chemin sur chaque nœud pouvant planifier une charge de travail.
Appliquez la ConfigMap et le DaemonSet de ce dépôt :
kubectl apply -f templates/configmap-seccomp-profile.yaml
kubectl apply -f templates/daemonset-distribute-profile.yaml
Le DaemonSet exécute un conteneur init qui copie le profil depuis la ConfigMap vers la racine seccomp du kubelet du nœud, puis stationne un conteneur pause minimal pour que le pod reste visible pour la surveillance de santé. Il tolère toutes les taints afin de s'exécuter également sur les nœuds du plan de contrôle.
Racine seccomp kubelet non standard : RKE2 utilise
/var/lib/rancher/rke2/agent/kubelet/seccomp. Remplacez le chemin en définissantNODE_SECCOMP_ROOTdans l'environnement du conteneur init du DaemonSet avant l'application.
Vérifiez que le fichier est présent sur un nœud :
kubectl -n kube-system exec -it \
$(kubectl -n kube-system get pod -l app.kubernetes.io/name=distribute-af-alg-seccomp \
-o jsonpath='{.items[0].metadata.name}') -- \
cat /var/lib/kubelet/seccomp/block-af-alg.json
Plutôt que de modifier les spécifications de pods individuelles ou les charts Helm, utilisez un
webhook d'admission mutant pour injecter seccompProfile automatiquement au moment de l'admission.
Deux options sont fournies : Kyverno et OPA Gatekeeper.
Les deux approches n'injectent le profil que lorsqu'un pod n'en déclare pas déjà un, donc les pods avec des profils explicites sont laissés intacts.
Important : Les pods existants en cours d'exécution ne sont pas mutés rétroactivement. Après avoir appliqué la politique, redémarrez vos déploiements pour prendre en compte le profil injecté :
kubectl rollout restart deployment -A
Ce modèle utilise l'API MutatingPolicy (policies.kyverno.io/v1),
qui a atteint la disponibilité générale dans Kyverno 1.17. L'API héritée ClusterPolicy
(kyverno.io/v1) a été dépréciée dans Kyverno 1.17 (janvier 2026) et est
prévue pour suppression dans 1.20 (octobre 2026) ; ne l'utilisez pas pour de nouvelles politiques.
L'expression CEL matchConditions vérifie que seccompProfile est absent
avant de muter, donc les pods qui déclarent déjà un profil sont laissés intacts.
# Installer Kyverno (si pas déjà présent)
helm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update
helm upgrade --install kyverno kyverno/kyverno -n kyverno --create-namespace
# Appliquer la politique
kubectl apply -f templates/kyverno-mutate-seccomp.yaml
Vérifiez qu'un nouveau pod reçoit le profil injecté :
kubectl run test --image=busybox --restart=Never -- sleep 30
kubectl get pod test -o jsonpath='{.spec.securityContext.seccompProfile}'
# Attendu : {"localhostProfile":"block-af-alg.json","type":"Localhost"}
kubectl delete pod test
Le CRD de mutation Assign de Gatekeeper utilise une condition pathTests pour injecter le
profil uniquement lorsque spec.securityContext.seccompProfile n'existe pas déjà.
La mutation est stable depuis Gatekeeper 3.10+ ; aucun indicateur de fonctionnalité n'est requis.
# Installer Gatekeeper (si pas déjà présent)
helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts
helm repo update
helm install -n gatekeeper-system gatekeeper gatekeeper/gatekeeper \
--create-namespace
# Appliquer la mutation
kubectl apply -f templates/gatekeeper-assign-seccomp.yaml
Vérifiez :
kubectl run test --image=busybox --restart=Never -- sleep 30
kubectl get pod test -o jsonpath='{.spec.securityContext.seccompProfile}'
# Attendu : {"localhostProfile":"block-af-alg.json","type":"Localhost"}
kubectl delete pod test
Gatekeeper vs. Kyverno : L'approche
Assignopère au niveau du champ et nécessite que le webhook de mutation de Gatekeeper soit activé. LeMutatingPolicyde Kyverno avec unmatchConditionCEL gère le conditionnel en ligne. Les deux atteignent le même résultat — préférez celui qui est déjà déployé dans votre cluster.
Les pods avec hostPID: true, hostNetwork: true, ou
securityContext.privileged: true ont un accès élevé que seccomp seul ne
contient pas entièrement. Auditez ces charges de travail séparément et supprimez les privilèges lorsque
c'est possible.
La ConfigMap comme source de vérité. Le JSON du profil réside dans
configmap-seccomp-profile.yaml plutôt que d'être intégré dans le DaemonSet
ou dupliqué entre les fichiers. Le DaemonSet le monte et le copie sur le nœud.
Mettre à jour le profil signifie modifier une ConfigMap et redémarrer les pods du DaemonSet
— aucun autre fichier ne change.
Le DaemonSet utilise la priorité system-node-critical. Cela garantit que le
pod de distribution n'est pas expulsé avant que les charges de travail qu'il protège ne soient planifiées,
ce qui laisserait les nœuds avec un fichier de profil manquant et des pods bloqués dans
CreateContainerError.
Gatekeeper exclut kube-system et gatekeeper-system. Injecter un
profil Localhost dans des pods système qui peuvent précéder l'installation du DaemonSet
risque une référence de profil cassée si le fichier n'est pas encore présent sur
le nœud. La politique Kyverno n'a pas besoin de cette exclusion car Kyverno
gère l'ordre des webhooks plus gracieusement, mais des règles exclude peuvent être ajoutées
là aussi si nécessaire.
Injection conditionnelle, pas de remplacement. À la fois le matchCondition CEL
du MutatingPolicy Kyverno et le test de chemin MustNotExist de Gatekeeper signifient que la
politique d'admission n'agit que lorsqu'un pod n'a pas de seccompProfile existant.
Les charges de travail qui déclarent déjà leur propre profil — y compris celles qui
ont légitimement besoin de AF_ALG via une liste blanche personnalisée — sont laissées intactes.