SELinux: por que o nginx retorna 403 mesmo com permissões
nginx retorna 403, mas as permissões parecem corretas? Aprenda a ler a negação do SELinux, corrigir labels com semanage e restorecon e manter o enforcing ativo.
Por que o nginx retorna 403 num ficheiro cujas permissões estão corretas
Quando o nginx retorna 403 num 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 da validação das permissões normais. O servidor Web só pode ler ficheiros que tenham um label de conteúdo Web. O seu ficheiro tem outro label, por isso a abertura falha e o nginx não tem nada para enviar.
Veja o label, 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 label SELinux. default_t é o que um caminho recebe quando a política nunca teve conhecimento dele. Nenhuma regra do servidor Web permite ler esse tipo. O log de erros apresenta um erro Unix normal, por isso o problema parece estar relacionado com 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. Portanto, 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 das 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 o autorizar.
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 atuais:
ps -eZ | grep nginx
id -ZOs processos de trabalho do nginx mostram um contexto terminado em httpd_t. A shell da sua sessão mostra unconfined_u:unconfined_r:unconfined_t:s0, porque a política targeted predefinida confina os serviços e deixa os utilizadores interativos sem confinamento. Isto é importante porque o SELinux não substitui a execução de serviços com utilizadores de privilégios mínimos. O SELinux limita aquilo a que um serviço pode aceder depois de alguém o comprometer.
Os três modos e quais imagens incluem o 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 qualquer política. getenforce apresenta o modo atual. sestatus também apresenta o modo definido em /etc/selinux/config, que é o modo restaurado depois de um reboot.
Rocky Linux, AlmaLinux, Fedora e RHEL incluem o SELinux no modo enforcing com a política targeted. Esse padrão comum é herdado, não uma coincidência, porque os quatro projetos descendem da mesma linhagem Red Hat, que passou pelo CentOS antes do aparecimento do Rocky Linux e do AlmaLinux. A escolha entre os dois não altera nada nesta página, porque ambos incluem a mesma política e as mesmas ferramentas. Assim, escolher entre Rocky Linux e AlmaLinux depende das garantias de compatibilidade e do suporte para CPUs mais antigas, não dos padrões de segurança. Ubuntu e Debian incluem o AppArmor, que cumpre a mesma função com um mecanismo diferente (a última secção aborda esse mecanismo). Por isso, a mesma aplicação pode ser instalada corretamente 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 está ausente: esse pacote contém semanage e audit2allow. setroubleshoot-server adiciona sealert e escreve um resumo claro de cada bloqueio no journal. Instale ambos num servidor novo, porque, quando precisar deles, algo já estará avariado.
Como ler 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 executar. tcontext é o contexto de destino, a etiqueta do objeto que o processo 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 permissive=0 indica que o pedido foi realmente bloqueado, em vez de apenas registado.
Se ausearch não produzir saída, 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: uma boolean que está desligada, uma etiqueta que não corresponde à política ou a ausência total de uma regra. sealert percorre todo o log e imprime 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 resposta correta é corrigir uma etiqueta numa única linha.
Há mais um ponto importante. A política contém regras dontaudit que ocultam recusas consideradas inofensivas. Por isso, um programa pode comportar-se de forma incorreta enquanto o log permanece vazio. Torne essas recusas visíveis durante um único teste:
sudo semodule -DB
# reproduce the problem, then read the log again
sudo semodule -BCorrigir um caminho com rótulo SELinux incorreto usando semanage fcontext e restorecon
São necessários dois 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 os relabelings previstos, porque -n significa não executar nenhuma ação. Depois de um restorecon real, o rótulo é apresentado por httpd_sys_content_t e o erro 403 desaparece sem reiniciar o serviço.
Use chcon apenas como teste. chcon -t httpd_sys_content_t index.html define o rótulo diretamente, e o próximo restorecon, uma atualização de pacotes ou um relabeling completo repõe-no, porque a política continua a indicar que o caminho deve ter outro rótulo. Num servidor onde o dnf-automatic aplica atualizações de segurança de forma programada, essa reposição ocorre no horário definido, e não enquanto está diante da máquina, pelo que o site pode deixar de funcionar horas depois da última alteração. semanage fcontext é a versão persistente. Liste o que foi registado com sudo semanage fcontext -l | grep '^/data'.
O conteúdo que o serviço precisa de gravar requer outro tipo. Use httpd_sys_rw_content_t para um diretório de uploads ou uma cache, e limite-o a esses caminhos: um site apenas de leitura sob um tipo gravável concede mais acesso do que a aplicação necessita.
Porque é que o rótulo estava incorreto? Quase sempre devido à forma como os ficheiros chegaram ao local. mv mantém o rótulo existente de um ficheiro, pelo que um site movido de /root chega com o rótulo admin_home_t e permanece assim. Um cp simples atribui ao ficheiro novo 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 é carregada corretamente a partir de /usr/share/nginx/html, mas falha a partir do seu próprio diretório, esta é a razão.
Corrigir uma classe de comportamento com um booleano
Nem todas as falhas estão relacionadas com uma etiqueta. Um reverse proxy num servidor Rocky ou AlmaLinux acabado de instalar 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 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 é a opção relevante: escreve o valor no disco. Sem -P, a alteração perde-se no próximo reboot, deixando o serviço a funcionar apenas até o servidor reiniciar. Confirme com semanage boolean -l | grep httpd_can_network_connect, que apresenta 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 fazem parte da política distribuída com a 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 são classificadas. Altere o nginx para 8081 e ele recusa iniciar:
nginx: [emerg] bind() to 0.0.0.0:8081 failed (13: Permission denied)httpd_t pode associar-se a portas classificadas como http_port_t, e 8081 não é uma delas. Adicione-a:
sudo semanage port -l | grep -w http_port_t
sudo semanage port -a -t http_port_t -p tcp 8081Consulte primeiro a lista. Várias portas altas já são permitidas, 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 usar uma porta SSH alterada. Bind to port 2222 on 0.0.0.0 failed: Permission denied em journalctl -u sshd significa que 2222 não está definida em ssh_port_t. Execute sudo semanage port -a -t ssh_port_t -p tcp 2222 antes de reiniciar o daemon e encerrar a sessão. Este é o passo que muitas 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 continua a ter de estar aberta: sudo firewall-cmd --permanent --add-port=8081/tcp && sudo firewall-cmd --reload neste caso, ou ufw numa imagem Debian ou Ubuntu. A flag --permanent tem a mesma armadilha após o reboot que -P numa boolean, e vale a pena consultar uma vez as zonas que determinam a que interfaces uma regra se aplica em noções básicas de firewalld para um VPS Rocky ou AlmaLinux.
Quando não há nenhuma opção booleana nem nenhum rótulo para alterar
Isto é raro num servidor normal e é neste ponto que podem ocorrer danos. O audit2allow pode criar um módulo de política a partir das negações registadas 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. Dois hábitos mantêm este procedimento seguro. Filtre a entrada para incluir apenas o programa que está a corrigir, usando -c, porque enviar para audit2allow uma semana de negações não relacionadas instala todas de uma vez. Nunca instale um módulo criado a partir de uma negação que não consiga explicar: uma regra que permita a httpd_t ler todos os ficheiros do sistema é fácil de gerar e difícil de detetar 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 abrangência. No modo enforcing, o serviço para na primeira recusa. Assim, corrige essa recusa, reinicia o serviço e encontra a segunda. No modo permissivo, a execução continua e o log regista todas as recusas 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 regressar ao modo enforcing. Esta é 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 enquanto trabalha nele, marque 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 reverte 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 fica evidente no dia em que uma aplicação Web é comprometida. Com o modo enforcing ativo, o código do atacante é 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 é recusada pela política, independentemente do que o utilizador Unix permitiria. Sem uma política carregada, esse mesmo código obtém todas as permissões da conta de serviço.
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. Ao voltar a ativar o SELinux, é necessário fazer uma relabelização completa. Caso contrário, vários serviços podem falhar ao mesmo tempo:
sudo fixfiles -F onboot
sudo rebootIsto grava /.autorelabel e faz a relabelização de 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. Como a máquina já vai ser reiniciada, vale a pena verificar primeiro o que mais foi colocado na fila para reinício. É isso que needs-restarting mostra depois de uma atualização com dnf deixar kernels e bibliotecas antigos na memória. 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 totalmente o SELinux é usar um argumento do kernel (sudo grubby --update-kernel ALL --args selinux=0). Esse comando é útil quando recebe um servidor configurado por outra pessoa. Ele não corrige um erro 403.
Containers adicionam mais um rótulo
Num host da família Red Hat, os processos dos containers executam em container_t e só podem ler ficheiros com o rótulo container_file_t. Um bind mount do host falha com Permission denied dentro do container, embora ls -l no host pareça perfeitamente normal. O sufixo :Z indica ao runtime que deve voltar a etiquetar o mount:
docker run -d -v /data/appdata:/var/lib/app:Z myimage:Z etiqueta o diretório apenas para este container. :z etiqueta-o para partilha entre containers. Se apontar :Z para um diretório utilizado por outros serviços, o diretório é etiquetado recursivamente, o que interrompe esses serviços. Por isso, atribua aos containers os seus próprios caminhos. Se o engine ainda não estiver instalado no sistema, tenha em atenção que, nestas distribuições, o comando docker corresponde frequentemente ao podman com esse nome, uma particularidade resolvida em os passos de instalação do Rocky e AlmaLinux antes de se deparar com este problema. O resto da configuração é igual ao de qualquer outra imagem, conforme explicado em executar Docker numa VPS.
Ubuntu e Debian fornecem AppArmor
A mesma finalidade, um 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 é necessário voltar a etiquetar nada nem existe 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 fluxo de trabalho é semelhante: 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 reativar. O Ubuntu confina um conjunto selecionado de serviços fornecidos por pacotes e deixa os restantes sem confinamento. Por isso, leia aa-status para ver o que está realmente ativo, em vez de assumir.
Um hábito aplica-se a ambos os sistemas. Quando um serviço indica Permission denied num recurso 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 negou 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, que mostra scontext a terminar em httpd_t e tcontext com o tipo incorreto. Depois, registe o rótulo 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 para reproduzir o problema uma vez, para que o log registe todas as negações numa única passagem. Leia-as com sudo ausearch -m AVC -ts recent, depois execute sudo setenforce 1 e corrija as causas. Um servidor deixado em modo permissivo regista todas as negações e não bloqueia nenhuma. Assim, mantém o ruído e perde a proteção. Se um serviço precisar de mais permissões enquanto trabalha, execute sudo semanage permissive -a httpd_t para que o resto da máquina 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 o serviço tem permissão para se associar. 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 durante o arranque com bind() ... Permission denied, mesmo que nenhum outro processo esteja a usar a porta.
O Ubuntu tem SELinux?
Não. Ubuntu e Debian incluem o AppArmor, que aplica um perfil associado ao caminho de um executável, e não rótulos nos ficheiros. Verifique-o com sudo aa-status e procure linhas apparmor="DENIED" em sudo journalctl -k. O Ubuntu confina um conjunto selecionado de serviços incluídos nos pacotes, pelo que muitos programas são executados sem confinamento por predefinição. É no Rocky Linux e no AlmaLinux que encontra o SELinux em modo enforcing imediatamente após a instalação, tal como no Fedora e no RHEL.