Da Capacidades ao AppArmor: O Que Realmente Impede um Atacante em um Container?

Da Capacidades ao AppArmor: O Que Realmente Impede um Atacante em um Container?

Este artigo explora as camadas de segurança em containers Linux, como capabilities, seccomp e AppArmor, e como elas trabalham em conjunto para proteger aplicações. Descubra como cada camada contribui para a defesa e quais são suas limitações.

MundiX News·26 de maio de 2026·9 min de leitura·👁 1 views

Imagine um container padrão com uma aplicação web. Ele contém uma vulnerabilidade, e um invasor consegue executar comandos – e então começa a parte interessante: o que exatamente o deterá?

Não em teoria, mas na prática. Não em um exemplo isolado de "como seccomp funciona", "o que são capabilities" e "por que AppArmor é necessário", mas na mesma carga de trabalho, onde todos esses mecanismos se encontram simultaneamente. No último ano, escrevi muito sobre os primitivos de segurança do Linux e como eles estão relacionados ao Kubernetes. Com muita frequência, sobre como aplicar restrições por meio do securityContext e, em seguida, fortalecê-las com ferramentas como Kyverno e KubeArmor, para que tudo isso possa ser usado não apenas em exemplos de laboratório, mas também em um ambiente real. Mas a configuração do mecanismo em si não responde à questão principal: qual ameaça ele realmente encerra e onde terminam suas capacidades?

No Linux, e, portanto, no Kubernetes, existem três mecanismos nos quais vale a pena se concentrar aqui: capabilities; seccomp; LSM – por exemplo, AppArmor ou SELinux. Normalmente, eles são explicados separadamente. Às vezes, eles também são implementados separadamente. É aqui que as falsas expectativas costumam aparecer: parece que se você ativar um mecanismo, ele "fechará a segurança do container". Na prática, é mais complicado. Capabilities definem o que um processo pode fazer em princípio. seccomp restringe como ele interage com o kernel por meio de chamadas de sistema. As políticas LSM avaliam o próprio comportamento no contexto de uma determinada carga de trabalho. Estes são diferentes níveis, diferentes modelos de controle e diferentes compromissos. Se você tentar forçar um deles a resolver tudo de uma vez, poderá quebrar o aplicativo ou deixar buracos óbvios. Portanto, é mais útil olhar para eles não como configurações intercambiáveis, mas como várias camadas de proteção. Cada camada fecha sua parte do ataque. Separadamente, eles podem ser contornados ou dispensados muito amplamente. Juntos, eles começam a parecer não um conjunto de recursos díspares, mas uma fronteira real para um processo comprometido. Neste artigo, pegarei uma carga de trabalho simples e mostrarei como capabilities, seccomp e AppArmor a afetam consistentemente: onde cada mecanismo ajuda, onde é impotente e por que faz sentido usá-los juntos. Esta não é uma classificação nem uma tentativa de escolher a "melhor" ferramenta. É uma tentativa de entender como construir uma proteção funcional a partir delas.

Reconsiderando

Capabilities são normalmente consideradas separadamente, seccomp – separadamente, LSM – também separadamente. É conveniente para estudar, mas não se supõe que eles sejam usados dessa forma.

Em uma carga de trabalho real, você não implanta "apenas seccomp" em isolamento. Você não confia apenas em capabilities. E LSMs não fecham tudo magicamente. Na prática, todos esses mecanismos funcionam no mesmo lugar:

  • o mesmo container
  • o mesmo processo
  • o mesmo caminho de execução

Ao mesmo tempo, eles não se sobrepõem totalmente e claramente não resolvem a mesma tarefa. Portanto, em vez da pergunta "o que cada um deles faz?", é mais útil perguntar onde cada mecanismo é usado e qual parte do problema ele tenta controlar.

Se você olhar para isso dessa forma, um padrão começa a emergir. Estes não são mecanismos intercambiáveis. Eles se sobrepõem e formam o comportamento da carga de trabalho. Um afeta o que ela pode fazer, o outro – como ela interage com o sistema, e o terceiro – quais ações são realmente permitidas.

