
Mitigação do Docker para CVE-2026-31431 ('Copy Fail'). Inclui também modelos Kubernetes.
Script idempotente para bloquear a criação de sockets AF_ALG para todos os contêineres Docker
em um host como mitigação para CVE-2026-31431 ("Copy Fail").
A CVE-2026-31431 é uma escalada de privilégio local no template criptográfico
authencesn do kernel Linux, presente em kernels compilados entre 2017 e
a disponibilidade da correção upstream (commit mainline a664bf3d603d). Um
usuário sem privilégios pode encadear uma operação de socket AF_ALG com splice() para
realizar uma escrita controlada de 4 bytes no cache de páginas de qualquer arquivo legível,
visando um binário setuid para obter um shell root. Uma prova de conceito em Python de 732 bytes
explora isso de forma confiável, sem corridas ou offsets por distribuição, em todas as
principais distribuições Linux que possuem um kernel afetado.
O primeiro passo obrigatório do exploit é abrir um socket AF_ALG
(socket(AF_ALG, SOCK_SEQPACKET, 0)). Bloquear essa syscall via seccomp
impede a exploração mesmo em kernels sem correção. O perfil seccomp padrão integrado do Docker
não bloqueia , e não é
suficiente — clusters testados mostraram que pods admitidos sob PSS Restricted
ainda podiam abrir sockets .
AF_ALGRuntimeDefaultAF_ALGConsulte o advisory do pesquisador original em https://copy.fail e o advisory da CERT-EU em https://cert.europa.eu/publications/security-advisories/2026-005/ para detalhes técnicos completos e disponibilidade de correção por distribuição.
Este script cobre a configuração global do daemon do Docker Engine. Para
Kubernetes, consulte a seção Kubernetes abaixo. Para cargas de trabalho
bare-metal ou VM (não containerizadas), desative o módulo do kernel algif_aead
em vez disso:
echo "install algif_aead /bin/false" > /etc/modprobe.d/disable-algif.conf
rmmod algif_aead 2>/dev/null || true
Essa abordagem não tem impacto em dm-crypt/LUKS, kTLS, IPsec/XFRM, OpenSSL, GnuTLS, NSS ou SSH.
Extrai o perfil seccomp padrão integrado ativo do Docker inspecionando o
HostConfig.SecurityOpt de um contêiner de curta duração. Isso evita qualquer
dependência de uma URL remota e garante que o perfil base corresponda à
versão do Docker realmente instalada. Uma busca no GitHub em
moby/profiles é usada como fallback
apenas se a inspeção do contêiner não retornar nada.
Corrige o perfil removendo socket da entrada de allowlist do Docker
e o adicionando novamente com um filtro de argumento que permite todas as famílias de endereços
exceto AF_ALG (valor 38) usando SCMP_CMP_NE. Todo o resto do comportamento
seccomp padrão do Docker é preservado.
Grava o perfil corrigido atomicamente em
/etc/seccomp/docker-block-af-alg.json (arquivo temporário + rename). Ignorado se
o conteúdo no disco já for idêntico.
Atualiza /etc/docker/daemon.json para definir "seccomp-profile" como o
caminho do perfil corrigido. O arquivo original é copiado para daemon.json.bak
na primeira modificação. Ignorado se já estiver configurado corretamente.
Recarrega o dockerd via systemctl reload docker (SIGHUP — sem necessidade de reinicialização).
Ignorado se nenhum dos arquivos foi alterado.
Verifica se o bloqueio está ativo executando uma sonda dentro de um contêiner, independentemente de quaisquer alterações feitas nas etapas acima.
O script é idempotente: executá-lo várias vezes produz o mesmo resultado e só recarrega o Docker quando algo realmente mudou.
Nota: Contêineres
--privilegedignoram todos os perfis seccomp, independentemente desta configuração. Audite seus arquivos Compose e comandos de execução para contêineres privilegiados separadamente.
docker no PATHcurl no PATH (apenas fallback)systemctl (host systemd)/etc/seccomp e /etc/docker, e para
systemctl reload docker# Aplicar a mitigação e verificar (uso normal)
sudo python3 harden-docker-seccomp.py
# Mostrar o que mudaria sem gravar nada ou recarregar o Docker
sudo python3 harden-docker-seccomp.py --dry-run
# Re-executar apenas a verificação do contêiner (sem alterações de configuração)
python3 harden-docker-seccomp.py --verify-only
# Saída detalhada
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 | Sucesso — a mitigação está ativa |
1 | Script não executado como root (ao corrigir), ou erro irrecuperável |
2 | Falha na verificação — AF_ALG não está bloqueado |
Os pods do Kubernetes compartilham o kernel do host, portanto, o mesmo primitivo de socket AF_ALG
é acessível a partir de qualquer pod em um nó afetado. O seccomp RuntimeDefault não é
suficiente — clusters testados mostraram que pods admitidos sob PSS Restricted
ainda podiam abrir sockets AF_ALG. Um perfil Localhost com uma regra de negação explícita
é necessário.
A mitigação requer duas coisas: o JSON do perfil presente no sistema de arquivos de cada nó, e cada spec de pod referenciando-o. As seções abaixo cobrem ambos, incluindo como injetar o perfil globalmente sem modificar specs de pods individuais.
O kubelet resolve perfis seccomp Localhost em relação à sua raiz seccomp,
que por padrão é /var/lib/kubelet/seccomp. O perfil deve existir nesse
caminho em cada nó que possa agendar uma carga de trabalho.
Aplique o ConfigMap e o DaemonSet deste repositório:
kubectl apply -f templates/configmap-seccomp-profile.yaml
kubectl apply -f templates/daemonset-distribute-profile.yaml
O DaemonSet executa um contêiner init que copia o perfil do ConfigMap para a raiz seccomp do kubelet do nó e, em seguida, estaciona um contêiner pause mínimo para que o pod permaneça visível para monitoramento de saúde. Ele tolera todos os taints para que também seja executado em nós do plano de controle.
Raiz seccomp do kubelet não padrão: RKE2 usa
/var/lib/rancher/rke2/agent/kubelet/seccomp. Substitua o caminho definindoNODE_SECCOMP_ROOTno env do contêiner init do DaemonSet antes de aplicar.
Verifique se o arquivo está presente em um nó:
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
Em vez de modificar specs de pods individuais ou Helm charts, use um
webhook de admissão mutante para injetar seccompProfile automaticamente no momento da admissão.
Duas opções são fornecidas: Kyverno e OPA Gatekeeper.
Ambas as abordagens só injetam o perfil quando um pod ainda não declara um, portanto, pods com perfis explícitos são deixados intactos.
Importante: Pods existentes em execução não são mutados retroativamente. Após aplicar a política, reinicie seus deployments para aplicar o perfil injetado:
kubectl rollout restart deployment -A
Este template usa a API MutatingPolicy (policies.kyverno.io/v1),
que atingiu GA no Kyverno 1.17. A API legada ClusterPolicy
(kyverno.io/v1) foi descontinuada no Kyverno 1.17 (janeiro de 2026) e está
planejada para remoção no 1.20 (outubro de 2026); não a use para novas políticas.
A expressão CEL matchConditions verifica se seccompProfile está ausente
antes de mutar, portanto, pods que já declaram um perfil são deixados intactos.
# Instalar Kyverno (se ainda não estiver presente)
helm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update
helm upgrade --install kyverno kyverno/kyverno -n kyverno --create-namespace
# Aplicar a política
kubectl apply -f templates/kyverno-mutate-seccomp.yaml
Verifique se um novo pod recebe o perfil injetado:
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.
O CRD de mutação Assign do Gatekeeper usa uma condição pathTests para injetar o
perfil apenas quando spec.securityContext.seccompProfile ainda não existe.
A mutação é estável desde o Gatekeeper 3.10+; nenhum feature flag é necessário.
# Instalar Gatekeeper (se ainda não estiver 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 a mutação
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: A abordagem
Assignopera no nível do campo e requer que o webhook de mutação do Gatekeeper esteja habilitado. OMutatingPolicydo Kyverno com ummatchConditionCEL lida com o condicional inline. Qualquer um alcança o mesmo resultado — prefira o que já estiver implantado em seu cluster.
Pods com hostPID: true, hostNetwork: true ou
securityContext.privileged: true têm acesso elevado que o seccomp sozinho não
contém completamente. Audite essas cargas de trabalho separadamente e remova privilégios quando
possível.
ConfigMap como fonte da verdade. O JSON do perfil reside em
configmap-seccomp-profile.yaml em vez de ser embutido no DaemonSet
ou duplicado entre arquivos. O DaemonSet o monta e o copia para o nó.
Atualizar o perfil significa editar um ConfigMap e reiniciar os pods do DaemonSet
— nenhum outro arquivo muda.
O DaemonSet usa prioridade system-node-critical. Isso garante que o
pod de distribuição não seja despejado antes que as cargas de trabalho que ele protege sejam agendadas,
o que deixaria nós com um arquivo de perfil ausente e pods travados em
CreateContainerError.
O Gatekeeper exclui kube-system e gatekeeper-system. Injetar um
perfil Localhost em pods do sistema que podem ser anteriores à instalação do DaemonSet
arrisca uma referência de perfil quebrada se o arquivo ainda não estiver presente no
nó. A política do Kyverno não precisa dessa exclusão porque o Kyverno
lida com a ordenação de webhooks de forma mais graciosa, mas regras exclude podem ser adicionadas
lá também, se necessário.
Injeção condicional, não substituição. Tanto o matchCondition CEL do MutatingPolicy
do Kyverno quanto o teste de caminho MustNotExist do Gatekeeper significam que a
política de admissão só age quando um pod não tem seccompProfile existente.
Cargas de trabalho que já declaram seu próprio perfil — incluindo aquelas que
legitimamente precisam de AF_ALG via uma allowlist personalizada — são deixadas intactas.