← Voltar para o Blog

SELinux e AppArmor: quando o culpado é a proteção

A permissão está certa, o dono está certo, e a aplicação insiste em "permission denied". Existe uma terceira camada de controle que quase ninguém lembra — e desligá-la não é a resposta.

Equipe EasyOps Cloud · · 7 min de leitura

O arquivo pertence ao usuário certo. A permissão é 640 e o processo está no grupo. Você conferiu três vezes. E a aplicação continua devolvendo Permission denied.

Quando a permissão tradicional do Unix está claramente correta e o acesso ainda é negado, existe uma terceira camada agindo: o controle de acesso obrigatório. Em RHEL, AlmaLinux e Rocky, ele se chama SELinux; em Debian e Ubuntu, AppArmor.

A reação mais comum é desligá-lo. É compreensível e é a pior das saídas — essas camadas existem justamente para conter o que a permissão comum não contém, e são o que transforma uma falha de aplicação em incidente limitado.

Reconhecer que é isso

O sintoma característico: permissão correta, usuário correto, acesso negado. Antes de qualquer coisa, confirme qual sistema está ativo e em que modo.

# RHEL, AlmaLinux, Rocky
getenforce
sestatus

# Debian, Ubuntu
sudo aa-status

getenforce responde Enforcing, Permissive ou Disabled. Em Permissive, ele registra o que bloquearia mas deixa passar — o que é a chave do diagnóstico, como veremos.

A confirmação definitiva vem do log:

# SELinux
sudo ausearch -m AVC -ts recent
sudo journalctl -t setroubleshoot --since '30 min ago'

# AppArmor
sudo journalctl -k --since '30 min ago' | grep -i apparmor
sudo dmesg | grep -i 'apparmor.*DENIED'

Uma linha de negação do SELinux tem esta cara:

type=AVC msg=audit(...): avc: denied { write } for pid=2841
comm="nginx" name="cache" dev="vda1" ino=131079
scontext=system_u:system_r:httpd_t:s0
tcontext=unconfined_u:object_r:default_t:s0 tclass=dir

Traduzindo: o processo no contexto httpd_t tentou escrever em um diretório cujo contexto é default_t, e a política não permite. O problema não é permissão — é rótulo.

O modelo mental do SELinux

Todo processo e todo arquivo carregam um rótulo de contexto. A política define quais contextos podem interagir com quais. É isso.

ls -Z /var/www/meuapp/
ps -eZ | grep nginx
id -Z

O erro mais comum acontece ao mover arquivo em vez de copiar, ou ao criar diretório fora do lugar esperado. Um arquivo movido de /home para /var/www mantém o rótulo de origem, e o servidor web não consegue lê-lo — mesmo com permissão 644 e dono correto.

A correção é restaurar o rótulo padrão do caminho:

sudo restorecon -Rv /var/www/meuapp/

Esse comando resolve uma fatia enorme dos casos, e é o primeiro a tentar. Ele consulta a política e aplica o contexto que aquele caminho deveria ter.

Se o diretório está em local não padrão, é preciso ensinar a política primeiro:

sudo semanage fcontext -a -t httpd_sys_content_t "/opt/site(/.*)?"
sudo restorecon -Rv /opt/site

Para conteúdo que a aplicação precisa escrever, o tipo é outro:

sudo semanage fcontext -a -t httpd_sys_rw_content_t "/opt/site/storage(/.*)?"
sudo restorecon -Rv /opt/site/storage

Repare na distinção entre conteúdo de leitura e de escrita. Rotular tudo como gravável anula boa parte da proteção — a ideia é justamente que o código não seja gravável pelo processo que o executa, como discutido no artigo sobre usuários e permissões.

Booleanos: o ajuste que resolve sem afrouxar

Boa parte dos bloqueios do SELinux não é sobre arquivo, e sim sobre comportamento — o servidor web conectar em banco remoto, enviar e-mail, ou acessar a rede.

Esses casos têm chaves prontas:

getsebool -a | grep httpd | head -20
# Permitir que o servidor web faça conexões de rede
sudo setsebool -P httpd_can_network_connect on

# Só para banco
sudo setsebool -P httpd_can_network_connect_db on

# Permitir envio de e-mail
sudo setsebool -P httpd_can_sendmail on

O -P torna permanente. Sem ele, o ajuste some no próximo reinício — e o problema volta semanas depois, sem que ninguém associe.

Procurar o booleano certo antes de escrever política customizada é quase sempre o caminho: eles são recortes estreitos, mantidos pela distribuição, e não abrem mais do que o necessário.

Diagnosticar em Permissive, corrigir em Enforcing

A técnica que resolve o caso difícil, sem deixar a máquina desprotegida permanentemente.

Coloque em modo permissivo, exercite a aplicação por completo, e colete tudo que teria sido bloqueado:

sudo setenforce 0
# exercite a aplicação: upload, envio de e-mail, geração de relatório
sudo ausearch -m AVC -ts recent | audit2allow -m meuapp

