SELinux e nginx: risolvere i 403 con permessi corretti
nginx restituisce 403 anche con permessi corretti? Leggi il denial SELinux, correggi il contesto con semanage e restorecon, mantenendo enforcing attivo.
Perché nginx restituisce 403 per un file con permessi corretti
Quando nginx restituisce 403 per un file i cui bit dei permessi sono corretti, nella quasi totalità dei casi SELinux (security-enhanced Linux) ne rifiuta la lettura. SELinux verifica un secondo insieme di regole dopo il controllo dei permessi normali. Il server web può leggere solo i file che hanno un contesto SELinux per i contenuti web. Il file ha un contesto diverso, quindi l'apertura fallisce e nginx non ha dati da inviare.
Controlla il contesto, non soltanto la modalità:
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.htmlIl punto visualizzato dopo drwxr-xr-x indica che il file ha un contesto SELinux. default_t è il contesto assegnato a un percorso che non è mai stato riconosciuto dalla policy. Nessuna regola del server web consente di leggere questo tipo di contesto. Il log degli errori mostra un normale errore Unix, per questo il problema sembra dipendere dai permessi:
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"Il kernel restituisce 13: Permission denied per entrambi i tipi di rifiuto: quello normale e quello causato da SELinux. Il primo obiettivo è quindi stabilire quale livello ha negato l'accesso. Non iniziare da setenforce 0.
La parte del modello necessaria
SELinux è un controllo degli accessi obbligatorio, generalmente indicato come MAC. Ogni processo viene eseguito in un dominio, ad esempio httpd_t per il server web. Ogni file e ogni porta di rete hanno un tipo, ad esempio httpd_sys_content_t. La policy è un elenco delle combinazioni consentite di dominio, tipo e azione. Tutto ciò che non compare nell’elenco viene negato. SELinux viene applicato dopo il controllo Unix tradizionale, quindi i bit dei permessi in drwxr-xr-x devono comunque consentire prima l’accesso. Entrambi i livelli devono autorizzarlo.
Un contesto completo ha quattro campi separati da due punti, come system_u:system_r:httpd_t:s0: l’utente SELinux, il ruolo, il tipo e il livello. Su un server si lavora quasi sempre sul terzo campo, il tipo. Due comandi mostrano i valori correnti:
ps -eZ | grep nginx
id -ZI processi worker di nginx mostrano un contesto che termina con httpd_t. La shell di login mostra unconfined_u:unconfined_r:unconfined_t:s0, perché la policy predefinita targeted confina i servizi e lascia indipendenti gli utenti interattivi. Questo è importante, perché SELinux non sostituisce l’esecuzione dei servizi con utenti dotati dei privilegi minimi. Limita ciò che un servizio può raggiungere dopo una compromissione.
Le tre modalità e quali distribuzioni includono SELinux
sestatus
getenforceLa modalità Enforcing blocca e registra gli eventi. La modalità Permissive consente tutte le operazioni e registra ciò che avrebbe bloccato. La modalità Disabled non carica alcuna policy. getenforce stampa la modalità corrente. sestatus stampa anche la modalità configurata in /etc/selinux/config, cioè quella che viene ripristinata dopo un riavvio.
Rocky Linux, AlmaLinux, Fedora e RHEL includono SELinux in modalità enforcing con la policy targeted. Ubuntu e Debian includono invece AppArmor, che svolge la stessa funzione con un meccanismo diverso (l'ultima sezione lo descrive). Di conseguenza, la stessa applicazione può installarsi correttamente su uno dei server e restituire 403 su un altro.
Installa gli strumenti prima di averne bisogno
sudo dnf install -y policycoreutils-python-utils setroubleshoot-serverSu un'immagine minimale, semanage: command not found indica che manca policycoreutils-python-utils: quel pacchetto contiene semanage e audit2allow. setroubleshoot-server aggiunge sealert e scrive nel journal un riepilogo in linguaggio semplice di ogni diniego. Installali entrambi su un server appena configurato, perché quando servono significa che qualcosa è già guasto.
Come leggere un rifiuto SELinux nel log di audit
Ogni rifiuto viene registrato dal demone di audit come messaggio 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=0Quattro campi descrivono l’intero evento. comm è il programma a cui è stato negato l’accesso. scontext è il contesto di origine, cioè il dominio in cui era in esecuzione il processo. tcontext è il contesto di destinazione, ovvero l’etichetta dell’oggetto che il processo ha tentato di usare. tclass indica il tipo di oggetto, in questo caso un file. Letti insieme, questi campi significano che il processo in httpd_t ha tentato di leggere un file con etichetta default_t e che permissive=0 conferma che la richiesta è stata effettivamente bloccata, non soltanto registrata.
Se ausearch non restituisce nulla, il demone di audit potrebbe non essere in esecuzione. In questo caso, i rifiuti vengono registrati nel ring buffer del kernel:
sudo journalctl -k | grep -i avcOra trasformate il record in una frase:
sudo ausearch -m AVC -ts recent | audit2why
sudo journalctl -t setroubleshoot --since today
sudo sealert -a /var/log/audit/audit.logaudit2why legge gli stessi record e indica la causa riconosciuta: un booleano disattivato, un’etichetta che non corrisponde alla policy oppure l’assenza completa di una regola. sealert analizza l’intero log e stampa un comando suggerito per ogni rifiuto. Considerate il suggerimento un’indicazione, non una soluzione definitiva. La formulazione cambia tra le release e sealert può proporre un modulo di policy personalizzato quando la soluzione corretta consiste nel correggere un’etichetta con una sola riga.
C’è un altro aspetto da conoscere. La policy contiene regole dontaudit che nascondono i rifiuti considerati innocui. Di conseguenza, un programma può comportarsi in modo anomalo mentre il log rimane vuoto. Rendete visibili questi rifiuti per la durata di un singolo test:
sudo semodule -DB
# reproduce the problem, then read the log again
sudo semodule -BCorreggere un percorso con etichetta errata usando semanage fcontext e restorecon
Sono necessari due comandi e l’ordine è importante. semanage fcontext -a registra quale dovrebbe essere l’etichetta del percorso. restorecon applica il valore predefinito registrato ai file presenti sul disco.
sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?"
sudo restorecon -Rv /data/www
ls -ldZ /data/www/index.htmlIl percorso è un’espressione regolare. (/.*)? include la directory stessa e tutto il relativo contenuto, come richiesto da una document root. Prima di apportare la modifica, verifica cosa cambierebbe: sudo restorecon -Rvn /data/www mostra le rietichettature previste, perché -n indica di non eseguire alcuna modifica. Dopo un restorecon effettivo, l’etichetta risulta httpd_sys_content_t e l’errore 403 scompare senza riavviare il servizio.
Usa chcon solo per un test. chcon -t httpd_sys_content_t index.html imposta direttamente l’etichetta, ma il successivo restorecon, un aggiornamento del pacchetto o una rietichettatura completa la reimposta, perché la policy continua a indicare che il percorso dovrebbe avere un’altra etichetta. semanage fcontext è la versione persistente. Elenca le configurazioni registrate con sudo semanage fcontext -l | grep '^/data'.
I contenuti che il servizio deve modificare richiedono un tipo diverso. Usa httpd_sys_rw_content_t per una directory di upload o una cache e limita l’uso a quei percorsi: un sito in sola lettura con un tipo scrivibile concede più accessi di quelli necessari all’applicazione.
Perché l’etichetta era errata? Quasi sempre dipende dal modo in cui sono arrivati i file. mv mantiene l’etichetta esistente di un file, quindi un sito spostato da /root arriva con l’etichetta admin_home_t e la conserva. Un semplice cp assegna al nuovo file l’etichetta predefinita della directory di destinazione, che di solito è il comportamento desiderato, mentre cp -a e rsync -X copiano insieme al file anche le etichette dell’origine. Un git clone in una nuova directory di primo livello produce default_t. Se una pagina viene caricata correttamente da /usr/share/nginx/html ma non dalla directory che hai creato, questa è la causa.
Correggere una classe di comportamento con un booleano
Non tutti i malfunzionamenti dipendono da un problema di etichetta. Un reverse proxy su un sistema Rocky o AlmaLinux appena installato restituisce 502 e il log degli errori riporta:
2026/08/11 10:02:55 [crit] 1183#1183: *3 connect() to 127.0.0.1:3000 failed (13: Permission denied) while connecting to upstreamIl server upstream funziona. Per impostazione predefinita, il dominio httpd_t non può aprire connessioni di rete in uscita, quindi la chiamata connect() viene rifiutata prima di raggiungere l'interfaccia loopback. Un'unica impostazione controlla questo comportamento:
getsebool -a | grep httpd_can_network
sudo setsebool -P httpd_can_network_connect on-P è il flag rilevante: scrive il valore su disco. Senza -P la modifica viene persa al riavvio successivo e il servizio funziona solo fino al riavvio della macchina. Verificare con semanage boolean -l | grep httpd_can_network_connect, che stampa il valore attivo accanto a quello memorizzato.
Preferire un booleano a una regola scritta manualmente quando ne esiste uno. I booleani sono inclusi nella policy della distribuzione, quindi vengono mantenuti e documentati e sono facili da individuare per chi interverrà in seguito. getsebool -a elenca tutti i booleani presenti nel sistema.
Consentire a un servizio di rimanere in ascolto su una porta non standard
Le porte sono associate anche a un tipo SELinux. Spostare nginx sulla porta 8081 ne impedisce l'avvio:
nginx: [emerg] bind() to 0.0.0.0:8081 failed (13: Permission denied)httpd_t può associare porte al tipo http_port_t, ma 8081 non è tra quelle consentite. Aggiungila:
sudo semanage port -l | grep -w http_port_t
sudo semanage port -a -t http_port_t -p tcp 8081Prima controlla l'elenco. Sono già consentite diverse porte alte, tra cui 8008 e 8443, mentre aggiungere una porta due volte genera l'errore ValueError: Port tcp/8081 already defined. Se la porta appartiene già a un tipo diverso, modificalo con semanage port -m -t http_port_t -p tcp 8081 invece di aggiungerla.
Lo stesso comando consente di utilizzare una porta SSH modificata. Bind to port 2222 on 0.0.0.0 failed: Permission denied in journalctl -u sshd indica che 2222 non è presente in ssh_port_t, quindi esegui sudo semanage port -a -t ssh_port_t -p tcp 2222 prima di riavviare il demone e chiudere la sessione. Questo è il passaggio che spesso viene omesso nelle guide generiche per mettere in sicurezza SSH su un VPS con un'immagine della famiglia Red Hat. SELinux non è neppure un firewall, quindi la porta deve essere comunque aperta: sudo firewall-cmd --permanent --add-port=8081/tcp && sudo firewall-cmd --reload in questo caso, oppure ufw su un'immagine Debian o Ubuntu.
Quando non esistono né un boolean né un’etichetta da modificare
Su un server normale è una situazione rara e proprio in questo caso si rischia di causare danni. audit2allow può creare un modulo di policy a partire dai rifiuti registrati nei log:
sudo ausearch -m AVC -ts recent -c nginx | audit2allow -M nginx_local
cat nginx_local.te
sudo semodule -i nginx_local.ppLeggere nginx_local.te prima di installarlo. Due abitudini aiutano a operare in sicurezza. Limitare l’input al solo programma che si sta correggendo con -c, perché passare a audit2allow una settimana di rifiuti non correlati installa tutte quelle autorizzazioni in una volta sola. Non installare mai un modulo creato a partire da un rifiuto che non si sa spiegare: una regola che consente a httpd_t di leggere ogni file del sistema è facile da generare e difficile da individuare mesi dopo. Rimuovere un modulo con sudo semodule -r nginx_local.
La modalità permissiva serve per la diagnostica, non per risolvere il problema
sudo setenforce 0
# reproduce the problem once, all the way through
sudo ausearch -m AVC -ts recent
sudo setenforce 1La modalità permissiva consente l'accesso e registra l'evento. Il suo valore principale è la completezza. In modalità enforcing, il servizio si arresta al primo diniego. Si risolve quel problema, si riavvia il servizio e si incontra il secondo diniego. In modalità permissiva, l'esecuzione continua e il log raccoglie tutti i dinieghi in un'unica passata. Poi si torna alla modalità enforcing e si risolvono insieme.
setenforce non modifica /etc/selinux/config, quindi un riavvio riporta il sistema in modalità enforcing. Questo costituisce una misura di sicurezza. Per lo stesso motivo, una presunta "correzione" ottenuta con setenforce 0 ricompare nel momento peggiore. Se un servizio richiede maggiore flessibilità durante le verifiche, applica la modifica al relativo dominio invece che all'intero sistema: sudo semanage permissive -a httpd_t lascia tutto il resto in modalità enforcing, mentre sudo semanage permissive -d httpd_t annulla la modifica.
Perché disabilitare SELinux costa più che correggere il contesto
Impostare SELINUX=disabled in /etc/selinux/config sostituisce una correzione del contesto su una sola riga con un server permanentemente meno protetto. La differenza si vede quando un'applicazione web viene compromessa. In modalità enforcing, il codice dell'attaccante viene eseguito in httpd_t, quindi può leggere i contenuti web, mentre la lettura di /etc/shadow o la scrittura di un'unità systemd viene rifiutata dalla policy, indipendentemente dalle autorizzazioni che l'utente Unix avrebbe concesso. Senza una policy caricata, lo stesso codice ottiene tutti i permessi dell'account del servizio.
Disabilitare SELinux comporta anche costi successivi. Quando non è caricata alcuna policy, i nuovi file vengono creati senza contesto, quindi il filesystem si allontana dalla configurazione prevista dalla policy. Quando si riattiva SELinux, è necessario eseguire un relabel completo; in caso contrario, diversi servizi possono non avviarsi contemporaneamente:
sudo fixfiles -F onboot
sudo rebootQuesto imposta /.autorelabel ed esegue il relabel di tutti i filesystem durante il boot successivo. Su un disco di grandi dimensioni l'operazione richiede molto tempo e la console può sembrare bloccata, quindi avviala quando puoi attendere. In Rocky Linux e AlmaLinux 9 il file di configurazione non disabilita più autonomamente la componente del kernel; il metodo documentato per disabilitare completamente SELinux consiste nell'usare un argomento del kernel (sudo grubby --update-kernel ALL --args selinux=0). Conoscere questo comando è utile quando si eredita il server di qualcun altro. Non è la correzione per un errore 403.
I container aggiungono un'etichetta
Su un host della famiglia Red Hat, i processi dei container vengono eseguiti in container_t e possono leggere soltanto i file con etichetta container_file_t. Un bind mount dall'host restituisce Permission denied all'interno del container, mentre ls -l sull'host appare perfettamente normale. Il suffisso :Z indica al runtime di rietichettare il mount:
docker run -d -v /data/appdata:/var/lib/app:Z myimage:Z assegna alla directory un'etichetta valida solo per questo container. :z la assegna per consentirne la condivisione tra container. Se si specifica :Z per una directory utilizzata da altri servizi, il runtime la rietichetta ricorsivamente e questi servizi smettono di funzionare. Assegnare quindi ai container percorsi dedicati. Per il resto, la configurazione corrisponde a quella di qualsiasi altra immagine, descritta in eseguire Docker su un VPS.
Ubuntu e Debian usano AppArmor
Stesso obiettivo, progettazione diversa. AppArmor limita un programma in base al percorso del relativo eseguibile, usando un profilo in /etc/apparmor.d/, invece di applicare etichette ai file sul disco. Non è necessario rietichettare nulla e non esiste alcun restorecon. Inizia da qui:
sudo aa-status
sudo journalctl -k | grep -i apparmorUn rifiuto viene registrato come apparmor="DENIED" operation="open" profile="/usr/sbin/nginx" name="/data/www/index.html" requested_mask="r". Il flusso operativo è analogo: leggi il rifiuto, individua il profilo e modifica la regola. sudo apt install apparmor-utils fornisce aa-complain (modalità permissiva per un singolo profilo) e aa-enforce per ripristinare il profilo. Ubuntu limita un insieme selezionato di servizi inclusi nei pacchetti e lascia gli altri senza restrizioni. Leggi quindi aa-status per verificare quali profili sono effettivamente attivi, senza dare per scontato che lo siano.
Una regola vale per entrambi i sistemi. Quando un servizio segnala Permission denied su qualcosa che sembra configurato correttamente, consulta il log di sicurezza prima di modificare i permessi. I bit dei permessi raramente sono il problema una seconda volta.
FAQ
Perché nginx restituisce 403 quando i permessi dei file sono corretti?
Perché SELinux ha negato la lettura, non a causa dei bit di autorizzazione. Il server Web viene eseguito nel dominio httpd_t e può leggere soltanto i file con un'etichetta adatta ai contenuti Web. Per questo un file con etichetta default_t o admin_home_t viene rifiutato e nginx non ha nulla da servire. Verificalo con sudo ausearch -m AVC -ts recent, che mostra scontext con termine in httpd_t e tcontext con il tipo errato. Quindi registra l'etichetta corretta e applicala: sudo semanage fcontext -a -t httpd_sys_content_t "/data/www(/.*)?" seguito da sudo restorecon -Rv /data/www.
È sicuro eseguire setenforce 0 per far funzionare un servizio?
setenforce 0 è un passaggio diagnostico, non una correzione. Usalo una sola volta per riprodurre il problema, in modo che il log raccolga tutte le negazioni in un'unica esecuzione. Leggile con sudo ausearch -m AVC -ts recent, quindi esegui sudo setenforce 1 e correggi le cause. Un server lasciato in modalità permissive registra tutte le negazioni senza bloccarne alcuna. In questo modo conservi il rumore nei log e perdi la protezione. Se un servizio ha bisogno di maggiore libertà durante l'intervento, esegui sudo semanage permissive -a httpd_t, così il resto del sistema continua a operare in modalità enforcing.
Come posso eseguire un servizio su una porta non standard con SELinux in modalità enforcing?
Aggiungi la porta al tipo a cui il servizio è autorizzato a fare bind. Per un server Web sulla porta 8081: sudo semanage port -a -t http_port_t -p tcp 8081. Per SSH sulla porta 2222: sudo semanage port -a -t ssh_port_t -p tcp 2222. Prima controlla l'elenco corrente con sudo semanage port -l | grep -w http_port_t, perché se una porta è già presente il comando restituisce ValueError: Port tcp/8081 already defined. Senza questo passaggio, il daemon termina all'avvio con bind() ... Permission denied, anche se nessun altro processo occupa la porta.
Ubuntu include SELinux?
No. Ubuntu e Debian distribuiscono AppArmor, che applica un profilo associato al percorso dell'eseguibile, anziché alle etichette dei file. Verificalo con sudo aa-status e cerca le righe apparmor="DENIED" in sudo journalctl -k. Ubuntu confina un insieme selezionato di servizi distribuiti come pacchetti, quindi molti programmi vengono eseguiti senza confinamento per impostazione predefinita. SELinux in modalità enforcing è invece già attivo in Rocky Linux e AlmaLinux, oltre che in Fedora e RHEL.