Dominando Segurança em Docker: Rootless Containers, Seccomp e AppArmor em Projetos Reais Já leu

Introdução: O Cenário Atual de Segurança em Containers Docker revolucionou a forma como desenvolvemos e deployamos aplicações, mas trouxe consigo responsabilidades significativas de segurança. Quando você executa um container, está essencialmente executando processos isolados no kernel do host, compartilhando recursos críticos. Por padrão, containers rodam como root, o que amplifica consideravelmente os riscos de segurança. Uma vulnerabilidade em uma aplicação containerizada pode comprometer não apenas o container, mas potencialmente o host inteiro. Neste artigo, vamos explorar três camadas essenciais de segurança em Docker que, quando implementadas corretamente, reduzem drasticamente a superfície de ataque. Você compreenderá não apenas como implementar essas tecnologias, mas por que cada uma delas é crítica para uma estratégia de segurança robusta em ambientes containerizados. Rootless Containers: Executando Docker Sem Privilégios de Root O Problema do Root em Containers Quando você executa , por padrão, você obtém um shell com privilégios UID 0 (root) dentro do container. Se um atacante conseguir escapar do isolamento do container, ele

Introdução: O Cenário Atual de Segurança em Containers

Docker revolucionou a forma como desenvolvemos e deployamos aplicações, mas trouxe consigo responsabilidades significativas de segurança. Quando você executa um container, está essencialmente executando processos isolados no kernel do host, compartilhando recursos críticos. Por padrão, containers rodam como root, o que amplifica consideravelmente os riscos de segurança. Uma vulnerabilidade em uma aplicação containerizada pode comprometer não apenas o container, mas potencialmente o host inteiro.

Neste artigo, vamos explorar três camadas essenciais de segurança em Docker que, quando implementadas corretamente, reduzem drasticamente a superfície de ataque. Você compreenderá não apenas como implementar essas tecnologias, mas por que cada uma delas é crítica para uma estratégia de segurança robusta em ambientes containerizados.

Rootless Containers: Executando Docker Sem Privilégios de Root

O Problema do Root em Containers

Quando você executa docker run -it ubuntu bash, por padrão, você obtém um shell com privilégios UID 0 (root) dentro do container. Se um atacante conseguir escapar do isolamento do container, ele terá acesso ao sistema com privilégios elevados. O Rootless Docker é um modo de execução onde o daemon Docker e os containers rodam como um usuário não-root no host, eliminando completamente esse vetor de ataque.

A diferença é fundamental: em um container rootless, mesmo que um atacante escape do isolamento, ele será um usuário comum, não root. Isso é segurança em profundidade — você não confia que o isolamento do container é perfeito, então adiciona uma camada extra de proteção.

Instalação e Configuração do Rootless Docker

Primeiro, você precisa remover a instalação padrão do Docker e instalar o rootless. Aqui está o processo em um sistema Ubuntu:

# 1. Remover Docker padrão (se instalado)
sudo apt-get remove docker docker-engine docker.io containerd runc

# 2. Instalar dependências necessárias
sudo apt-get install -y uidmap dbus-user-session

# 3. Download e instalação do Docker Rootless
curl -fsSL https://get.docker.com/rootless | sh

# 4. Configurar o ambiente do usuário
export PATH=/home/$USER/bin:$PATH
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock

# 5. Ativar o systemd user service para iniciar automaticamente
systemctl --user enable docker
systemctl --user start docker

# 6. Verificar se está funcionando
docker ps

Verificando o Comportamento do Rootless

Vamos criar um teste prático para demonstrar como o rootless muda o comportamento do UID:

# Com Docker padrão (rootful), você veria UID 0:
# docker run alpine id
# uid=0(root) gid=0(root) groups=0(root)

# Com Docker rootless, mesmo sem especificar usuário, você vê mapeamento:
docker run alpine id
# uid=0(root) gid=0(root) groups=0(root)  # Dentro do container (mapeado)

# Mas no host:
ps aux | grep docker-containerd
# Mostrará processos rodando como seu usuário regular, não root

A mágica acontece através do user namespace mapping. O UID 0 dentro do container é mapeado para um UID diferente (geralmente na faixa 100000+) no host. Se você executar um comando malicioso que escape e tentar fazer algo como rm -rf /, ele será ejecutado como um usuário sem privilégios no host.

# Dockerfile exemplo com usuário explícito
FROM alpine:latest

RUN addgroup -g 1000 appuser && \
    adduser -D -u 1000 -G appuser appuser

USER appuser

WORKDIR /app
COPY . .

CMD ["sh", "-c", "id"]
# Ao executar:
docker build -t myapp .
docker run myapp
# uid=1000(appuser) gid=1000(appuser) groups=1000(appuser)

Limitações do Rootless

O rootless Docker tem algumas limitações que você precisa estar ciente. Network privilegiado (portas abaixo de 1024) requer configuração extra, pois o usuário não pode vincular a portas privilegiadas. Drivers de volume e rede têm comportamento ligeiramente diferente. Além disso, nem todos os drivers de storage são suportados igualmente. Para a maioria das aplicações modernas, essas limitações são aceitáveis, mas em ambientes legados pode haver conflitos.

Seccomp: Restringindo Chamadas de Sistema

O Que É Seccomp e Por Que Importa

Seccomp (Secure Computing) é um mecanismo do kernel Linux que limita as syscalls (chamadas de sistema) que um processo pode fazer. Imagine que sua aplicação Node.js precisa apenas de syscalls de leitura, escrita e rede. Por que permitir que ela execute syscalls para criar dispositivos, carregar módulos de kernel ou trocar de usuário? Seccomp funciona como um firewall para syscalls.

Docker vem com um perfil Seccomp padrão que já bloqueia centenas de syscalls perigosas. Mas você pode e deve criar perfis customizados para suas aplicações específicas. Quanto mais restritivo, menor a superfície de ataque.

Perfil Seccomp Padrão do Docker

# Visualizar o perfil padrão que Docker usa
docker run --rm alpine cat /proc/self/status | grep Seccomp

# Executar sem Seccomp (MUITO inseguro, apenas para testes)
docker run --security-opt seccomp=unconfined alpine cat /proc/self/status

Criando um Perfil Seccomp Customizado

Aqui está um perfil Seccomp customizado para uma aplicação web simples:

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "defaultErrnoRet": 1,
  "archMap": [
    {
      "architecture": "SCMP_ARCH_X86_64",
      "subArchitectures": [
        "SCMP_ARCH_X86",
        "SCMP_ARCH_X32"
      ]
    }
  ],
  "syscalls": [
    {
      "names": [
        "accept4",
        "arch_specific_syscall",
        "bind",
        "brk",
        "clone",
        "close",
        "connect",
        "dup",
        "dup2",
        "dup3",
        "epoll_create1",
        "epoll_ctl",
        "epoll_wait",
        "exit",
        "exit_group",
        "fcntl",
        "fstat",
        "fstatfs",
        "futex",
        "getcwd",
        "getpeername",
        "getpid",
        "getrandom",
        "getrlimit",
        "getsockname",
        "getsockopt",
        "gettimeofday",
        "listen",
        "lseek",
        "madvise",
        "mmap",
        "mprotect",
        "mremap",
        "munmap",
        "nanosleep",
        "open",
        "openat",
        "pipe",
        "pipe2",
        "poll",
        "pread64",
        "prlimit64",
        "proctitle",
        "pselect6",
        "read",
        "readv",
        "recvfrom",
        "recvmsg",
        "restart_syscall",
        "rt_sigaction",
        "rt_sigprocmask",
        "sched_getaffinity",
        "sched_yield",
        "seccomp",
        "select",
        "sendmsg",
        "sendto",
        "set_robust_list",
        "set_tid_address",
        "setitimer",
        "setsockopt",
        "sigaction",
        "sigaltstack",
        "sigpending",
        "sigprocmask",
        "sigsuspend",
        "socket",
        "socketpair",
        "stat",
        "statfs",
        "statx",
        "tgkill",
        "time",
        "timer_create",
        "timer_delete",
        "timer_gettime",
        "timer_settime",
        "timerfd_create",
        "timerfd_gettime",
        "timerfd_settime",
        "tkill",
        "uname",
        "write",
        "writev"
      ],
      "action": "SCMP_ACT_ALLOW"
    },
    {
      "names": [
        "ptrace"
      ],
      "action": "SCMP_ACT_ALLOW",
      "includes": {
        "minKernel": "4.8"
      }
    }
  ]
}

Salve este arquivo como seccomp-profile.json. A estrutura é simples: defaultAction define o comportamento padrão (neste caso, bloquear com erro), e o array syscalls lista quais chamadas são permitidas.

Aplicando Seccomp em um Container

# Executar com perfil customizado
docker run --security-opt seccomp=seccomp-profile.json alpine id

# Executar com Seccomp desativado (inseguro!)
docker run --security-opt seccomp=unconfined alpine id

# Verificar quais syscalls foram bloqueadas
docker run --security-opt seccomp=seccomp-profile.json alpine strace -e trace=open ls
# Você verá ENOSYS (Function not implemented) para syscalls bloqueadas

Exemplo Prático: Ajustando Perfil para Node.js

Se você tem uma aplicação Node.js simples que apenas faz requisições HTTP, pode ser ainda mais restritivo:

# Inicie um container e capture as syscalls usadas
docker run --security-opt seccomp=unconfined node:alpine node -e "console.log('Hello')"

# Analise com ferramentas como syscalltracer para criar um perfil mínimo
# Remova todas as syscalls desnecessárias do seu perfil JSON

AppArmor: Controle Obrigatório de Acesso

O Que É AppArmor

AppArmor é um sistema de controle obrigatório de acesso (Mandatory Access Control) que restringe o que um programa pode fazer baseado em um perfil. Enquanto Seccomp restringe syscalls, AppArmor restringe quais arquivos um processo pode acessar, quais redes pode usar, quais capacidades de kernel pode ter. É outra camada de defesa em profundidade.

Docker vem com um perfil AppArmor padrão chamado docker-default. Você pode criar perfis customizados para aplicações específicas que exigem proteção extra.

Verificando AppArmor no Sistema

# Verificar se AppArmor está instalado e ativo
sudo systemctl status apparmor

# Listar perfis AppArmor carregados
sudo aa-status

# Procurar por perfis Docker
sudo aa-status | grep docker

Perfil AppArmor Padrão do Docker

# O perfil padrão está aqui
cat /etc/apparmor.d/docker-default

O perfil padrão do Docker permite acesso a muitos caminhos. Para uma aplicação específica, você quer ser mais restritivo.

Criando um Perfil AppArmor Customizado

Vamos criar um perfil para uma aplicação que apenas lê dados de /app e escreve em /tmp:

#include <tunables/global>

profile app-profile flags=(attach_disconnected,mediate_deleted) {
  #include <abstractions/base>
  #include <abstractions/nameservice>
  #include <abstractions/openssl>

  # Permitir ler binários e bibliotecas do sistema
  /lib/** mr,
  /usr/lib/** mr,
  /usr/bin/** mr,
  /bin/** mr,

  # Permitir leitura de dados da aplicação
  /app/** r,

  # Permitir escrita em diretório temporário
  /tmp/** rw,

  # Permitir comunicação de rede
  network inet stream,
  network inet dgram,

  # Permitir sinais básicos
  signal (send) type=term peer=unconfined,
  signal (receive) type=term,

  # Bloquear acesso perigoso ao sistema de arquivos
  deny /sys/** rwk,
  deny /proc/** rwk,
  deny /root/** rwk,
  deny /home/** rwk,

  # Negar syscalls de kernel module
  deny /sys/module/** rw,
}

Salve como /etc/apparmor.d/app-profile.

Carregando e Testando o Perfil

# Validar a sintaxe do perfil
sudo apparmor_parser -r /etc/apparmor.d/app-profile

# Colocar em modo de log (aprendizado) primeiro
sudo aa-complain /etc/apparmor.d/app-profile

# Ver denials no log
sudo tail -f /var/log/audit/audit.log | grep apparmor

# Depois de ajustado, colocar em modo enforce
sudo aa-enforce /etc/apparmor.d/app-profile

# Usar o perfil no Docker
docker run --security-opt apparmor=app-profile alpine ls /app

Integração de AppArmor com Dockerfile

FROM alpine:latest

# Criar estrutura de diretórios esperada pelo perfil
RUN mkdir -p /app /tmp

# Copiar aplicação
COPY app.sh /app/

# Dar permissões
RUN chmod +x /app/app.sh

# Usuário não-root
RUN addgroup -g 1000 appuser && \
    adduser -D -u 1000 -G appuser appuser

USER appuser

WORKDIR /app

CMD ["./app.sh"]
# Build e execução com AppArmor
docker build -t secure-app .
docker run --security-opt apparmor=app-profile secure-app

Combinando as Três Camadas: Uma Estratégia Completa

Segurança real vem de múltiplas camadas. Um perfil Seccomp bem configurado pode bloquear uma syscall perigosa. Um AppArmor bem configurado pode bloquear o acesso a arquivos críticos. E o Rootless Docker garante que, mesmo se ambos falharem, o atacante não terá privilégios elevados.

# docker-compose.yml com todas as camadas
version: '3.8'
services:
  webapp:
    image: myapp:latest
    security_opt:
      - no-new-privileges:true
      - seccomp=seccomp-profile.json
      - apparmor=app-profile
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE
    read_only: true
    tmpfs:
      - /tmp
      - /run
    networks:
      - isolated
    restart: on-failure

networks:
  isolated:
    driver: bridge

Este docker-compose demonstra como combinar:
- no-new-privileges: Previne que o processo ganhe privilégios
- Seccomp: Restringe syscalls
- AppArmor: Restringe acesso a arquivos e redes
- cap_drop/add: Controla capabilities do Linux
- read_only: Sistema de arquivos somente leitura
- tmpfs: Espaço temporário isolado

Conclusão

Ao longo deste artigo, você aprendeu três tecnologias complementares que formam a base de uma estratégia robusta de segurança em Docker. Rootless Containers eliminam o risco de escalonamento de privilégios ao host, garantindo que mesmo em caso de escape do isolamento, o atacante tenha apenas permissões de usuário comum. Seccomp funciona como um firewall de baixo nível, bloqueando chamadas de sistema perigosas antes que possam ser executadas, reduzindo drasticamente a superfície de ataque do kernel. AppArmor implementa controle obrigatório de acesso, garantindo que processos só possam acessar os recursos especificamente necessários para sua operação, bloqueando tanto acesso ao sistema de arquivos quanto operações de rede não autorizadas. A verdadeira segurança em containers não vem de depender em uma única tecnologia, mas de aplicar essas camadas em conjunto — defesa em profundidade que aumenta exponencialmente a dificuldade de um ataque bem-sucedido.

Referências


Artigos relacionados