1,834 25/03/2026 07/04/2026 9 min

Introduzione

Questo scenario capita spesso in produzione: un reverse proxy Nginx pubblica un sito PHP, il backend sta in rete privata e il firewall usa nftables. Dopo un cambio di routing o un tunnel VPN, il sito smette di rispondere solo su alcune richieste, oppure i login falliscono con timeout strani. Il problema non è quasi mai “PHP rotto”. Di solito è un mix di NAT, MTU, DNS e regole troppo larghe o troppo strette.

Qui vediamo una configurazione concreta, pensata per produzione. L’obiettivo è esporre solo 80 e 443 sul proxy, tenere PHP-FPM e il database fuori dalla rete pubblica, e ridurre i rischi con policy esplicite, logging minimo e controlli prima del rilascio.

Warning: se hai già una policy firewall in uso, non applicare queste regole alla cieca. Adatta nomi interfacce, subnet e indirizzi. Un errore su nftables può tagliare fuori SSH e accesso al pannello.

Prerequisiti

  • Server Linux con nftables attivo.
  • Nginx come reverse proxy pubblicato su Internet.
  • Backend PHP-FPM su rete privata, o su una VLAN separata.
  • DNS pubblico già puntato al proxy.
  • Accesso console o IPMI per emergenza.

Note: se usi Plesk, la parte PHP-FPM si configura da Siti web e dominiImpostazioni PHP. Le regole di rete restano però a livello sistema.

Step 1: disegna il flusso reale del traffico

Prima di toccare nftables, chiarisci il percorso. Il client raggiunge il proxy su 443. Il proxy parla con PHP-FPM o con un backend applicativo su rete privata. Il database non deve essere esposto fuori dalla LAN. Se hai una VPN site-to-site, verifica anche la rotta di ritorno.

Uno schema utile in produzione è questo:

Internet → [Nginx reverse proxy] → 10.20.0.10 PHP-FPM/app → 10.20.0.20 DB

Se il backend è su un’altra rete, controlla che il gateway risponda correttamente e che il traffico non esca da un’interfaccia diversa. Un routing asimmetrico genera timeout intermittenti, soprattutto sotto carico.

# Output:

Flusso definito, subnet note, porte minime identificate.

Step 2: imposta una policy nftables da produzione

La base deve essere semplice. Default drop in ingresso e forwarding, accetta solo ciò che serve. Mantieni SSH solo da IP amministrativi o da una VPN. Il resto deve passare dal proxy.

table inet filter {
  set admin_v4 {
    type ipv4_addr
    elements = { 203.0.113.10 }
  }

  chain input {
    type filter hook input priority 0; policy drop;

    iif "lo" accept
    ct state established,related accept

    ip saddr @admin_v4 tcp dport 22 accept
    tcp dport { 80, 443 } accept

    ip protocol icmp accept
    ip6 nexthdr icmpv6 accept
  }

  chain forward {
    type filter hook forward priority 0; policy drop;
  }

  chain output {
    type filter hook output priority 0; policy accept;
  }
}

Questa base limita la superficie. Non apre il database, non apre PHP-FPM, non apre porte debug. Se il backend vive sullo stesso host del proxy, la regola su 80 e 443 resta sufficiente lato pubblico.

Note: in ambiente con IPv6, non dimenticare le regole per ICMPv6. Senza, rompi la discovery MTU e alcune connessioni HTTPS si comportano male.

# Output:

Solo SSH amministrativo e web pubblico risultano raggiungibili.

Step 3: aggiungi NAT e forwarding solo se il backend è separato

Se il proxy inoltra verso un backend privato, il forwarding deve essere esplicito. In molti casi Nginx parla al backend via rete interna e non serve NAT. Se invece devi mascherare l’origine o uscire da una VLAN dedicata, usa regole chiare e tracciabili.

table ip nat {
  chain prerouting {
    type nat hook prerouting priority -100;
  }

  chain postrouting {
    type nat hook postrouting priority 100;
    oifname "eth0" ip saddr 10.20.0.0/24 masquerade
  }
}

Il masquerade va usato solo quando serve davvero. Se puoi instradare con rotta statica, meglio ancora. Meno NAT significa meno debug, meno stato da mantenere e meno sorprese durante i failover.

Se il backend è dietro un router, controlla la rotta di ritorno:

ip route show
ip rule show

# Output:

Il backend risponde al proxy senza traffico asimmetrico.

Step 4: sistema MTU e MSS per evitare timeout HTTPS

Il sintomo classico è questo: homepage ok, login o upload che restano in attesa. Spesso c’è una VPN o un tunnel tra proxy e backend, e l’MTU effettiva è più bassa di quella dell’interfaccia. Se i pacchetti frammentati vengono bloccati, TLS si rompe in modo sporadico.

Una soluzione pragmatica è allineare MTU sul tunnel e, se necessario, fare MSS clamp sul firewall. Questo riduce i problemi senza toccare l’applicazione.

table inet mangle {
  chain forward {
    type filter hook forward priority -150;
    tcp flags syn tcp option maxseg size set rt mtu
  }
}

Se usi WireGuard o una VPN simile, imposta un MTU prudente, ad esempio 1380 o 1420, in base al percorso reale. Testa con ping senza frammentazione verso il backend e verso il proxy.

ping -M do -s 1372 10.20.0.10

# Output:

Reply senza fragmentation needed, oppure errore chiaro se l’MTU è troppo alta.

Step 5: DNS coerente tra pubblico e privato

In produzione spesso il dominio pubblico punta al proxy, ma il backend deve risolvere lo stesso nome verso un IP interno. Se usi un reverse proxy con upstream per nome DNS, un resolver instabile può causare blocchi casuali dopo il reload di Nginx.

