SELinux e nginx: correggere gli errori 403
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, quasi sempre la causa è SELinux (Security-Enhanced Linux), che rifiuta la lettura. SELinux verifica un secondo insieme di regole dopo il controllo dei permessi normali. Il web server può leggere soltanto i file che hanno un contesto SELinux adatto al contenuto web. Il file ha un contesto diverso, quindi l'apertura fallisce e nginx non ha nulla da inviare.
Controllare il contesto, non soltanto i permessi:
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 quando la policy non lo ha mai classificato. Nessuna regola del web server consente di leggere file con questo tipo. Il log degli errori mostra un normale errore Unix, perciò il problema sembra causato 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 meccanismo di controllo degli accessi obbligatorio, generalmente indicato con 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 contiene l’elenco delle combinazioni consentite di dominio, tipo e azione; tutto ciò che non compare nell’elenco viene negato. Il controllo viene eseguito dopo il controllo Unix tradizionale, quindi i bit di autorizzazione in drwxr-xr-x devono comunque consentire prima l’accesso. Entrambi i livelli devono autorizzarlo.
Un contesto completo contiene quattro campi separati da due punti, ad esempio 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 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 targeted predefinita confina i servizi e lascia indisturbati gli utenti interattivi. Questo aspetto è importante, perché SELinux non sostituisce l’esecuzione dei servizi con utenti dotati del minimo privilegio. Limita le risorse che un servizio può raggiungere dopo una compromissione.
Le tre modalità e quali immagini includono SELinux
sestatus
getenforceEnforcing blocca e registra gli eventi. Permissive consente tutto e registra ciò che avrebbe bloccato. Disabled non carica alcuna policy. getenforce mostra la modalità corrente. sestatus mostra anche la modalità configurata in /etc/selinux/config, cioè quella ripristinata dopo un riavvio.
Rocky Linux, AlmaLinux, Fedora e RHEL includono SELinux in modalità enforcing con la policy targeted. Questa impostazione predefinita comune deriva dalla stessa origine, non è una coincidenza: tutte e quattro le distribuzioni appartengono alla stessa tradizione Red Hat, passata da CentOS prima della comparsa di Rocky Linux e AlmaLinux. La scelta tra le due non cambia nulla per quanto riguarda questa pagina, perché includono la stessa policy e gli stessi strumenti. Di conseguenza, la scelta tra Rocky Linux e AlmaLinux dipende dalle garanzie di compatibilità e dal supporto per CPU meno recenti, non dalle impostazioni di sicurezza predefinite. Ubuntu e Debian includono invece AppArmor, che svolge lo stesso compito con un meccanismo diverso (l'ultima sezione lo illustra). Per questo 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: questo 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 ne hai bisogno 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 contengono tutte le informazioni necessarie. 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, cioè l'etichetta dell'oggetto che il processo ha tentato di utilizzare. 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 alcun risultato, il demone di audit potrebbe non essere in esecuzione. In questo caso, i rifiuti vengono scritti nel ring buffer del kernel:
sudo journalctl -k | grep -i avcOra converti 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 che riconosce: un booleano disattivato, un'etichetta che non corrisponde alla policy oppure l'assenza di qualsiasi regola. sealert analizza l'intero log e stampa un comando suggerito per ogni rifiuto. Considera il suggerimento solo come un'indicazione. La formulazione cambia tra le diverse release e sealert propone talvolta un modulo di policy personalizzato quando la soluzione corretta consiste nel correggere una sola etichetta.
È importante conoscere anche un altro aspetto. La policy contiene regole dontaudit che nascondono i rifiuti considerati innocui. Di conseguenza, un programma può comportarsi in modo anomalo anche se il log resta vuoto. Rendi nuovamente 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
Servono due comandi e l’ordine è importante. semanage fcontext -a registra quale dovrebbe essere l’etichetta di un percorso. restorecon applica ai file su disco il valore predefinito registrato.
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 suo contenuto, come richiesto da una document root. Prima di modificare l’etichetta, verificare quali cambiamenti verrebbero applicati: sudo restorecon -Rvn /data/www mostra le rietichettature previste, perché -n indica che non deve essere eseguita alcuna azione. Dopo un restorecon effettivo, l’etichetta risulta httpd_sys_content_t e l’errore 403 scompare senza riavviare il servizio.
Usare 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. Su un sistema in cui dnf-automatic applica gli aggiornamenti di sicurezza secondo una pianificazione, il ripristino avviene secondo la propria pianificazione e non mentre si è davanti alla macchina. Di conseguenza, il sito può smettere di funzionare ore dopo l’ultima modifica. semanage fcontext è la versione persistente. Elencare ciò che è stato registrato con sudo semanage fcontext -l | grep '^/data'.
Il contenuto che il servizio deve poter modificare richiede un tipo diverso. Usare httpd_sys_rw_content_t per una directory di upload o per una cache e limitarlo a quei percorsi: un sito in sola lettura con un tipo che consente la scrittura 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 conserva l’etichetta esistente di un file. Di conseguenza, 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 in genere è il comportamento desiderato. Invece, cp -a e rsync -X copiano anche le etichette del file sorgente. Un git clone in una nuova directory di primo livello produce default_t. Quando una pagina viene caricata correttamente da /usr/share/nginx/html ma non dalla propria directory, 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 upstreamL'upstream funziona correttamente. Per impostazione predefinita, il dominio httpd_t non è autorizzato ad 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 importante: scrive il valore su disco. Senza -P, la modifica viene persa al riavvio successivo e il servizio funziona solo fino al riavvio della macchina. Verifica con semanage boolean -l | grep httpd_can_network_connect, che visualizza il valore attivo accanto a quello memorizzato.
Quando esiste, preferisci un booleano a una regola scritta manualmente. I booleani fanno parte dei criteri della distribuzione, quindi vengono mantenuti, documentati e sono facili da individuare per chi dovrà intervenire in seguito. getsebool -a elenca tutti i booleani presenti nel sistema.
Fare ascoltare un servizio su una porta non standard
Anche le porte sono associate a etichette. Sposta nginx sulla porta 8081: il servizio non si avvia:
nginx: [emerg] bind() to 0.0.0.0:8081 failed (13: Permission denied)httpd_t può associare le porte a http_port_t, ma 8081 non è tra queste. 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, e aggiungere una porta due volte genera ValueError: Port tcp/8081 already defined. Se la porta appartiene già a un tipo diverso, modificala con semanage port -m -t http_port_t -p tcp 8081 invece di aggiungerla.
Lo stesso comando consente di usare una porta SSH modificata. Bind to port 2222 on 0.0.0.0 failed: Permission denied in journalctl -u sshd indica che 2222 manca da ssh_port_t; esegui quindi 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 saltato quando si segue una guida generica per mettere in sicurezza SSH su un VPS con un'immagine della famiglia Red Hat. SELinux non è un firewall, quindi la porta deve comunque essere aperta: sudo firewall-cmd --permanent --add-port=8081/tcp && sudo firewall-cmd --reload in questo caso, oppure ufw su un'immagine Debian o Ubuntu. Il flag --permanent comporta lo stesso rischio di dimenticare il riavvio previsto da -P applicato a un booleano; inoltre, vale la pena leggere una volta le zone che determinano a quali interfacce si applica una regola in nozioni di base su firewalld per un VPS Rocky o AlmaLinux.
Quando non c'è né un boolean né un'etichetta da modificare
Su un server normale è raro arrivare a questo punto, ed è qui che si possono causare danni. audit2allow può creare un modulo di policy a partire dai dinieghi registrati nel 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 accorgimenti mantengono sicura questa procedura. Filtrare l'input limitandolo all'unico programma che si sta correggendo con -c, perché passare a audit2allow una settimana di dinieghi non correlati concede tutti quei permessi in una sola volta. Non installare mai un modulo creato a partire da un diniego 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 è una correzione
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 lo registra nei log. Il suo valore principale è la completezza. In modalità enforcing, il servizio si arresta al primo diniego. È quindi necessario correggere il problema, riavviare il servizio e incontrare il secondo diniego. In modalità permissiva, l'esecuzione continua e il log raccoglie tutti i dinieghi in un'unica esecuzione. È quindi possibile tornare alla modalità enforcing e correggerli insieme.
setenforce non modifica /etc/selinux/config, quindi un riavvio riporta il sistema alla modalità enforcing. Questo costituisce una rete di sicurezza. Spiega anche perché una "correzione" consistente in setenforce 0 ricompare nel momento peggiore. Se un servizio ha bisogno di maggiore libertà mentre ci lavori, contrassegna quel dominio invece dell'intero sistema: sudo semanage permissive -a httpd_t mantiene 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 riga con un server permanentemente meno protetto. La differenza emerge 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 concesse dall'utente Unix. Senza una policy caricata, lo stesso codice ottiene tutti i privilegi disponibili all'account del servizio.
Anche la disabilitazione comporta un costo successivo. Quando non è caricata alcuna policy, i nuovi file vengono creati senza contesto, quindi il filesystem si desincronizza dalla policy. Quando si riattiva SELinux, è necessario eseguire un relabel completo, altrimenti molti servizi possono non avviarsi contemporaneamente:
sudo fixfiles -F onboot
sudo rebootQuesto scrive /.autorelabel ed esegue il relabel di ogni filesystem durante il boot successivo. Su un disco di grandi dimensioni richiede molto tempo e la console può sembrare bloccata; avvialo quindi quando puoi attendere. Poiché la macchina verrà comunque riavviata, conviene verificare prima quali altri servizi sono già stati contrassegnati per il riavvio. È proprio ciò che segnala needs-restarting dopo che un aggiornamento dnf ha lasciato in memoria vecchi kernel e librerie. In Rocky Linux e AlmaLinux 9 il file di configurazione non disabilita più autonomamente la parte 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'ulteriore 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 non funziona nel container e restituisce Permission denied, mentre ls -l sull'host sembra del tutto normale. Il suffisso :Z indica al runtime di rietichettare il mount:
docker run -d -v /data/appdata:/var/lib/app:Z myimage:Z assegna la directory a questo solo container. :z la assegna per la condivisione tra container. Se punti :Z a una directory utilizzata da altri servizi, il comando rietichetta ricorsivamente quella directory e interrompe il funzionamento dei servizi. Assegna quindi ai container percorsi dedicati. Se il motore non è ancora installato sul sistema, considera che in queste distribuzioni il comando docker spesso corrisponde a podman, che risponde a quel nome. Gli step di installazione per Rocky e AlmaLinux risolvono questo aspetto prima che si presenti. Per il resto, la configurazione segue la stessa procedura di qualsiasi altra immagine, descritta in esecuzione di Docker su un VPS.
Ubuntu e Debian includono 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 visualizzato come apparmor="DENIED" operation="open" profile="/usr/sbin/nginx" name="/data/www/index.html" requested_mask="r". Il flusso di lavoro è 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 lo riporta alla modalità precedente. Ubuntu limita un insieme selezionato di servizi installati tramite pacchetti e lascia gli altri senza confinamento. Leggi quindi aa-status per verificare cosa è realmente attivo, invece di dare per scontato il contrario.
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. Raramente il problema sono i bit, per la seconda volta.
FAQ
Perché nginx restituisce 403 quando i permessi del file sono corretti?
Perché SELinux ha negato la lettura, non a causa dei bit dei permessi. Il server Web viene eseguito nel dominio httpd_t e può leggere soltanto i file con un'etichetta prevista per il contenuto Web. Di conseguenza, un file con etichetta default_t o admin_home_t viene rifiutato e nginx non ha nulla da pubblicare. Verificalo con sudo ausearch -m AVC -ts recent: il comando mostra scontext che termina 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 per riprodurre il problema una sola volta, in modo che il log raccolga tutti i dinieghi in un'unica esecuzione. Leggili con sudo ausearch -m AVC -ts recent, quindi esegui sudo setenforce 1 e correggi le cause. Un server lasciato in modalità permissiva registra ogni diniego e non ne blocca nessuno: si mantiene il rumore e si perde la protezione. Se un servizio necessita temporaneamente di maggiore libertà mentre lavori, esegui sudo semanage permissive -a httpd_t, così il resto della macchina rimane in modalità enforcing.
Come eseguo un servizio su una porta non standard con SELinux in modalità enforcing?
Aggiungi la porta al tipo a cui il servizio è autorizzato ad associarsi. 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. Controlla prima l'elenco corrente con sudo semanage port -l | grep -w http_port_t, perché una porta già presente nell'elenco produce ValueError: Port tcp/8081 already defined. Senza questo passaggio, il demone 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 applica restrizioni a un insieme selezionato di servizi inclusi nei pacchetti, quindi molti programmi vengono eseguiti senza restrizioni per impostazione predefinita. Rocky Linux e AlmaLinux sono le distribuzioni in cui SELinux è attivo in modalità enforcing già dall'installazione, insieme a Fedora e RHEL.