A saída é uma política legível, mostrando exatamente o que faltou. Leia antes de aplicar — é aqui que se decide se aquilo é legítimo ou se a aplicação está tentando algo que não deveria.

sudo ausearch -m AVC -ts recent | audit2allow -M meuapp
sudo semodule -i meuapp.pp
sudo setenforce 1

Duas advertências. O audit2allow gera permissões para tudo que apareceu, inclusive o que era bug — revise em vez de aplicar cegamente. E prefira restorecon ou booleano quando eles resolverem: política customizada é mais uma coisa para manter.

O modo permissivo deve durar minutos, não dias. Deixar assim "até resolver" é desligar a proteção com passos extras.

AppArmor, que é mais simples

O AppArmor trabalha por caminho em vez de rótulo, o que o torna mais fácil de ler.

sudo aa-status
ls /etc/apparmor.d/

Cada perfil lista os caminhos que o programa pode acessar e com quais permissões. As negações aparecem no log do kernel, indicando o perfil e o arquivo:

sudo journalctl -k | grep 'apparmor="DENIED"' | tail

Para colocar um perfil específico em modo de aprendizado, sem afetar os demais:

sudo aa-complain /etc/apparmor.d/usr.sbin.nginx
# exercite a aplicação
sudo aa-logprof                      # revisa e sugere ajustes
sudo aa-enforce /etc/apparmor.d/usr.sbin.nginx

O aa-logprof é interativo e pergunta item a item — mais trabalhoso que o audit2allow e mais seguro, porque força a leitura de cada permissão concedida.

Editando à mão, a sintaxe é direta:

# /etc/apparmor.d/local/usr.sbin.nginx
/opt/site/** r,
/opt/site/storage/** rw,
sudo systemctl reload apparmor

Onde isso costuma morder

Os cenários que geram mais chamados, em ordem:

  • Porta não padrão. Rodar o servidor web em 8080 é bloqueado até a porta ser declarada: sudo semanage port -a -t http_port_t -p tcp 8080.
  • Diretório de aplicação fora de `/var/www`. Precisa de rótulo ou perfil.
  • Arquivo movido em vez de copiado, mantendo o rótulo antigo.
  • Backup restaurado sem preservar contexto — restorecon -R resolve.
  • Container com volume do host. O SELinux exige o sufixo :z ou :Z na montagem, senão o processo dentro não acessa.

Esse último aparece muito e tem correção de um caractere:

docker run -v /opt/dados:/dados:Z minha-imagem

Por que não desligar

Vale ser direto sobre o que se perde. Essas camadas contêm exatamente o cenário que mais importa: a aplicação foi comprometida e o atacante tenta ir além dela. Sem elas, um processo comprometido faz tudo que o usuário dele pode fazer; com elas, ele faz apenas o que a política prevê para aquele papel.

Em servidor exposto à internet, essa diferença é a barreira entre um site comprometido e uma máquina comprometida.

Se ainda assim for necessário desligar temporariamente para destravar produção, faça-o com prazo e com registro — e trate como pendência, não como solução. É a mesma disciplina descrita no artigo sobre o que fazer quando não dá para atualizar: exceção consciente e documentada é defensável; exceção esquecida vira o buraco que ninguém lembra que abriu.

Um roteiro de dez minutos

Quando aparecer um Permission denied inexplicável, esta sequência resolve a maioria dos casos sem improviso:

# 1. É o MAC mesmo?
getenforce 2>/dev/null; sudo aa-status 2>/dev/null | head -3

# 2. O que foi negado, exatamente
sudo ausearch -m AVC -ts recent 2>/dev/null | tail -20
sudo journalctl -k --since '15 min ago' | grep -i 'denied'

# 3. Rótulo do arquivo versus rótulo esperado
ls -Z /caminho/do/arquivo
matchpathcon /caminho/do/arquivo     # o que a política espera

# 4. A correção mais provável
sudo restorecon -Rv /caminho/

O passo 3 é o que dá a resposta na maior parte dos casos: o matchpathcon mostra lado a lado o contexto atual e o esperado, e a divergência entre os dois é o problema inteiro.

Se restorecon não resolver, a pergunta seguinte é se o comportamento — não o arquivo — está sendo bloqueado. Aí o caminho é a lista de booleanos, e só depois a política customizada.

Uma observação sobre expectativa: essas camadas parecem hostis no começo porque falham de forma opaca, sem dizer o que fazer. Depois de aprender a ler a negação, elas ficam previsíveis — e a mesma opacidade que irrita durante a configuração é o que impede um invasor de descobrir o que a política permite.

Vale uma nota sobre distribuições. Ao migrar de CentOS para Debian ou Ubuntu — ou o contrário —, muda também o sistema de controle obrigatório, e com ele todo o vocabulário de diagnóstico. Quem vinha de restorecon e booleanos passa a lidar com perfis por caminho, e vice-versa. É um custo de transição real e raramente mencionado nos guias de migração, como o de para onde ir depois do CentOS.

Suba um servidor em minutos

Preço em real, suporte em português e dados no Brasil.

Criar conta