SELinux no servidor: por que o nginx retorna 403
O nginx retorna 403 mesmo com permissões corretas? Leia a recusa do SELinux, ajuste o rótulo com semanage e restorecon e mantenha o modo enforcing.
Por que o nginx retorna 403 para um ficheiro cujas permissões estão corretas
Quando o nginx retorna 403 para um ficheiro cujos bits de permissão estão corretos, quase sempre é o SELinux (security-enhanced Linux) que está a recusar a leitura. O SELinux verifica um segundo conjunto de regras depois de as permissões normais serem satisfeitas. O servidor web só pode ler ficheiros que tenham um rótulo de conteúdo web. O seu ficheiro tem outro rótulo, por isso a abertura falha e o nginx não tem nada para enviar.
Consulte o rótulo, não apenas o modo:
ls -ldZ /data/www /data/www/index.htmldrwxr-xr-x. root root unconfined_u:object_r:default_t:s0 /data/www
-rw-r--r--. root root unconfined_u:object_r:default_t:s0 /data/www/index.htmlO ponto apresentado depois de drwxr-xr-x significa que o ficheiro tem um rótulo SELinux. default_t é o valor atribuído a um caminho que nunca foi identificado pela política. Nenhuma regra do servidor web permite ler esse tipo. O log de erros mostra um erro Unix comum, por isso o problema parece ser de permissões:
2026/08/11 09:14:02 [error] 1183#1183: *1 open() "/data/www/index.html" failed (13: Permission denied), client: 203.0.113.9, server: _, request: "GET / HTTP/1.1"O kernel retorna 13: Permission denied para os dois tipos de recusa: a normal e a causada pelo SELinux. Por isso, a primeira tarefa é descobrir qual camada recusou o acesso. Não comece por setenforce 0.
A parte do modelo de que precisa
O SELinux é um mecanismo de controlo de acesso obrigatório, normalmente designado por MAC. Cada processo é executado num domínio, como httpd_t para o servidor Web. Cada ficheiro e cada porta de rede têm um tipo, como httpd_sys_content_t. A política é uma lista de combinações permitidas de domínio, tipo e ação. Tudo o que não estiver nessa lista é negado. O SELinux é aplicado depois da verificação Unix clássica. Por isso, os bits de permissão em drwxr-xr-x também têm de permitir primeiro o acesso. As duas camadas têm de autorizar a operação.
Um contexto completo tem quatro campos separados por dois-pontos, como system_u:system_r:httpd_t:s0: o utilizador SELinux, a função, o tipo e o nível. Num servidor, quase todo o trabalho incide no terceiro campo, o tipo. Dois comandos mostram os valores ativos:
ps -eZ | grep nginx
id -ZOs processos de trabalho do nginx apresentam um contexto terminado em httpd_t. A sua shell de login apresenta unconfined_u:unconfined_r:unconfined_t:s0, porque a política targeted predefinida confina os serviços e não restringe os utilizadores interativos. Isto é importante, porque o SELinux não substitui a execução de serviços com utilizadores que tenham o mínimo de privilégios. O SELinux limita os recursos a que um serviço pode aceder depois de alguém o comprometer.
Os três modos e quais imagens têm SELinux
sestatus
getenforceO modo enforcing bloqueia e regista. O modo permissive permite tudo e regista o que teria sido bloqueado. O modo disabled não carrega nenhuma política. getenforce mostra o modo atual. sestatus também mostra o modo definido em /etc/selinux/config, que é o modo aplicado novamente depois de um reboot.
Rocky Linux, AlmaLinux, Fedora e RHEL são fornecidos com o SELinux em modo enforcing e com a política targeted. Ubuntu e Debian usam AppArmor, que cumpre a mesma função através de um mecanismo diferente (a última secção aborda esse mecanismo). Por isso, a mesma aplicação pode ser instalada sem problemas num dos seus servidores e devolver 403 noutro.
Instale as ferramentas antes de precisar delas
sudo dnf install -y policycoreutils-python-utils setroubleshoot-serversemanage: command not found numa imagem mínima significa que policycoreutils-python-utils não está instalado: esse pacote contém semanage e audit2allow. setroubleshoot-server adiciona sealert e escreve no journal um resumo claro de cada recusa. Instale ambos num servidor novo, porque, quando precisar deles, algo já estará avariado.
Como interpretar uma recusa do SELinux no log de auditoria
Cada recusa é registada pelo daemon de auditoria como uma mensagem AVC (access vector cache):
sudo ausearch -m AVC,USER_AVC,SELINUX_ERR -ts recenttype=AVC msg=audit(1754896442.881:412): avc: denied { read } for pid=1183 comm="nginx" name="index.html" dev="vda1" ino=17203 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:default_t:s0 tclass=file permissive=0Quatro campos contêm toda a informação. comm é o programa que foi bloqueado. scontext é o contexto de origem, o domínio em que o processo estava a ser executado. tcontext é o contexto de destino, a etiqueta do objeto que tentou aceder. tclass é o tipo de objeto, neste caso um ficheiro. Em conjunto, significam que o processo em httpd_t tentou ler um ficheiro com a etiqueta default_t, e que permissive=0 confirma que o pedido foi realmente bloqueado, em vez de apenas registado.
Se ausearch não mostrar nada, o daemon de auditoria pode não estar em execução. Nesse caso, as recusas são gravadas no buffer circular do kernel:
sudo journalctl -k | grep -i avcAgora transforme o registo numa frase em português:
sudo ausearch -m AVC -ts recent | audit2why
sudo journalctl -t setroubleshoot --since today
sudo sealert -a /var/log/audit/audit.logaudit2why lê os mesmos registos e identifica a causa que reconhece: um booleano desativado, uma etiqueta que não corresponde à política ou a ausência total de uma regra. sealert percorre todo o log e apresenta um comando sugerido para cada recusa. Trate a sugestão como uma indicação. O texto varia entre releases, e sealert por vezes propõe um módulo de política personalizado quando a solução correta é corrigir uma etiqueta numa única linha.
Há mais um aspeto importante. A política contém regras dontaudit que ocultam recusas consideradas inofensivas. Por isso, um programa pode comportar-se incorretamente enquanto o log permanece vazio. Reative essas recusas durante um único teste:
sudo semodule -DB
# reproduce the problem, then read the log again
sudo semodule -BCorrija um caminho com rótulo incorreto usando semanage fcontext e restorecon
São necessários 2 comandos, e a ordem é importante. semanage fcontext -a regista qual deve ser o rótulo de um caminho. restorecon aplica esse padrão registado aos ficheiros no disco.
sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?"
sudo restorecon -Rv /data/www
ls -ldZ /data/www/index.htmlO caminho é uma expressão regular. (/.*)? abrange o próprio diretório e tudo o que está dentro dele, que é o necessário para uma raiz de documentos. Veja o que seria alterado antes de fazer a alteração: sudo restorecon -Rvn /data/www mostra as alterações de rótulo planeadas, porque -n significa não executar nenhuma ação. Depois de um restorecon real, o rótulo aparece como httpd_sys_content_t e o erro 403 desaparece sem reiniciar o serviço.
Use chcon apenas para testes. chcon -t httpd_sys_content_t index.html define o rótulo diretamente, e o próximo restorecon, uma atualização de pacote ou uma alteração completa de rótulos repõe esse valor, porque a política continua a indicar que o caminho deve ter outro rótulo. semanage fcontext é a versão persistente. Liste o que está registado com sudo semanage fcontext -l | grep '^/data'.
O conteúdo que o serviço precisa de escrever requer um tipo diferente. Use httpd_sys_rw_content_t para um diretório de carregamentos ou uma cache, e limite-o a esses caminhos: um site apenas de leitura sob um tipo que permite escrita concede mais acesso do que a aplicação necessita.
Por que motivo o rótulo estava incorreto? Quase sempre por causa da forma como os ficheiros chegaram ao local. mv mantém o rótulo existente de um ficheiro, portanto um site movido de /root chega com o rótulo admin_home_t e permanece assim. Um cp simples atribui ao novo ficheiro o rótulo predefinido do diretório de destino, que normalmente é o comportamento pretendido, enquanto cp -a e rsync -X copiam os rótulos da origem juntamente com o ficheiro. Um git clone para um novo diretório de nível superior produz default_t. Quando uma página carrega corretamente a partir de /usr/share/nginx/html, mas falha a partir do seu próprio diretório, esta é a razão.
Corrija uma classe de comportamento com um booleano
Algumas falhas não são causadas por um problema de rótulo. Um reverse proxy numa máquina Rocky ou AlmaLinux recém-instalada devolve 502, e o log de erros mostra:
2026/08/11 10:02:55 [crit] 1183#1183: *3 connect() to 127.0.0.1:3000 failed (13: Permission denied) while connecting to upstreamO seu upstream está correto. O domínio httpd_t não tem permissão, por padrão, para abrir ligações de rede de saída, por isso a chamada connect() é recusada antes de chegar à interface de loopback. Um único parâmetro controla todo esse comportamento:
getsebool -a | grep httpd_can_network
sudo setsebool -P httpd_can_network_connect on-P é o sinalizador relevante: grava o valor no disco. Sem -P, a alteração perde-se no reboot seguinte. O serviço funciona até a máquina reiniciar. Confirme com semanage boolean -l | grep httpd_can_network_connect, que mostra o valor em execução junto do valor armazenado.
Prefira um booleano a uma regra escrita manualmente sempre que existir essa opção. Os booleanos são fornecidos com a política da distribuição. Por isso, são mantidos, documentados e fáceis de localizar pela pessoa seguinte. getsebool -a lista todos os booleanos existentes no sistema.
Fazer um serviço escutar numa porta não padrão
As portas também têm rótulos. Mova o nginx para 8081 e ele recusará iniciar:
nginx: [emerg] bind() to 0.0.0.0:8081 failed (13: Permission denied)httpd_t pode associar-se a portas com o rótulo http_port_t, e 8081 não tem esse rótulo. Adicione-o:
sudo semanage port -l | grep -w http_port_t
sudo semanage port -a -t http_port_t -p tcp 8081Verifique primeiro a lista. Várias portas altas já estão autorizadas, incluindo 8008 e 8443, e adicionar uma porta duas vezes falha com ValueError: Port tcp/8081 already defined. Se a porta já pertencer a um tipo diferente, altere-o com semanage port -m -t http_port_t -p tcp 8081 em vez de o adicionar.
O mesmo comando permite que uma porta SSH alterada funcione. Bind to port 2222 on 0.0.0.0 failed: Permission denied em journalctl -u sshd significa que 2222 está ausente de ssh_port_t, por isso execute sudo semanage port -a -t ssh_port_t -p tcp 2222 antes de reiniciar o daemon e encerrar a sessão. Esta é a etapa que as pessoas ignoram quando seguem um guia genérico para proteger o SSH num VPS numa imagem da família Red Hat. O SELinux também não é uma firewall, por isso a porta ainda tem de estar aberta: sudo firewall-cmd --permanent --add-port=8081/tcp && sudo firewall-cmd --reload neste caso, ou ufw numa imagem Debian ou Ubuntu.
Quando não existe boolean nem rótulo para alterar
Isto é raro num servidor normal e é neste ponto que podem ocorrer danos. audit2allow pode criar um módulo de política a partir das negações presentes no log:
sudo ausearch -m AVC -ts recent -c nginx | audit2allow -M nginx_local
cat nginx_local.te
sudo semodule -i nginx_local.ppLeia nginx_local.te antes de o instalar. Duas práticas mantêm este procedimento seguro. Filtre a entrada para incluir apenas o programa que está a corrigir, usando -c, porque encaminhar uma semana de negações não relacionadas para audit2allow concede todas de uma vez. Nunca instale um módulo criado a partir de uma negação que não consiga explicar: é fácil gerar uma regra que permita a httpd_t ler todos os ficheiros do sistema, mas pode ser difícil detetá-la meses mais tarde. Remova um módulo com sudo semodule -r nginx_local.
O modo permissivo serve para diagnóstico, não para corrigir o problema
sudo setenforce 0
# reproduce the problem once, all the way through
sudo ausearch -m AVC -ts recent
sudo setenforce 1O modo permissivo permite o acesso e regista o evento. O seu verdadeiro valor está na completude. No modo enforcing, o serviço para na primeira negação. Assim, corrige essa negação, reinicia o serviço e encontra a segunda. No modo permissivo, a execução continua e o log regista todas as negações numa única passagem. Depois, volta ao modo enforcing e corrige-as em conjunto.
setenforce não altera /etc/selinux/config. Por isso, um reboot faz o sistema voltar ao modo enforcing. Isto funciona como uma rede de segurança. Também explica por que motivo uma “correção” que consistia em setenforce 0 reaparece no pior momento possível. Se um serviço precisar de mais permissões durante o trabalho, marque apenas esse domínio em vez de todo o sistema: sudo semanage permissive -a httpd_t mantém tudo o resto no modo enforcing, e sudo semanage permissive -d httpd_t desfaz essa alteração.
Por que desativar o SELinux custa mais do que corrigir o contexto
Definir SELINUX=disabled em /etc/selinux/config troca uma correção de contexto de uma linha por um servidor permanentemente mais fraco. A diferença aparece no dia em que uma aplicação web é comprometida. Com o modo enforcing ativo, o código do invasor é executado em httpd_t. Por isso, pode ler o conteúdo web, mas a leitura de /etc/shadow ou a escrita de uma unidade systemd são recusadas pela política, independentemente do que o utilizador Unix permitiria. Sem uma política carregada, o mesmo código obtém tudo o que a conta de serviço tiver acesso para obter.
A desativação também gera um custo posterior. Enquanto nenhuma política está carregada, os ficheiros novos são criados sem contexto. Assim, o sistema de ficheiros fica desalinhado da política. Quando o SELinux é reativado, é necessário fazer uma relabelização completa, ou vários serviços falham ao mesmo tempo:
sudo fixfiles -F onboot
sudo rebootIsto grava /.autorelabel e relabeliza todos os sistemas de ficheiros durante o próximo boot. Num disco grande, o processo demora bastante e a consola parece bloqueada. Por isso, inicie-o quando puder esperar. No Rocky Linux e no AlmaLinux 9, o ficheiro de configuração já não desativa sozinho a parte do kernel. A forma documentada de desativar completamente o SELinux é usar um argumento do kernel (sudo grubby --update-kernel ALL --args selinux=0). Esse comando é útil quando assume a administração de um servidor configurado por outra pessoa. Não é a correção para um erro 403.
Os contentores acrescentam mais um rótulo
Num host da família Red Hat, os processos dos contentores são executados em container_t e só podem ler ficheiros identificados com o rótulo container_file_t. Um bind mount do host falha com Permission denied dentro do contentor, embora ls -l no host pareça perfeitamente normal. O sufixo :Z instrui o runtime a voltar a identificar o mount:
docker run -d -v /data/appdata:/var/lib/app:Z myimage:Z identifica o diretório apenas para este contentor. :z identifica-o para partilha entre contentores. Se apontar :Z para um diretório usado por outros serviços, o runtime identifica esse diretório recursivamente, o que interrompe esses serviços. Por isso, atribua aos contentores os seus próprios caminhos. O restante da configuração é igual ao de qualquer outra imagem, conforme descrito em executar Docker numa VPS.
O Ubuntu e o Debian incluem AppArmor
A função é a mesma, mas o desenho é diferente. O AppArmor confina um programa pelo caminho do executável, usando um perfil em /etc/apparmor.d/, em vez de etiquetar ficheiros no disco. Não há nada para voltar a etiquetar nem restorecon. Comece aqui:
sudo aa-status
sudo journalctl -k | grep -i apparmorUma recusa aparece como apparmor="DENIED" operation="open" profile="/usr/sbin/nginx" name="/data/www/index.html" requested_mask="r". O procedimento tem a mesma estrutura: leia a recusa, encontre o perfil e altere a regra. sudo apt install apparmor-utils fornece aa-complain (permissivo para um perfil) e aa-enforce para o repor. O Ubuntu confina um conjunto selecionado de serviços empacotados e deixa os restantes sem confinamento. Por isso, leia aa-status para ver o que está realmente ativo, em vez de assumir.
Há um hábito comum aos dois sistemas. Quando um serviço indica Permission denied para algo que parece correto, leia o log de segurança antes de alterar as permissões. As permissões raramente são o problema duas vezes.
FAQ
Por que o nginx retorna 403 quando as permissões do ficheiro estão corretas?
Porque o SELinux recusou a leitura, e não por causa dos bits de permissão. O servidor Web é executado no domínio httpd_t e só pode ler ficheiros identificados para conteúdo Web. Por isso, um ficheiro identificado como default_t ou admin_home_t é recusado e o nginx não tem nada para servir. Confirme com sudo ausearch -m AVC -ts recent. O comando mostra scontext a terminar em httpd_t e tcontext com o tipo incorreto. Depois, registe o identificador correto e aplique-o: sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?" seguido de sudo restorecon -Rv /data/www.
É seguro executar setenforce 0 para fazer um serviço funcionar?
setenforce 0 é uma etapa de diagnóstico, não uma correção. Use-o uma vez para reproduzir o problema, para que o log registe todas as recusas numa única execução. Leia-as com sudo ausearch -m AVC -ts recent, execute sudo setenforce 1 e corrija as causas. Um servidor deixado em modo permissivo regista todas as recusas e não bloqueia nenhuma. Assim, mantém o ruído e perde a proteção. Se um serviço precisar de funcionar enquanto trabalha, execute sudo semanage permissive -a httpd_t para que o restante sistema continue em modo enforcing.
Como executo um serviço numa porta não padrão com o SELinux em modo enforcing?
Adicione a porta ao tipo ao qual esse serviço tem permissão para fazer bind. Para um servidor Web na porta 8081: sudo semanage port -a -t http_port_t -p tcp 8081. Para SSH na porta 2222: sudo semanage port -a -t ssh_port_t -p tcp 2222. Verifique primeiro a lista atual com sudo semanage port -l | grep -w http_port_t, porque uma porta que já esteja listada falha com ValueError: Port tcp/8081 already defined. Sem esta etapa, o daemon termina no arranque com bind() ... Permission denied, mesmo que nenhum outro processo esteja a utilizar a porta.
O Ubuntu tem SELinux?
Não. Ubuntu e Debian incluem AppArmor, que aplica um perfil associado ao caminho de um executável, e não identificadores nos ficheiros. Verifique com sudo aa-status e procure linhas apparmor="DENIED" em sudo journalctl -k. O Ubuntu confina um conjunto selecionado de serviços fornecidos em pacotes, por isso muitos programas são executados sem confinamento por predefinição. É no Rocky Linux e no AlmaLinux que encontrará o SELinux em modo enforcing por predefinição, além do Fedora e do RHEL.