AppArmor per applicazioni PHP: confinamento a livello kernel senza complessità SELinux

AppArmor per applicazioni PHP: confinamento a livello kernel senza complessità SELinux

Un VPS compromesso e la lezione sul confinamento che avrei dovuto imparare prima

A marzo 2025 ho eseguito un hardening d'emergenza su un VPS OVH di un'azienda del settore servizi di consulenza legale con circa 25 dipendenti, dopo che il loro portale clienti Symfony era stato compromesso attraverso una vulnerabilità di deserialize nel pacchetto liuggio/statsd-php-client (che stavano usando come dipendenza legacy di un vecchio modulo di analytics rimosso cinque anni prima ma mai dis-installato). L'attaccante aveva ottenuto esecuzione di codice come utente www-data, aveva letto il .env per le credenziali MySQL, aveva dumpato 18 mesi di dati clienti dal database, e prima di essere scoperto aveva installato una backdoor persistente in /var/www/portale/config/ camuffata da file di configurazione legittimo. Il danno forense è stato contenuto perché il monitoring esterno aveva catturato il traffico di exfiltration in tempo per attivare la risposta, ma il punto rilevante per questa discussione è un altro: una volta che l'attaccante era entrato come www-data, aveva accesso a tutto ciò che www-data poteva leggere o scrivere su tutto il filesystem. Il .env con credenziali era leggibile perché era un file di proprietà www-data. Le backup SQL in /var/backups/mysql/ erano leggibili perché i permessi erano 644. Il codice sorgente era scrivibile perché era di proprietà www-data. Nulla di tutto questo era necessario per il funzionamento del portale - era solo il risultato di configurazioni permissive accumulate negli anni.

Il confinamento a livello kernel è la risposta ingegneristica a questo problema: dichiari esplicitamente cosa un processo può e non può fare sul filesystem, sulla rete, sui system call, e il kernel applica quelle regole indipendentemente da quello che il processo prova a fare. Un PHP-FPM confinato che abbia compromissione applicativa non può leggere file fuori dal suo perimetro autorizzato, non può aprire connessioni TCP verso porte non previste, non può eseguire binari non consentiti. SELinux è lo strumento canonico in ambiente Red Hat ed è potente ma la sua complessità è notevole: la documentazione è vasta, i messaggi di errore criptici, la curva di apprendimento per un team generico è nell'ordine di settimane. AppArmor è l'alternativa nativa su Debian e Ubuntu - meno potente in assoluto ma con una sintassi path-based che è intuitiva al primo sguardo, e documentazione ufficiale compatta nella AppArmor wiki del progetto mantenuta dal team upstream con esempi pratici su apparmor.net. Dopo la compromissione del cliente del settore legale, ho configurato profili AppArmor per PHP-FPM, Nginx e MySQL in due ore di lavoro. Un attacco futuro con lo stesso vettore - esecuzione di codice arbitrario come www-data - non avrebbe potuto leggere /var/backups/ né contattare server esterni non esplicitamente in allowlist.

Il modello mentale di AppArmor: profili path-based che il kernel fa rispettare

AppArmor funziona attraverso profili: file di testo che dichiarano per un binario specifico (identificato dal suo path assoluto) quali file può leggere o scrivere, quali reti può usare, quali capability Linux può invocare, quali altri binari può eseguire. Il kernel Linux con AppArmor abilitato intercetta ogni syscall del processo e verifica contro il profilo corrispondente - se l'operazione non è autorizzata, la syscall fallisce con EACCES o simile, indipendentemente dai permessi Unix standard. Questo è il punto chiave: AppArmor è additivo rispetto ai permessi Unix tradizionali. Un file con permessi 666 rimane leggibile da chiunque secondo il modello Unix, ma se il profilo AppArmor per PHP-FPM non include quel path, PHP-FPM non può leggerlo anche se tecnicamente avrebbe i permessi.

La sintassi di base di un profilo AppArmor è questa, per un esempio minimalista:

