Hardening Laravel e Symfony: checklist NIS2-ready per PMI
A settembre 2025 un cliente emiliano - PMI manifatturiera con 45 dipendenti e un gestionale Laravel 10 che coordina produzione, magazzino e fatturazione - mi ha chiesto un audit di sicurezza perché il loro principale cliente enterprise aveva richiesto evidenze di conformità NIS2 come condizione per il rinnovo del contratto di fornitura. La Direttiva NIS2, entrata in vigore in Italia ad ottobre 2024, non si applica direttamente a PMI sotto i 50 dipendenti - ma si applica ai loro clienti enterprise, che a cascata richiedono ai fornitori di dimostrare misure di sicurezza proporzionate. Per il mio cliente, "dimostrare" significava avere documentazione verificabile: non bastava dire "siamo sicuri", serviva provare come.
L'audit iniziale ha rivelato un quadro che trovo tipico nelle PMI italiane con applicazioni PHP in produzione: APP_DEBUG=true nel .env di produzione (che espone stack trace, variabili d'ambiente e percorsi di file a chiunque provochi un errore), 7 CVE critiche nelle dipendenze Composer non aggiornate da otto mesi, zero header di sicurezza HTTP (nessun CSP, nessun HSTS, nessun X-Frame-Options), nessun rate limiting sugli endpoint di login e API, logging solo su file locale senza alerting, deploy manuale via SFTP senza nessun gate di qualità, e backup del database solo su disco locale senza test di ripristino. Era un'applicazione funzionante, utilizzata ogni giorno, che generava fatturato - ma esposta a rischi che un singolo attaccante moderatamente capace avrebbe potuto sfruttare in poche ore.
In 14 giorni lavorativi ho portato quell'applicazione da "rischio latente" a "NIS2-ready operativo", producendo evidenze documentali per ogni intervento. In questo articolo ti racconto la checklist che ho usato - la stessa che applico a ogni sprint di hardening su applicazioni Laravel e Symfony.
Stai cercando un Consulente Informatico esperto per mettere in sicurezza le tue applicazioni PHP? Nel mio profilo professionale trovi l'esperienza concreta su hardening Laravel, Symfony e compliance NIS2. Contattami per una consulenza diretta.
Che cos'è l'hardening NIS2-ready e perché serve anche alle PMI non direttamente in scope?
L'hardening NIS2-ready è un insieme di misure tecniche e organizzative che riduce la superficie d'attacco dell'applicazione e produce prove verificabili di ogni controllo implementato. L'articolo 21 della NIS2 richiede dieci categorie di misure: policy di sicurezza, gestione degli incidenti, continuità operativa, supply chain security, gestione delle vulnerabilità, crittografia, controllo degli accessi, autenticazione multi-fattore, comunicazioni sicure e formazione. Per un'applicazione web Laravel o Symfony, queste categorie si traducono in interventi concreti che ho organizzato in sette blocchi distribuiti su 14 giorni.
Il punto cruciale per le PMI è la supply chain: l'articolo 21 richiede che le aziende in scope valutino la sicurezza dei propri fornitori. Un gestionale insicuro usato da un fornitore diventa un rischio per il cliente enterprise - e quel cliente ha l'obbligo legale di mitigare quel rischio, anche chiedendo al fornitore di adeguarsi o sostituirlo. Questo è esattamente ciò che era successo al mio cliente emiliano.
Giorni 1-2: segreti, configurazione e il disastro di APP_DEBUG
Il primo intervento è sempre la configurazione sicura - perché è il fix con il rapporto rischio/effort più alto. APP_DEBUG=true in produzione espone l'intera configurazione dell'applicazione a chiunque provochi un errore 500, incluse le credenziali del database, le API key di terze parti e il percorso del filesystem. È l'equivalente di lasciare la cassaforte aperta con un cartello "guardare dentro".
<?php
// .env di produzione - VERIFICARE SEMPRE questi tre parametri
// APP_DEBUG DEVE essere false in produzione
// APP_ENV DEVE essere "production"
// APP_KEY DEVE essere generata e mai committata nel repository
APP_DEBUG=false
APP_ENV=production
APP_KEY=base64:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXOltre a APP_DEBUG, verifico che nessun segreto sia hardcodato nel codice sorgente. La ricerca è semplice:
# Cercare credenziali hardcodate nel codice
grep -rn "password\|api_key\|secret\|token" app/ config/ --include="*.php" | grep -v "env(" | grep -v ".env"Per Symfony, il componente Secrets Management (bin/console secrets:set) gestisce i segreti in modo cifrato - ogni segreto è cifrato con una chiave che varia per ambiente, e il vault cifrato può essere committato nel repository senza rischi. Su Laravel, la best practice è env() per tutto e .env nel .gitignore.
Giorni 3-4: dipendenze e supply chain security
Il secondo blocco è l'audit delle dipendenze - Composer per PHP, npm per il frontend. Le CVE nelle dipendenze sono il vettore di attacco più sfruttato su applicazioni web dopo le misconfigurazioni, e la NIS2 richiede esplicitamente la gestione della supply chain software.
# Audit dipendenze PHP
composer audit --no-interaction --locked
# Output: Found 7 security vulnerability advisories affecting 5 packages
# Audit dipendenze frontend
npm audit --omit=dev --audit-level=highNel caso emiliano, delle 7 CVE critiche, 4 erano in laravel/framework (risolte aggiornando da 10.28 a 10.48), 2 in guzzlehttp/guzzle e 1 in league/flysystem. L'aggiornamento è stato incrementale - una dipendenza alla volta, con test dopo ogni composer update - perché un aggiornamento bulk che rompe qualcosa è peggio delle CVE che stai cercando di risolvere.
Il gate permanente che installo nella CI:
# GitHub Actions: security gate
- name: Composer audit
run: composer audit --no-interaction --locked
# Il job FALLISCE se ci sono CVE critiche o altePer un approfondimento sulla sicurezza della supply chain Composer - inclusi typosquatting, dependency confusion e policy allow-plugins - ho scritto un articolo dedicato alla supply chain security per Laravel e Symfony.
Giorni 5-6: header HTTP di sicurezza
Gli header HTTP sono la prima linea di difesa contro intere classi di attacchi - XSS, clickjacking, MIME sniffing, downgrade HTTPS. La configurazione che applico a livello di Nginx (preferisco gli header a livello di reverse proxy anziché nel middleware applicativo, perché coprono anche gli asset statici e le pagine di errore):
# /etc/nginx/conf.d/security-headers.conf
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), camera=(), microphone=()" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;La Content-Security-Policy (CSP) è la più complessa e la più potente. La configuro sempre in modalità Content-Security-Policy-Report-Only per le prime due settimane, monitorando i report per capire quali risorse legittime vengono bloccate, e poi la passo a enforcement. Una CSP mal configurata che blocca risorse legittime può rompere l'applicazione - e un'applicazione rotta è peggio di un'applicazione senza CSP.
Il riferimento autorevole per la configurazione degli header è l'OWASP Secure Headers Project.
Giorni 7-8: validazione input e rate limiting
Ogni input che entra nell'applicazione - form, API, URL parameter, header, cookie - deve essere validato. Laravel e Symfony hanno meccanismi di validazione eccellenti, ma nella pratica trovo regolarmente endpoint che accettano input non validato - specialmente nelle API interne "che usa solo il frontend".
<?php
// Laravel: Form Request con validazione rigorosa
class StoreOrderRequest extends FormRequest
{
public function rules(): array
{
return [
'product_id' => ['required', 'uuid', 'exists:products,id'],
'quantity' => ['required', 'integer', 'min:1', 'max:999'],
'note' => ['nullable', 'string', 'max:500'],
];
}
}Il rate limiting è la difesa contro brute force e abuse. In Laravel 12 il middleware throttle è configurabile per endpoint:
<?php
// RouteServiceProvider o bootstrap/app.php
RateLimiter::for('login', function (Request $request) {
return Limit::perMinute(5)->by($request->ip());
});
// Nelle rotte
Route::post('/login', LoginController::class)->middleware('throttle:login');Giorni 9-10: logging strutturato e alerting
Passare dal logging "debug su file locale" a un sistema di osservabilità che rilevi anomalie in tempo reale. L'intervento minimo che ho implementato sul cliente emiliano:
<?php
// config/logging.php - canale dedicato security separato dall'applicativo
'channels' => [
'security' => [
'driver' => 'daily',
'path' => storage_path('logs/security.log'),
'level' => 'info',
'days' => 30,
],
],Con alert automatico via email/Telegram su eventi critici (login falliti ripetuti, errori 403/500 anomali, modifiche a utenti admin). Per una guida completa all'osservabilità PHP - inclusi Monolog, canali separati e centralizzazione - ho scritto un articolo dedicato all'osservabilità minima per applicazioni PHP.
Giorni 11-12: CI/CD con security gate
L'obiettivo è impedire che codice insicuro arrivi in produzione. Il gate minimo che installo:
# GitHub Actions: pipeline con gate di sicurezza
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: composer install --no-dev --prefer-dist
- run: composer audit --locked # CVE check
- run: vendor/bin/phpstan analyse -l 5 # analisi statica
- run: vendor/bin/phpunit # test suiteSe uno dei gate fallisce, il deploy è bloccato. L'articolo sulla sicurezza Docker per container PHP estende questo approccio alla supply chain dei container.
Giorni 13-14: backup verificato e documentazione
L'ultimo blocco è il backup testato - non "il backup esiste" ma "ho verificato che il ripristino funziona". Ho eseguito un restore completo su un ambiente di test e misurato il RTO (Recovery Time Objective) reale: 47 minuti dall'inizio del ripristino al gestionale operativo. Quel numero - 47 minuti - è l'evidenza che la NIS2 richiede. Ho documentato la strategia completa di backup per VPS con BorgBackup in un articolo dedicato.
La documentazione prodotta durante i 14 giorni - decision log, report audit dipendenze, screenshot dei gate CI, log di test di restore - è ciò che il cliente ha presentato al suo cliente enterprise come evidenza di conformità. Non è compliance di carta: sono prove di misure operative funzionanti.
Come misuro i risultati e quali KPI presento al management?
I KPI che produco alla fine dei 14 giorni sono quattro, e sono gli stessi che il management può presentare in un audit o al cliente enterprise:
- CVE critiche/alte nelle dipendenze: da 7 a 0 (obiettivo permanente: zero CVE critiche, patch entro 7 giorni dalla disclosure)
- Percentuale endpoint con rate limiting: da 0% al 100% sugli endpoint sensibili (login, API, password reset)
- Header di sicurezza: da 0 a 6 header attivi, verificabili con securityheaders.com (obiettivo: grade A)
- RTO testato: 47 minuti dal trigger al servizio operativo (documentato con timestamp)
Questi numeri sono la differenza tra "diciamo di essere sicuri" e "possiamo dimostrare di essere sicuri". La NIS2 richiede verificabilità - e verificabilità significa numeri, date e prove. Il report che ho prodotto per il cliente emiliano - 12 pagine con screenshot dei gate CI, output degli audit, risultato del test di restore e timeline degli interventi - è stato sufficiente per soddisfare la richiesta del cliente enterprise senza necessità di un audit esterno completo.
Il risultato sul gestionale emiliano
Dopo i 14 giorni, il gestionale era in una condizione radicalmente diversa. APP_DEBUG=false, zero CVE critiche, header di sicurezza con grade A su securityheaders.com, rate limiting su tutti gli endpoint sensibili, logging strutturato con alert su anomalie, pipeline CI/CD con gate che bloccano il deploy su CVE critiche o test falliti, e backup verificato con RTO documentato. Il costo dell'intero sprint è stato una frazione di quanto sarebbe costato un incidente - e il contratto con il cliente enterprise è stato rinnovato senza ulteriori richieste.
Il punto che ripeto sempre ai titolari di PMI: l'hardening non è un costo, è un'assicurazione con ROI misurabile. Il costo di un incidente - tra downtime, ripristino, danni reputazionali e possibili sanzioni NIS2 (fino al 2% del fatturato globale per le entità essenziali, fino all'1.4% per le importanti) - supera di ordini di grandezza il costo di 14 giorni di hardening strutturato. E la NIS2 prevede anche la responsabilità personale dei dirigenti in caso di mancata implementazione delle misure di sicurezza - un aspetto che i titolari di PMI spesso sottovalutano.
Se la tua applicazione Laravel o Symfony è in produzione senza un hardening strutturato, e il tuo cliente o il tuo settore richiede evidenze di sicurezza, la checklist che ho descritto è un punto di partenza realistico. 14 giorni, sette blocchi, outcome verificabili. Se vuoi adattarla alla tua realtà specifica, contattami.