E o que é ainda mais importante, cada um deles deixa lacunas em lugares diferentes. É por isso que a análise separada rapidamente atinge o teto. A coisa mais interessante começa quando você vê como eles se alinham. Então eles começam a parecer não como recursos separados, mas como partes de uma fronteira comum.

Diagrama

Se você representar isso esquematicamente, não obterá um conjunto de mecanismos separados, mas uma sequência. Cada nível avalia a mesma ação à sua maneira.

Estes são os níveis que avaliam a mesma ação de maneiras diferentes.

  • Capabilities determinam o que é possível em princípio.
  • seccomp controla como o sistema é usado.
  • LSMs decidem qual comportamento é realmente permitido.

Eles não se substituem. Eles existem precisamente porque os outros níveis não são suficientes.

Começando com uma carga de trabalho comprometida

Para maior clareza, vamos pegar um exemplo simples.

Suponha que tenhamos um container executando um aplicativo web básico. Nada de especial: um pequeno serviço que aceita solicitações e processa dados de entrada. Um exemplo de aplicativo pode ser encontrado no github.

Para este exemplo, deixamos tudo simples intencionalmente: sem iniciar de um usuário não privilegiado, sem proteção adicional, apenas com foco nesses três mecanismos.

O aplicativo tem uma vulnerabilidade, e o invasor consegue executar comandos dentro do container. Neste momento, a carga de trabalho faz exatamente o que foi criada para fazer, apenas não da maneira que pretendíamos.

E então surge a pergunta: o que realmente impede esse comportamento?

Capabilities: um fosso com água

Em nosso container comprometido, o invasor pode executar comandos dentro da carga de trabalho. Nesta fase, o container não faz nada "especial".

Capabilities no nível superior determinam o que esse processo pode fazer em geral. Mesmo por padrão, os containers não são iniciados com o conjunto completo de privilégios. Docker e Kubernetes já descartam um grande número de capabilities e deixam apenas um conjunto reduzido.

Este é o fosso com água.

Agora, vamos voltar ao invasor. Ele ainda pode executar comandos, explorar o sistema de arquivos, ler os arquivos disponíveis e estabelecer conexões de rede de saída.

Vamos verificar isso.

Antes de remover capabilities

Do container comprometido, o invasor pode executar comandos:

bash
python3 -c 'import socket; socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP); print("raw socket created")'
# unshare -r -n bash
unshare -r -n bash
# cat /etc/shadow
cat /etc/shadow
root:*:20339:0:99999:7:::
daemon:*:20339:0:99999:7:::
bin:*:20339:0:99999:7:::
sys:*:20339:0:99999:7:::
sync:*:20339:0:99999:7:::

Depois de remover capabilities

Depois de remover capabilities como CAP_NET_RAW, algumas ações se tornam impossíveis. Aqui está o manifesto de Deployment atualizado:

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: flask-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: flask-app
  template:
    metadata:
      labels:
        app: flask-app
    spec:
      containers:
        - name: flask
          image: sfmatt/flask-vuln-demo
          ports:
            - containerPort: 5000
          securityContext:
            capabilities:
              drop:
                - NET_RAW

Após aplicar o novo manifesto e reiniciar o Deployment:

bash
# python3 -c 'import socket; socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP); print("raw socket created")'
python3 -c 'import socket; socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP); print("raw socket created")'
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/usr/local/lib/python3.11/socket.py", line 232, in __init__
    _socket.socket.__init__(self, family, type, proto, fileno)
PermissionError: [Errno 1] Operation not permitted
# unshare -r -n bash
unshare -r -n bash
# cat /etc/shadow
cat /etc/shadow
root:*:20339:0:99999:7:::
daemon:*:20339:0:99999:7:::
bin:*:20339:0:99999:7:::
sys:*:20339:0:99999:7:::
sync:*:20339:0:99999:7:::

