
Mitigación de Docker para CVE-2026-31431 ('Copy Fail'). Incluye también plantillas de Kubernetes.
Script idempotente para bloquear la creación de sockets AF_ALG para todos los contenedores Docker
en un host como mitigación para CVE-2026-31431 ("Copy Fail").
CVE-2026-31431 es una escalada de privilegios local en la plantilla criptográfica
authencesn del kernel de Linux, presente en kernels compilados entre 2017 y
la disponibilidad del parche oficial (commit de mainline a664bf3d603d). Un
usuario sin privilegios puede encadenar una operación de socket AF_ALG con splice() para
realizar una escritura controlada de 4 bytes en la caché de páginas de cualquier archivo legible,
apuntando a un binario setuid para obtener una shell root. Una prueba de concepto en Python de 732 bytes
explota esto de forma fiable, sin condiciones de carrera ni offsets por distribución, en todas
las principales distribuciones de Linux que incluyen un kernel afectado.
El primer paso obligatorio del exploit es abrir un socket AF_ALG
(socket(AF_ALG, SOCK_SEQPACKET, 0)). Bloquear esa syscall mediante seccomp
previene la explotación incluso en kernels sin parche. El perfil seccomp predeterminado integrado
de Docker no bloquea , y no es
suficiente: los clústeres probados mostraron que los pods admitidos bajo PSS Restricted
aún podían abrir sockets .
AF_ALGRuntimeDefaultAF_ALGConsulte el aviso del investigador original en https://copy.fail y el aviso de CERT-EU en https://cert.europa.eu/publications/security-advisories/2026-005/ para obtener detalles técnicos completos y disponibilidad de parches por distribución.
Este script cubre la configuración global del daemon de Docker Engine. Para
Kubernetes, consulte la sección de Kubernetes a continuación. Para cargas de trabajo
en bare-metal o VM (no contenedorizadas), deshabilite el módulo del kernel algif_aead
en su lugar:
echo "install algif_aead /bin/false" > /etc/modprobe.d/disable-algif.conf
rmmod algif_aead 2>/dev/null || true
Ese enfoque no tiene impacto en dm-crypt/LUKS, kTLS, IPsec/XFRM, OpenSSL, GnuTLS, NSS o SSH.
Extrae el perfil seccomp integrado activo de Docker inspeccionando el
HostConfig.SecurityOpt de un contenedor de corta duración. Esto evita cualquier
dependencia de una URL remota y garantiza que el perfil base coincida con la
versión de Docker realmente instalada. Una descarga de GitHub desde
moby/profiles se utiliza como respaldo
solo si la inspección del contenedor no produce resultados.
Parchea el perfil eliminando socket de la entrada de lista blanca de Docker
y re-agregándolo con un filtro de argumentos que permite todas las familias de direcciones
excepto AF_ALG (valor 38) usando SCMP_CMP_NE. Todo el resto del comportamiento
seccomp predeterminado de Docker se conserva.
Escribe el perfil parcheado de forma atómica en
/etc/seccomp/docker-block-af-alg.json (archivo temporal + renombrado). Se omite si
el contenido en disco ya es idéntico.
Actualiza /etc/docker/daemon.json para establecer "seccomp-profile" en la
ruta del perfil parcheado. El archivo original se respalda en daemon.json.bak
en la primera modificación. Se omite si ya está configurado correctamente.
Recarga dockerd mediante systemctl reload docker (SIGHUP: no se requiere
reinicio). Se omite si ninguno de los archivos cambió.
Verifica que el bloqueo esté activo ejecutando una sonda dentro de un contenedor, independientemente de si se realizaron cambios en los pasos anteriores.
El script es idempotente: ejecutarlo varias veces produce el mismo resultado y solo recarga Docker cuando algo realmente cambió.
Nota: Los contenedores
--privilegedomiten todos los perfiles seccomp independientemente de esta configuración. Audite sus archivos Compose y comandos de ejecución para contenedores privilegiados por separado.
docker en PATHcurl en PATH (solo respaldo)systemctl (host con systemd)/etc/seccomp y /etc/docker, y para
systemctl reload docker# Aplicar la mitigación y verificar (uso normal)
sudo python3 harden-docker-seccomp.py
# Mostrar qué cambiaría sin escribir nada ni recargar Docker
sudo python3 harden-docker-seccomp.py --dry-run
# Re-ejecutar solo la verificación del contenedor (sin cambios de configuración)
python3 harden-docker-seccomp.py --verify-only
# Salida detallada
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.
| Código | Significado |
|---|---|
0 | Éxito: la mitigación está activa |
1 | El script no se ejecutó como root (al parchear), o error irrecuperable |
2 | Falló la verificación: AF_ALG no está bloqueado |
Los pods de Kubernetes comparten el kernel del host, por lo que la misma primitiva de socket
AF_ALG es accesible desde cualquier pod en un nodo afectado. El seccomp RuntimeDefault no es
suficiente: los clústeres probados mostraron que los pods admitidos bajo PSS Restricted
aún podían abrir sockets AF_ALG. Se requiere un perfil Localhost con una regla de denegación explícita.
La mitigación requiere dos cosas: el JSON del perfil presente en el sistema de archivos de cada nodo, y cada especificación de pod que lo referencie. Las secciones a continuación cubren ambas, incluyendo cómo inyectar el perfil globalmente sin modificar las especificaciones individuales de los pods.
El kubelet resuelve los perfiles seccomp Localhost en relación con su raíz de seccomp,
que por defecto es /var/lib/kubelet/seccomp. El perfil debe existir en esa
ruta en cada nodo que pueda programar una carga de trabajo.
Aplique el ConfigMap y el DaemonSet de este repositorio:
kubectl apply -f templates/configmap-seccomp-profile.yaml
kubectl apply -f templates/daemonset-distribute-profile.yaml
El DaemonSet ejecuta un contenedor init que copia el perfil desde el ConfigMap a la raíz de seccomp del kubelet del nodo, y luego estaciona un contenedor pause mínimo para que el pod permanezca visible para el monitoreo de salud. Tolera todos los taints para que también se ejecute en nodos del plano de control.
Raíz de seccomp del kubelet no estándar: RKE2 usa
/var/lib/rancher/rke2/agent/kubelet/seccomp. Sobrescriba la ruta estableciendoNODE_SECCOMP_ROOTen el entorno del contenedor init del DaemonSet antes de aplicar.
Verifique que el archivo esté presente en un nodo:
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
En lugar de modificar las especificaciones individuales de los pods o los charts de Helm, use un
webhook de admisión mutante para inyectar seccompProfile automáticamente en el momento de la admisión.
Se proporcionan dos opciones: Kyverno y OPA Gatekeeper.
Ambos enfoques solo inyectan el perfil cuando un pod no declara ya uno, por lo que los pods con perfiles explícitos no se modifican.
Importante: Los pods existentes en ejecución no se mutan retroactivamente. Después de aplicar la política, reinicie sus deployments para que recojan el perfil inyectado:
kubectl rollout restart deployment -A
Esta plantilla usa la API MutatingPolicy (policies.kyverno.io/v1),
que alcanzó GA en Kyverno 1.17. La API heredada ClusterPolicy
(kyverno.io/v1) fue deprecada en Kyverno 1.17 (enero de 2026) y está
planificada para su eliminación en 1.20 (octubre de 2026); no la use para políticas nuevas.
La expresión CEL matchConditions verifica que seccompProfile esté ausente
antes de mutar, por lo que los pods que ya declaran un perfil no se modifican.
# Instalar Kyverno (si aún no está presente)
helm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update
helm upgrade --install kyverno kyverno/kyverno -n kyverno --create-namespace
# Aplicar la política
kubectl apply -f templates/kyverno-mutate-seccomp.yaml
Verifique que un pod nuevo reciba el perfil inyectado:
kubectl run test --image=busybox --restart=Never -- sleep 30
kubectl get pod test -o jsonpath='{.spec.securityContext.seccompProfile}'
# Esperado: {"localhostProfile":"block-af-alg.json","type":"Localhost"}
kubectl delete pod test
Consulte templates/kyverno-mutate-seccomp.yaml.
El CRD de mutación Assign de Gatekeeper usa una condición pathTests para inyectar el
perfil solo cuando spec.securityContext.seccompProfile no existe ya.
La mutación ha sido estable desde Gatekeeper 3.10+; no se requiere ningún flag de funcionalidad.
# Instalar Gatekeeper (si aún no está presente)
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
# Aplicar la mutación
kubectl apply -f templates/gatekeeper-assign-seccomp.yaml
Verifique:
kubectl run test --image=busybox --restart=Never -- sleep 30
kubectl get pod test -o jsonpath='{.spec.securityContext.seccompProfile}'
# Esperado: {"localhostProfile":"block-af-alg.json","type":"Localhost"}
kubectl delete pod test
Consulte templates/gatekeeper-assign-seccomp.yaml.
Gatekeeper vs. Kyverno: El enfoque
Assignopera a nivel de campo y requiere que el webhook de mutación de Gatekeeper esté habilitado. ElMutatingPolicyde Kyverno con unmatchConditionCEL maneja la condición condicional en línea. Cualquiera logra el mismo resultado: prefiera el que ya esté implementado en su clúster.
Los pods con hostPID: true, hostNetwork: true o
securityContext.privileged: true tienen acceso elevado que seccomp por sí solo no
contiene por completo. Audite esas cargas de trabajo por separado y elimine los privilegios cuando sea posible.
ConfigMap como fuente de verdad. El JSON del perfil reside en
configmap-seccomp-profile.yaml en lugar de estar incrustado en el DaemonSet
o duplicado entre archivos. El DaemonSet lo monta y lo copia al nodo.
Actualizar el perfil significa editar un ConfigMap y reiniciar los pods del DaemonSet:
ningún otro archivo cambia.
El DaemonSet usa prioridad system-node-critical. Esto garantiza que el pod de
distribución no sea desalojado antes de que se programen las cargas de trabajo que protege,
lo que dejaría nodos con un archivo de perfil faltante y pods atascados en
CreateContainerError.
Gatekeeper excluye kube-system y gatekeeper-system. Inyectar un
perfil Localhost en pods del sistema que pueden ser anteriores a la instalación del DaemonSet
conlleva el riesgo de una referencia de perfil rota si el archivo aún no está presente en
el nodo. La política de Kyverno no necesita esta exclusión porque Kyverno
maneja el ordenamiento de webhooks de manera más elegante, pero las reglas exclude
también se pueden agregar allí si es necesario.
Inyección condicional, no sobrescritura. Tanto el matchCondition CEL del
MutatingPolicy de Kyverno como la prueba de ruta MustNotExist de Gatekeeper significan que la
política de admisión solo actúa cuando un pod no tiene un seccompProfile existente.
Las cargas de trabajo que ya declaran su propio perfil, incluidas aquellas que
legítimamente necesitan AF_ALG mediante una lista blanca personalizada, no se modifican.