DevOps
Docker
Kubernetes
LINUX
Segurança
capabilities Linux, container hardening, Cosign, Docker security, hostPath, imagem Docker assinada, Kubernetes security, NetworkPolicy, Pod Security, RBAC no Kubernetes, readOnlyRootFilesystem, runAsNonRoot, seccomp, supply chain security, Trivy
0 Comentários
Segurança de Containers em Docker e Kubernetes: hardening, isolamento e automação prática
Sumário
- O ponto de partida: ameaça real, não teoria
- Imagens pequenas, previsíveis e sem lixo operacional
- Dockerfile orientado a hardening
- Execução sem root e redução de capacidades Linux
- Docker run com política mínima
- Kubernetes: securityContext de verdade
- Segredos não pertencem à imagem nem ao Git
- Docker e Compose: não embutir credenciais
- Kubernetes Secret com montagem como arquivo
- Assinatura, provenance e scanner no pipeline
- Scanner de imagem no CI
- Assinatura com cosign
- Exemplo de pipeline em GitLab CI
- Namespace, seccomp, AppArmor e SELinux: a camada Linux que realmente segura a barra
- Seccomp como filtro de syscalls
- AppArmor e SELinux em hosts Linux
- Rede: segmentação explícita entre workloads
- Default deny de entrada e saída
- RBAC sem permissões excessivas e sem atalhos
- Desabilitar montagem automática de token
- Políticas de admissão: impedir configuração insegura antes de chegar ao runtime
- Exemplo de política Kyverno para bloquear privileged
- Host Linux endurecido: o container herda o que o nó permite
- Checagens rápidas no host
- Observabilidade e resposta: detectar container fora do padrão
- O que observar no runtime
- Checklist operacional para aplicar já no próximo rollout
A superfície de ataque de containers não termina no docker run. Ela começa no build da imagem, atravessa o runtime, entra no scheduler do kubernetes e se espalha por permissões de kernel, rede, secret management e supply chain. Em ambientes linux e devops, tratar containers como “mini-VMs” é um erro clássico: o modelo correto é assumir compartilhamento de kernel, reduzir privilégios ao mínimo e automatizar controles em cada camada do pipeline.
Uma estratégia séria de segurança de containers combina quatro frentes: imagens mínimas e verificadas, runtime com isolamento estrito, políticas de admissão e rede no cluster, e observabilidade para detectar desvio de comportamento. Abaixo, o foco vai ser operacional: comandos, manifests, hardening e decisões que funcionam em produção.
O ponto de partida: ameaça real, não teoria
Container não elimina risco; ele redistribui risco. O kernel do host continua sendo a barreira principal, então qualquer falha de configuração que aumente privilégio dentro do container vira potencial de escape lateral, persistência ou movimentação entre namespaces. Entre os vetores mais comuns estão:
- imagens com pacotes desnecessários e binários de administração;
- processos rodando como root sem necessidade;
--privileged,CAP_SYS_ADMINe volumes sensíveis expostos;- secretos embutidos na imagem ou em variáveis de ambiente vazadas;
- ausência de assinatura e verificação de integridade de imagens;
- workloads Kubernetes sem
securityContexte sem políticas de admissão.
O erro mais caro é acreditar que a segurança acontece só no cluster. Na prática, a cadeia completa precisa ser controlada: Dockerfile, registry, scanner, CI/CD, manifestos, políticas e runtime no host Linux.
Imagens pequenas, previsíveis e sem lixo operacional
O build da imagem define a maior parte da exposição. Quanto menos componentes, menos superfície de ataque e menos dependências para manter. Isso não significa escolher a menor base possível de forma cega; significa construir imagens previsíveis, com pacotes mínimos e sem ferramentas de debug embarcadas.
Dockerfile orientado a hardening
# syntax=docker/dockerfile:1.7
FROM golang:1.23-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" -o /out/app ./cmd/app
FROM alpine:3.20
RUN addgroup -S app && adduser -S app -G app
WORKDIR /app
COPY --from=build /out/app /app/app
USER app:app
EXPOSE 8080
ENTRYPOINT ["/app/app"]
Esse Dockerfile usa multi-stage build, elimina toolchain no runtime e executa sem root. Algumas melhorias adicionais fazem diferença em ambientes mais rígidos:
- usar
readOnlyRootFilesystemno Kubernetes; - montar
/tmpcomoemptyDircommedium: Memoryse a aplicação exigir escrita temporária; - remover shells se a aplicação não precisa deles;
- fixar versões por digest, não por tag mutável.
Tag como alpine:3.20 ainda permite drift se o conteúdo da tag mudar. Em produção, a referência robusta é por digest:
docker pull alpine:3.20
docker image inspect alpine:3.20 --format '{{index .RepoDigests 0}}'
Depois, use o digest no pipeline ou no manifesto do Kubernetes. Isso congela a origem exata do artefato e evita surpresa silenciosa em deploys automatizados.
Execução sem root e redução de capacidades Linux
O maior salto de segurança em containers é parar de rodar como root. Root dentro do container não é igual a root no host, mas continua perigoso o suficiente para facilitar abuso de binários, escrita em volumes e exploração de falhas no runtime ou no kernel.
Docker run com política mínima
docker run
--name app
--read-only
--cap-drop ALL
--cap-add NET_BIND_SERVICE
--security-opt no-new-privileges
--pids-limit 100
--memory 256m
--cpus 0.5
-p 8080:8080
myapp@sha256:abcdef123456...
Esse conjunto reduz a chance de escalada lateral. --read-only bloqueia persistência no filesystem do container. --cap-drop ALL remove capabilities herdadas do kernel. no-new-privileges impede ganho de privilégio via SUID/SGID e execuções subsequentes. pids-limit reduz risco de fork bomb. Limites de memória e CPU evitam ruído operacional e ajudam a mitigar abuso por carga maliciosa.
Se o serviço escuta em porta alta, nem NET_BIND_SERVICE precisa existir. Em cluster, essa decisão precisa refletir no securityContext e na porta exposta pelo Service, não em gambiarras no entrypoint.
Kubernetes: securityContext de verdade
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
replicas: 2
selector:
matchLabels:
app: app
template:
metadata:
labels:
app: app
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: myapp@sha256:abcdef123456...
ports:
- containerPort: 8080
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir:
medium: Memory
Esse manifesto já corta uma lista grande de riscos. runAsNonRoot impede execução como UID 0. allowPrivilegeEscalation: false bloqueia escalada via setuid. RuntimeDefault aplica um perfil seccomp padrão do runtime, o que corta syscalls desnecessárias. O uso de emptyDir para /tmp dá ao aplicativo um local de escrita controlado sem abrir o root filesystem.
Segredos não pertencem à imagem nem ao Git
Senha em variável de ambiente parece prática até o primeiro kubectl describe pod, dump de processo ou log de debug. O tratamento certo de segredos passa por separação clara entre artefato e configuração sensível.
Docker e Compose: não embutir credenciais
services:
app:
image: myapp@sha256:abcdef123456...
env_file:
- .env
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt
Mesmo em ambientes locais, secret no Compose é melhor do que variável solta em arquivo de ambiente commiteado por acidente. Em produção, a decisão correta é usar um mecanismo externo: Kubernetes Secret, Vault, SOPS, External Secrets Operator ou integração nativa com KMS, dependendo da maturidade do ambiente.
Kubernetes Secret com montagem como arquivo
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
type: Opaque
stringData:
username: appuser
password: supersecreto
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
template:
spec:
containers:
- name: app
image: myapp@sha256:abcdef123456...
volumeMounts:
- name: db-credentials
mountPath: /run/secrets/db
readOnly: true
volumes:
- name: db-credentials
secret:
secretName: db-credentials
defaultMode: 0400
Montar como arquivo reduz exposição acidental em logs e ajuda o aplicativo a ler credenciais sob demanda. Para aplicações que insistem em variáveis, o ideal é adaptar o código. Se isso não for possível, limite a leitura da variável ao processo principal e evite exportar o valor em shell scripts intermediários.
Assinatura, provenance e scanner no pipeline
Securing containers não se fecha sem supply chain. A imagem precisa ser verificada antes de entrar em produção. Três controles importam: análise de vulnerabilidades, assinatura criptográfica e rastreabilidade da origem do build.
Scanner de imagem no CI
trivy image --severity HIGH,CRITICAL --exit-code 1 myapp:build
O scanner precisa bloquear o pipeline quando encontrar vulnerabilidades críticas exploráveis no contexto da aplicação. Um relatório bonito sem gate de falha não muda a postura de risco. O ajuste fino depende do ambiente, mas o princípio é direto: falha de build quando o risco ultrapassa o limite definido pela equipe.
Assinatura com cosign
cosign generate-key-pair
cosign sign --key cosign.key myapp@sha256:abcdef123456...
cosign verify --key cosign.pub myapp@sha256:abcdef123456...
Assinar a imagem protege contra adulteração no registry e garante que o deploy só aceite artefatos produzidos pela pipeline autorizada. O passo seguinte é impor verificação no cluster. Em Kubernetes, isso geralmente entra via policy controller ou admission webhook. Sem enforcement, assinatura vira documentação, não controle.
Provenance também importa. Registros modernos podem armazenar metadados de build, mas o essencial é preservar ligação entre commit, build, imagem e assinatura. Em pipelines baseadas em Git, gere um artefato que registre:
- SHA do commit;
- hash da imagem;
- versão da ferramenta de build;
- dependências resolvidas;
- resultado do scanner.
Exemplo de pipeline em GitLab CI
stages:
- test
- build
- scan
- sign
docker_build:
stage: build
image: docker:27
services:
- docker:27-dind
variables:
DOCKER_TLS_CERTDIR: ""
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
image_scan:
stage: scan
image: aquasec/trivy:latest
script:
- trivy image --severity HIGH,CRITICAL --exit-code 1 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
image_sign:
stage: sign
image: cgr.dev/chainguard/cosign:latest
script:
- cosign sign --key env://COSIGN_KEY $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
O pipeline acima ainda precisa de credenciais protegidas e de um registry confiável. Não adianta assinar em um estágio e publicar a chave privada em variável sem controle. Em cenários mais rígidos, use KMS, HSM ou identidade federada para assinatura sem chave persistente em disco.
Namespace, seccomp, AppArmor e SELinux: a camada Linux que realmente segura a barra
Em Linux, a segurança do container depende do conjunto de namespaces, cgroups, LSMs e syscalls permitidas. Namespace isola visão; LSM impede ações específicas; cgroup controla consumo. Deixar essa base desligada é o mesmo que abrir mão do que o kernel oferece de melhor.
Seccomp como filtro de syscalls
Muitos workloads não precisam de dezenas de syscalls. Um perfil seccomp personalizado reduz a superfície de exploração. Exemplo simplificado:
{
"defaultAction": "SCMP_ACT_ERRNO",
"archMap": [
{ "architecture": "SCMP_ARCH_X86_64", "subArchitectures": ["SCMP_ARCH_X86", "SCMP_ARCH_X32"] }
],
"syscalls": [
{ "names": ["read", "write", "exit", "futex", "clock_gettime", "epoll_wait", "openat", "close"], "action": "SCMP_ACT_ALLOW" }
]
}
Esse arquivo precisa ser calibrado com base na aplicação real. O processo correto é instrumentar a carga, observar syscalls e depois restringir. Usar o perfil padrão do runtime já melhora muito, mas perfis customizados fecham portas adicionais.
AppArmor e SELinux em hosts Linux
Em distribuições com AppArmor, perfis por aplicação ajudam a restringir acesso a arquivos e capacidades. Em ambientes com SELinux, o contexto correto evita que um container tenha acesso indevido a arquivos do host ou volumes montados. A equipe que administra o cluster deve padronizar isso na camada do nó, não como ajuste manual por pod.
Com Docker, o uso de AppArmor aparece em flags ou perfis do daemon. No Kubernetes, a adoção depende da versão e da configuração do runtime. O ponto operacional é o mesmo: o pod precisa herdar um contexto restritivo por padrão, e a exceção deve ser formalizada por política, nunca por improviso.
Rede: segmentação explícita entre workloads
Em clusters Kubernetes, rede plana é convite a movimentação lateral. Se qualquer pod alcança qualquer serviço, a contensão de incidente fica fraca. A resposta prática é aplicar NetworkPolicies com default deny e abrir apenas o tráfego necessário.
Default deny de entrada e saída
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny
namespace: app
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
Com isso, nada entra e nada sai até regras explícitas serem adicionadas. Depois, libere apenas o que o serviço precisa:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-app-to-db
namespace: app
spec:
podSelector:
matchLabels:
app: app
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
name: data
podSelector:
matchLabels:
app: postgres
ports:
- protocol: TCP
port: 5432
Esse tipo de segmentação reduz impacto de comprometimento. Se um pod cair, ele não conversa com o cluster inteiro. Complementando isso, serviços sensíveis devem ficar em namespaces separados, com RBAC e políticas próprias. Segurança boa em Kubernetes nasce de isolamento por domínio funcional, não apenas por times.
RBAC sem permissões excessivas e sem atalhos
Outro erro recorrente é permitir que workloads consultem o API server sem necessidade. ServiceAccount padrão em muitos clusters vem com mais acesso do que deveria. A regra operacional é simples: se o pod não consulta o Kubernetes API, desabilite o uso do token e remova qualquer permissão implícita.
Desabilitar montagem automática de token
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-sa
namespace: app
automountServiceAccountToken: false
No pod ou deployment, vincule essa conta apenas se houver motivo claro. Se o aplicativo realmente precisar falar com a API, crie Role e RoleBinding minimais. Nunca use ClusterRole amplo por conveniência. A prática de “dar admin para resolver e depois ajustar” costuma sobreviver em produção por meses.
Políticas de admissão: impedir configuração insegura antes de chegar ao runtime
Após consolidar boas práticas no manifesto, o próximo passo é impedir que elas sejam quebradas por engano. Isso entra na camada de admissão. Em clusters atuais, ferramentas como Kyverno ou OPA Gatekeeper traduzem requisitos de segurança em política auditável e versionada como código.
Exemplo de política Kyverno para bloquear privileged
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: disallow-privileged
spec:
validationFailureAction: Enforce
rules:
- name: privileged-containers
match:
resources:
kinds:
- Pod
validate:
message: "Containers privilegiados são proibidos"
pattern:
spec:
containers:
- securityContext:
=(privileged): false
Uma política assim elimina classes inteiras de erro humano. O mesmo padrão vale para bloquear hostNetwork, hostPID, hostIPC, volumes hostPath e imagens sem digest. Sem enforcement, todo time acaba produzindo sua própria exceção.
Host Linux endurecido: o container herda o que o nó permite
Um nó Kubernetes mal configurado invalida boa parte da disciplina no nível do pod. O host precisa ser tratado como infraestrutura crítica. Isso inclui atualização frequente do kernel, runtime compatível, política de reboot controlada, firewall, logging e redução de serviços expostos.
Checagens rápidas no host
uname -r
systemctl status kubelet
docker info 2>/dev/null | grep -i security
sysctl net.ipv4.ip_forward
sysctl kernel.unprivileged_userns_clone
Em hosts Linux, ajuste sysctls com intenção clara. Não habilite features por padrão sem entender o efeito no isolamento. Revise acesso SSH, autenticação por chave, rotação de credenciais e grupo sudo. O nó é parte da superfície do cluster, não uma caixa preta “administrada pela plataforma”.
Se o ambiente roda em bare metal ou em VMs administradas por equipe própria, vale integrar hardening via Ansible ou outra ferramenta de IaC. Exemplo de tarefa simples para restringir sysctl:
- name: Harden kernel parameters
hosts: k8s_nodes
become: true
tasks:
- name: Set restrictive sysctl
ansible.posix.sysctl:
name: kernel.kptr_restrict
value: "2"
state: present
reload: true
Observabilidade e resposta: detectar container fora do padrão
Segurança sem monitoramento vira controle estático. Quando um container começa a abrir conexões inesperadas, gerar processos filhos demais, escrever onde não deveria ou tocar arquivos sensíveis, o sinal precisa aparecer. Ferramentas de runtime security e logs estruturados ajudam a fechar esse ciclo.
O que observar no runtime
- processos executados fora da árvore esperada;
- syscalls anormais bloqueadas por seccomp;
- conexões de saída para destinos não previstos;
- tentativas de escrita em filesystem read-only;
- uso excessivo de CPU ou criação massiva de processos;
- erros de permissão em volumes montados.
Em Kubernetes, vale integrar logs do kubelet, eventos do cluster e métricas do runtime em um stack de observabilidade. Alertas úteis são aqueles com contexto: namespace, pod, image digest, node, serviceAccount e alteração recente no deployment. Sem isso, a investigação vira caça ao tesouro.
Checklist operacional para aplicar já no próximo rollout
Uma política de segurança eficiente funciona quando se torna parte do fluxo de entrega, não quando depende de memória individual. Antes de promover um workload, valide este conjunto mínimo:
- imagem construída com multi-stage e sem toolchain no runtime;
- execução sem root;
readOnlyRootFilesystemhabilitado;- capabilities reduzidas a zero ou ao estritamente necessário;
allowPrivilegeEscalation: false;- seccomp padrão ou perfil customizado;
- secret montado como arquivo, não exposto em Git;
- imagem assinada e verificada;
- scan de vulnerabilidades com gate no CI;
- NetworkPolicy com default deny;
- RBAC mínimo e
automountServiceAccountToken: falsequando possível; - limites de CPU, memória e pids;
- policy de admissão bloqueando privilégios perigosos.
Quando isso entra como padrão, o ganho é imediato: menos ruído operacional, menos superfície de ataque e menos chance de deploy inseguro passar despercebido. Em Docker e Kubernetes, segurança boa não é um pacote adicional; é uma propriedade da pipeline inteira, do host Linux ao manifesto final. Quem automatiza esses controles enxerga o cluster como um sistema controlado. Quem deixa para revisar depois normalmente aprende em incidente.
Sou um profissional na área de Tecnologia da informação, especializado em monitoramento de ambientes, Sysadmin e na cultura DevOps. Possuo certificações de Segurança, AWS e Zabbix.