Agora, a criação de um raw-socket está bloqueada, e os outros comandos ainda funcionam. Raw-sockets permitem a interação de baixo nível com a pilha de rede. Para um invasor, isso pode ser útil, mas um aplicativo web normal, via de regra, não precisa disso.

Capabilities reduzem o conjunto do que é possível em princípio. Eles removem categorias inteiras de ações, mas não controlam como o comportamento restante é executado. Isso pode ser visto pelos últimos comandos, que ainda são executados com sucesso.

seccomp: a parede

Capabilities definem o que é possível. seccomp controla como o sistema é usado. Mesmo após a restrição de capabilities, cada ação dentro do container ainda passa pelo kernel. seccomp filtra essas interações no nível das chamadas de sistema.

Esta é a parede.

Para seccomp, não importa o que o processo está tentando fazer. Só importa como ele faz isso. Portanto, mesmo comandos que parecem permissíveis podem falhar se dependerem de chamadas de sistema proibidas.

Nesta fase, ainda existem dois caminhos disponíveis:

bash
# unshare -r -n bash
unshare -r -n bash
# cat /etc/shadow
cat /etc/shadow
root:*:20339:0:99999:7:::
daemon:*:20339:0:99999:7:::
bin:*:20339:0:99999:7:::
sys:*:20339:0:99999:7:::
sync:*:20339:0:99999:7:::

Agora vamos aplicar seccomp.

Após aplicar seccomp

Após aplicar seccomp, algumas chamadas de sistema e cenários de uso se tornam indisponíveis. Aqui está o manifesto de Deployment atualizado:

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: flask-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: flask-app
  template:
    metadata:
      labels:
        app: flask-app
    spec:
      containers:
        - name: flask
          image: sfmatt/flask-vuln-demo
          ports:
            - containerPort: 5000
          securityContext:
            capabilities:
              drop:
                - NET_RAW
            seccompProfile:
              type: RuntimeDefault   # inclui a filtragem de chamadas de sistema

E após aplicar o novo manifesto:

bash
# unshare -r -n bash
unshare -r -n bash
unshare: unshare failed: Operation not permitted
# cat /etc/shadow
cat /etc/shadow
root:*:20339:0:99999:7:::
daemon:*:20339:0:99999:7:::
bin:*:20339:0:99999:7:::
sys:*:20339:0:99999:7:::
sync:*:20339:0:99999:7:::

Agora unshare está bloqueado. Por que exatamente este exemplo? unshare cria novos namespaces e depende de chamadas de sistema do kernel que seccomp restringe.

Capabilities não cobriram esse caminho, mas seccomp cobriu. No entanto, o comando restante ainda funciona. Comandos que dependem de chamadas de sistema permitidas ainda serão executados com sucesso.

LSM: a guarda

LSMs definem qual comportamento é realmente permitido. Mesmo após a restrição de capabilities e a filtragem de chamadas de sistema, o container ainda pode executar um amplo conjunto de ações "permitidas". Do ponto de vista do kernel, essas ações são perfeitamente normais.

E é aqui que nossos bons e velhos LSMs entram em jogo. AppArmor ou SELinux avaliam o comportamento levando em consideração o contexto. Sua pergunta é diferente: não o que o processo pode fazer e não de que maneira, mas se essa ação é permitida para uma determinada carga de trabalho.

Esta é a guarda.

Neste nível, não restringimos mais o acesso ao sistema como tal. Definimos à força o que é permitido para esta carga de trabalho.

Nesta fase, resta um caminho:

bash
# cat /etc/shadow
cat /etc/shadow
root:*:20339:0:99999:7:::
daemon:*:20339:0:99999:7:::
bin:*:20339:0:99999:7:::
sys:*:20339:0:99999:7:::
sync:*:20339:0:99999:7:::

Agora, vamos voltar ao invasor. Ele ainda pode executar comandos e interagir com o sistema por meio de chamadas de sistema permitidas, mas agora essas ações são avaliadas de acordo com a política de segurança.