# /etc/apparmor.d/usr.sbin.php-fpm8.2
#include <tunables/global>

/usr/sbin/php-fpm8.2 {
    #include <abstractions/base>
    #include <abstractions/php>
    #include <abstractions/nameservice>

    # Capabilities consentite
    capability setuid,
    capability setgid,
    capability chown,

    # Lettura/scrittura file di log
    /var/log/php*fpm.log rw,
    /var/log/php-fpm/ r,
    /var/log/php-fpm/** rw,

    # Lettura configurazione PHP
    /etc/php/8.2/fpm/** r,
    /etc/php/8.2/cli/** r,

    # Accesso alle applicazioni servite
    /var/www/ r,
    /var/www/** r,
    /var/www/*/storage/** rw,
    /var/www/*/bootstrap/cache/** rw,
    /var/www/*/public/uploads/** rw,

    # Socket Unix per comunicazione con Nginx
    /run/php/php8.2-fpm.sock rw,
    /run/php/php8.2-fpm.pid w,

    # Accesso a MySQL via socket Unix
    /var/run/mysqld/mysqld.sock rw,

    # Redis socket se usato
    /var/run/redis/redis.sock rw,

    # Esecuzione di binari consentiti
    /usr/bin/php8.2 ix,
    /usr/bin/cat ix,
    /usr/bin/uname ix,

    # NEGAZIONI ESPLICITE
    deny /etc/shadow r,
    deny /root/** rwkl,
    deny /home/*/.ssh/** rwkl,
    deny /var/backups/** rwkl,
    deny /var/www/**/.env w,
}

La sintassi merita una spiegazione. Ogni riga dopo la parentesi graffa è una regola. I modificatori dopo il path indicano le operazioni consentite: r per read, w per write, x per execute, k per lock, l per link, m per mmap. La sintassi ** è un glob ricorsivo (qualunque file in qualunque sottodirectory), * è non-ricorsivo. Gli ix sul binario significano "execute, inheriting profile" - il processo figlio eredita lo stesso profilo invece di essere non-confinato. Le regole deny sono esplicite: anche se una regola precedente lo consentisse implicitamente (improbabile ma possibile con abstractions condivise), deny vince. I #include importano set di regole pre-definite dalla distribuzione - abstractions/base contiene tutto ciò che un processo di solito ha bisogno per funzionare (lettura di /etc/hosts, /etc/nsswitch.conf, accesso a /dev/urandom), abstractions/php aggiunge percorsi specifici per PHP.

La strategia di rollout: complain mode prima di enforce

Il rischio più grande con AppArmor (o con qualunque sistema di confinamento) è attivarlo in modalità enforce senza prima verificare che il profilo copra tutte le operazioni legittime del processo. Se manca una regola, il processo fallisce in produzione - PHP-FPM non riesce più a leggere un file di sessione, l'applicazione va in errore 500, il telefono del team ops squilla alle 3 del mattino. La strategia corretta è la modalità complain (logging ma non enforcement) che permette di osservare le violazioni reali per una settimana prima di passare a enforce.

La sequenza operativa è questa. Prima, installo AppArmor e userland tools su Debian/Ubuntu:

# Installazione AppArmor base e utility
apt update
apt install apparmor apparmor-utils apparmor-profiles apparmor-profiles-extra

# Verifica che il modulo kernel sia attivo
aa-status
# Output atteso: "apparmor module is loaded"

Poi carico il profilo in modalità complain, in modo che le violazioni finiscano nei log di sistema ma non blocchino il processo:

# Copio il profilo scritto
cp /path/to/usr.sbin.php-fpm8.2 /etc/apparmor.d/

# Carica il profilo in modalita' complain
apparmor_parser -r /etc/apparmor.d/usr.sbin.php-fpm8.2
aa-complain /usr/sbin/php-fpm8.2

# Verifica stato
aa-status | grep php-fpm
# Output atteso: "/usr/sbin/php-fpm8.2" sotto "profiles are in complain mode"

