
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 AF_ALG, et 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.
Voir 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