Após aplicar LSM

Após aplicar a política LSM, você pode bloquear até mesmo as interações com o sistema que parecem permissíveis por si só. Neste exemplo, usaremos AppArmor diretamente.

Primeiro, criamos um perfil AppArmor no nó:

#include <tunables/global>

profile flask-deny-shadow flags=(attach_disconnected,mediate_deleted) {
  #include <abstractions/base>

  file,
  network,
  capability,

  deny /etc/shadow r,
}

Carregamos o perfil no nó:

bash
sudo apparmor_parser -r flask-deny-shadow

Em seguida, vinculamos o perfil à carga de trabalho usando appArmorProfile:

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: flask-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: flask-app
  template:
    metadata:
      labels:
        app: flask-app
    spec:
      containers:
        - name: flask
          image: sfmatt/flask-vuln-demo
          ports:
            - containerPort: 5000
          securityContext:
            capabilities:
              drop:
                - NET_RAW
            seccompProfile:
              type: RuntimeDefault
            appArmorProfile:
              type: Localhost
              localhostProfile: flask-deny-shadow

Após aplicar o manifesto e reiniciar o Deployment:

bash
root@flask-app-687986cfdf-cschv:/app# cat /etc/shadow
cat: /etc/shadow: Permission denied

Nesta fase, o comando falha não porque o processo não tem capabilities, e não porque uma chamada de sistema foi bloqueada. Ele falha porque esse comportamento viola o perfil AppArmor especificado para a carga de trabalho.

E agora temos três caminhos sobrepostos. Cada nível fechou seu caminho para o ataque.

Para verificar o quão confiante você está na segurança do K8s, faça um teste de entrada. Isso ajudará você a ver rapidamente seus pontos fortes e encontrar as lacunas que precisam ser fechadas.

Visualização dos níveis

Cada nível remove um caminho separado. Capabilities bloqueiam a criação de raw-sockets, seccomp bloqueia a criação de namespaces e AppArmor bloqueia o acesso a um arquivo com dados confidenciais.

Vamos resumir

A ordem não é o principal aqui. Você pode construir esses níveis em uma ordem diferente e ainda chegar ao mesmo resultado. Então, se quiser, pode começar pela parede.

O importante é outro: todos os três níveis devem estar presentes.

  • Capabilities removem categorias inteiras de ações.
  • seccomp restringe como o sistema é usado.
  • LSMs definem qual comportamento é permitido.

Sozinhos, nenhum desses mecanismos resolve o problema completamente. Juntos, eles funcionam muito bem. E, espero, não voltarei a este trio por algum tempo.

A questão da segurança de containers rapidamente atinge a prática: quais configurações realmente restringem o atacante, onde permanecem os pontos fracos e como isso é transferido para um cluster Kubernetes.

Em breve, a OTUS realizará duas aulas gratuitas sobre segurança K8s. Nelas, você poderá conhecer especialistas, fazer perguntas sobre seus casos e, ao mesmo tempo, entender como o formato de treinamento se adapta às suas tarefas.

  • 28 de maio às 20:00. "Segurança K8s: conceitos básicos e problemas comuns" Vamos analisar os mecanismos básicos de segurança do Kubernetes e as áreas que devem ser verificadas primeiro. Inscreva-se
  • 18 de junho às 20:00. "Kubernetes sob mira: por que até mesmo um estagiário pode hackear seu cluster e como evitar isso" Vamos falar sobre vetores de ataque típicos, histórias de hacking e cenários em que o acesso a um container se torna um problema para todo o cluster. Inscreva-se

A lista completa de aulas gratuitas de maio pode ser encontrada no resumo.

Tags:

  • segurança Kubernetes
  • segurança de containers
  • container security
  • capabilities
  • seccomp
  • LSM
  • AppArmor
  • securityContext
  • proteção de cluster

📤 Compartilhar & Baixar