Per questo conviene separare bene i record. Il dominio pubblico punta all’IP del proxy. Il nome interno, usato solo in rete privata, punta al backend.

frontend.example.com.   A     198.51.100.20
app.internal.example.   A     10.20.0.10

Nel proxy, evita upstream risolti “a sentimento”. Definisci un resolver affidabile e un timeout ragionevole.

resolver 10.20.0.53 valid=30s ipv6=off;
proxy_connect_timeout 3s;
proxy_read_timeout 30s;

Se il DNS interno viene gestito da un pannello, verifica che i record privati non finiscano nella zona pubblica. Un leak di DNS interno è un problema di sicurezza prima ancora che di networking.

# Output:

Il dominio pubblico e il nome interno risolvono in modo coerente.

Step 6: configura Nginx per il backend PHP in modo sicuro

Il reverse proxy deve passare solo gli header necessari e preservare l’origine reale del client. Se il backend usa PHP-FPM locale, il socket resta non esposto. Se invece il backend è remoto, usa un upstream separato e limita l’accesso con firewall e ACL di rete.

server {
  listen 443 ssl http2;
  server_name frontend.example.com;

  ssl_certificate     /etc/letsencrypt/live/frontend.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/frontend.example.com/privkey.pem;

  location / {
    proxy_pass http://10.20.0.10:8080;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;
    proxy_http_version 1.1;
    proxy_read_timeout 30s;
  }
}

Warning: non fidarti di X-Forwarded-For se il proxy non è l’unico punto di ingresso. Blocca gli accessi diretti al backend dal firewall, non solo in Nginx.

Se il backend è PHP-FPM locale con Plesk o un altro pannello, la via corretta è: vai in Siti web e dominiImpostazioni PHP e verifica handler, socket e limiti di memoria. Poi allinea le regole nftables per consentire solo il traffico necessario.

# Output:

Il proxy inoltra correttamente e mantiene gli header di produzione.

Step 7: limita l’accesso al backend e al database

Il backend non deve essere “raggiungibile perché tanto è interno”. Un host compromesso sulla stessa subnet può provare a parlare con PHP-FPM, Redis o MySQL. Blocca tutto ciò che non è esplicitamente autorizzato.

table inet filter {
  chain input {
    type filter hook input priority 0; policy drop;
    ct state established,related accept
    iif "lo" accept
    ip saddr 10.20.0.0/24 tcp dport 9000 accept
    ip saddr 10.20.0.0/24 tcp dport 3306 accept
  }
}

La regola va adattata al tuo modello. Se PHP-FPM ascolta su socket Unix, la porta 9000 non serve. Se il database è remoto, consenti solo l’IP del backend applicativo e non tutta la subnet.

Usa anche l’autenticazione a livello applicativo. Il firewall non sostituisce password forti, rotazione segreti e privilege separation.

# Output:

Solo i nodi autorizzati parlano con servizi interni.

Step 8: verifica che il routing non si rompa dopo il reload

Un errore comune è la regola che funziona subito ma si spezza al reboot o al reload. Il test deve coprire persistenza, DNS e stato connessioni. Fai almeno questi controlli.

nft list ruleset
ip route get 10.20.0.10
ss -ltnp | grep -E ':(80|443|22)\b'
curl -I https://frontend.example.com

Se il proxy risponde ma l’app no, guarda il backend. Se l’app risponde in locale ma non dal proxy, il problema è quasi sempre firewall o routing.

# Output:

Regole presenti dopo il reload e percorso di rete coerente.

Verifica finale

La verifica finale deve essere pratica. Apri il sito da una rete esterna. Controlla che 80 reindirizzi a 443. Verifica che il backend non sia raggiungibile direttamente dall’esterno con scansione porte. Poi controlla i log del proxy e del firewall.

journalctl -u nftables -n 50 --no-pager
journalctl -u nginx -n 50 --no-pager
ss -ntu state established

Se hai un sistema di monitoraggio, aggiungi una check HTTP sul dominio pubblico e una check TCP verso il backend solo dalla rete interna. È il modo più semplice per accorgerti di una rotta sbagliata prima che lo facciano gli utenti.

Troubleshooting

Errore 1: curl: (28) Connection timed out after 30000 milliseconds

La causa più probabile è MTU errata o rotta di ritorno asimmetrica tra proxy e backend.

Fix:

ip route get 10.20.0.10
ping -M do -s 1372 10.20.0.10
nft add rule inet mangle forward tcp flags syn tcp option maxseg size set rt mtu

# Output:

La connessione torna stabile e il timeout sparisce.

Errore 2: connect() failed (111: Connection refused) while connecting to upstream

Il backend non ascolta sull’IP previsto, oppure il firewall blocca la porta interna.

Fix:

ss -ltnp | grep 8080
nft list ruleset | grep 8080
systemctl restart nginx

# Output:

L’upstream risponde dal proxy e il 502 sparisce.

Errore 3: no route to host

La subnet interna non ha una rotta valida, o il forwarding è disabilitato sul nodo intermedio.

Fix:

sysctl -w net.ipv4.ip_forward=1
ip route replace 10.20.0.0/24 via 10.30.0.1
nft add rule inet filter forward ct state established,related accept

# Output:

Il traffico attraversa il nodo e raggiunge il backend.

Conclusione

In produzione il problema non è “aprire la porta giusta”. Il punto è ridurre il percorso possibile del traffico e rendere prevedibili routing, MTU e DNS. Con nftables restrittivo, proxy pulito e backend non esposto, il sistema diventa molto più facile da mantenere.

Il prossimo passo concreto è salvare questa configurazione in un file versionato, applicarla su un host di staging e fare un test di failover con un reboot completo.