Per una settimana (la finestra che uso di default, ma adattabile al profilo di traffico dell'applicazione) il profilo gira in complain. Le violazioni vengono loggate in /var/log/audit/audit.log (se auditd è installato) o in /var/log/kern.log (altrimenti). Lo strumento aa-logprof legge quei log, propone automaticamente le regole da aggiungere al profilo per coprire le operazioni osservate, e aggiorna il profilo in modo interattivo:

# Analisi iterativa dei log e aggiornamento del profilo
aa-logprof

Questo comando apre una sessione interattiva dove per ogni violazione rilevata propone una regola (spesso con diverse granularità possibili - path esatto, pattern con wildcard, abstraction pre-definita). Il pattern che uso è preferire regole più restrittive quando possibile, usare wildcard solo quando il pattern è chiaramente ricorrente (ad esempio tutte le sessioni PHP in /var/lib/php/sessions/* si possono coprire con una singola regola /var/lib/php/sessions/** rw), e scegliere abstraction pre-definite quando coprono esattamente il caso d'uso (ad esempio abstractions/php per gli accessi standard di PHP).

Sul cliente del settore legale, una settimana di complain mode ha rivelato 47 violazioni uniche che non avevo incluso nel profilo iniziale: accessi a file temporanei in /tmp/ che PHP-FPM usa per upload files, letture di alcuni file di configurazione Symfony che scoprivo essere in path non standard, una chiamata a /usr/bin/exiftool che un modulo di processing immagini invocava per estrarre metadata. Dopo aver aggiornato il profilo, altre 48 ore in complain senza nuove violazioni mi hanno dato la confidenza per passare a enforce.

Stai cercando un Consulente Informatico esperto per introdurre confinamento AppArmor su VPS Linux in produzione con applicazioni PHP, migliorando la postura di sicurezza senza la complessità operativa di SELinux? Nel mio profilo professionale trovi l'esperienza concreta su hardening Linux, confinamento kernel, profili AppArmor calibrati per Nginx, PHP-FPM, MySQL e Redis.

Profili coordinati: Nginx, PHP-FPM e MySQL come insieme

Il confinamento ha senso come architettura olistica - se confini PHP-FPM ma lasci Nginx non confinato, un attaccante che compromette Nginx (meno probabile ma non impossibile) ha ancora accesso completo al sistema. Il pattern che applico è di configurare profili AppArmor coordinati per tutti i processi sensibili del web server. Il profilo Nginx che uso come baseline è questo, abbreviato:

# /etc/apparmor.d/usr.sbin.nginx
#include <tunables/global>

/usr/sbin/nginx {
    #include <abstractions/base>
    #include <abstractions/nameservice>
    #include <abstractions/openssl>

    capability setuid,
    capability setgid,
    capability net_bind_service,

    # Solo lettura per config
    /etc/nginx/ r,
    /etc/nginx/** r,
    /etc/ssl/certs/** r,
    /etc/ssl/private/** r,
    /etc/letsencrypt/** r,

    # Log files
    /var/log/nginx/*.log w,
    /var/run/nginx.pid w,

    # Static files serviti direttamente
    /var/www/*/public/** r,

    # Socket PHP-FPM (solo comunicazione)
    /run/php/php8.2-fpm.sock rw,

    # Cache nginx
    /var/cache/nginx/** rw,

    # Nega accesso a tutto il resto
    deny /etc/shadow r,
    deny /root/** rwkl,
    deny /home/** rwkl,
    deny /var/www/**/storage/** rwkl,
    deny /var/www/**/.env rwkl,
}

Il dettaglio importante è la deny sui path sensibili dell'applicazione PHP: Nginx non ha nessun motivo legittimo per leggere il .env di un'applicazione, né per accedere a /var/www/**/storage/. Se viene compromesso, non può raggiungere quelle risorse. Allo stesso modo, PHP-FPM non ha motivo legittimo di leggere /etc/nginx/ o di contattare socket Nginx; se viene compromesso, non può manomettere la configurazione del reverse proxy.

La segmentazione di responsabilità fra profili è il pattern di principle of least privilege applicato a livello di processo - l'equivalente del pattern di microservizi ma al livello del kernel. Ho discusso un approccio complementare nel mio articolo sulla sicurezza dei container Docker per applicazioni Laravel e Symfony su PMI, che descrive il confinamento a livello container come strato aggiuntivo di difesa. AppArmor e container security sono complementari: il container isola i filesystem e le risorse, AppArmor confina le syscall anche dentro il container. In produzione uso entrambi dove possibile.

Le trappole operative: quando AppArmor rompe la produzione

Nonostante il rollout gradated in complain mode, ci sono scenari specifici dove AppArmor produce problemi in produzione anche dopo il passaggio a enforce. Il primo è quello che chiamo "la syscall nuova": un update dell'applicazione aggiunge una dipendenza che fa operazioni non previste dal profilo. Se il deploy non include la procedura di verifica AppArmor in staging, la nuova versione gira in staging (dove AppArmor è in complain o disattivato) senza problemi, ma in produzione inizia a fallire perché il profilo enforce blocca operazioni legittime nuove.

La mitigazione è duplice. Prima: includere in CI/CD un test che il profilo AppArmor del processo principale non sia regresso dalla versione precedente (nessuna regola rimossa, solo aggiunte). Seconda: abilitare complain mode temporaneamente su staging e su una piccola percentuale di produzione durante i deploy, osservare per 2-4 ore, e solo poi ri-abilitare enforce. Questo pattern di "canary AppArmor" è analogo al canary deployment generale - metti una porzione del traffico a rischio misurato prima di commit totale.

La seconda trappola è quella dei path assoluti hardcoded che cambiano nel tempo. Il profilo sopra ha /usr/sbin/php-fpm8.2 nel path binario. Quando l'OS viene aggiornato da PHP 8.2 a 8.3, il path diventa /usr/sbin/php-fpm8.3 e il profilo diventa inattivo - il nuovo binario gira non-confinato finché qualcuno non aggiorna il profilo. La mitigazione è monitorare il tool aa-unconfined che elenca processi in ascolto di rete che non hanno profilo AppArmor attivo; se PHP-FPM dopo un upgrade appare in quella lista, qualcosa nel deploy è da sistemare.

La terza trappola è il falso senso di sicurezza. AppArmor protegge dagli attacchi dopo che l'attaccante ha ottenuto esecuzione di codice; non previene la vulnerabilità applicativa che ha permesso l'accesso iniziale. È un layer di contenimento, non un sostituto dell'audit di sicurezza per applicazioni PHP legacy e della guida pratica alle vulnerabilità che descrivo nel dettaglio in un articolo dedicato. Le due strategie sono complementari: l'audit applicativo fixes le vulnerabilità note, AppArmor limita l'impatto di vulnerabilità non ancora note (zero-day) se vengono sfruttate.

Monitoring e alerting: sapere quando il confinamento si attiva in produzione

Le violazioni AppArmor in modalità enforce producono log nel kernel audit log. Questi log vanno monitorati attivamente per due ragioni: primo, una violazione indica potenzialmente un tentativo di attacco - un processo PHP-FPM che cerca di leggere /etc/shadow non è un comportamento legittimo, è un attaccante che ha ottenuto esecuzione di codice e sta cercando di escalare; secondo, una violazione può indicare un bug del profilo, dove il profilo è troppo restrittivo e una operazione legittima viene bloccata.

La distinzione fra i due casi si fa ispezionando il contesto: quale processo, quale file, quale syscall, in quale momento, con quale PID parent. Il setup di alerting che installo usa auditd con una regola dedicata che invia al canale #ops-security di Slack ogni violazione AppArmor rilevata:

# /etc/audit/rules.d/apparmor.rules
-w /var/log/audit/audit.log -p wa -k apparmor_log
-a always,exit -F arch=b64 -S all -F msgtype=1400 -k apparmor_deny
-a always,exit -F arch=b64 -S all -F msgtype=1401 -k apparmor_allowed

Un processo python separato legge in tail /var/log/audit/audit.log, filtra per msgtype 1400 (AppArmor DENY), e forwarda alerts su Slack. Il team di ops valuta ogni alert: se è un falso positivo (nuova operazione legittima da aggiungere al profilo), il profilo viene aggiornato; se è un pattern sospetto (PHP-FPM che tenta di leggere path sensibili), parte la procedura di incident response. Nei 14 mesi successivi al rollout di AppArmor sul cliente del settore legale, abbiamo avuto 4 alert: 3 falsi positivi (operazioni legittime che il profilo non copriva su moduli applicativi aggiunti successivamente), 1 alert vero che si è rivelato essere un tentativo di un dipendente della software house che stava facendo penetration test non autorizzato (la situazione è stata risolta con una conversazione, non era un attaccante esterno ma il pattern di rilevazione era coerente). Il tasso di falso positivo del 75% può sembrare alto, ma ognuno dei falsi ha portato a un miglioramento del profilo, e il valore dell'unico vero alert è stato il motivo per cui l'azienda del cliente ha iniziato a richiedere autorizzazione scritta per qualunque attività di testing di sicurezza sul loro perimetro - un cambio di governance che prima non avevano.

Le metriche di rischio ridotto: cosa AppArmor ha effettivamente prevenuto

Quantificare il beneficio di un sistema di confinamento è intrinsecamente difficile - stai misurando quello che non è successo. Ma ci sono due segnali indiretti che uso. Il primo è la blast radius di un ipotetico compromesso applicativo: cosa avrebbe ottenuto un attaccante se avesse compromesso PHP-FPM prima di AppArmor? Risposta: lettura di .env con credenziali, letture di backup, accesso a file di altri utenti del sistema, potenziale installazione di persistenti, esfiltrazione massiva di dati. Cosa otterrebbe lo stesso attaccante dopo AppArmor? Risposta: accesso ai soli file dell'applicazione servita e ai socket espliciti, nessun accesso a .env in scrittura, nessuna esecuzione di binari non in allowlist, nessuna lettura di backup o home directories. La blast radius si è ridotta di un ordine di grandezza misurato in numero di file accessibili.

Il secondo segnale è la compliance evidence verso i clienti enterprise del settore legale che chiedono audit di sicurezza. Nei sei mesi successivi all'implementazione, il cliente ha superato tre audit di fornitura da studi legali di grandi dimensioni, e in tutti e tre l'esistenza di un sistema di confinamento kernel-level è stata esplicitamente apprezzata e citata come "pratica sopra la media per aziende di dimensioni simili". La stessa azienda prima, senza AppArmor, aveva superato audit con qualche osservazione sulla "hardening di sistema" che venivano accettate con "da migliorare nei prossimi 12 mesi". Il percepito esterno di sicurezza cambia quando puoi dimostrare pratiche concrete, non solo politiche scritte.

Se gestisci VPS Linux con applicazioni PHP in produzione, hai una postura di sicurezza di base già ordinata (firewall, fail2ban, patching puntuale) e vuoi introdurre un layer aggiuntivo di difesa che limiti l'impatto di vulnerabilità applicative non ancora note, oppure hai appena subito un incidente di sicurezza e stai valutando misure strutturali per ridurre la blast radius di eventi futuri, contattami per una valutazione: in due giornate di lavoro analizzo il tuo stack applicativo, identifico i processi sensibili che meritano profili AppArmor dedicati, configuro i profili in complain mode con una finestra di osservazione calibrata sul tuo traffico, guido il passaggio a enforce senza downtime, e ti consegno una procedura di manutenzione dei profili integrata con il tuo workflow di deploy. L'intera operazione si completa tipicamente in due settimane di elapsed time, con meno di due giornate-uomo di effort da parte del tuo team interno.

Ultima modifica: