Nginx da EGPHP

La porta d'ingresso, costruita come si deve.

Nginx decide cosa arriva al tuo sito: quale server e quale pool PHP rispondono, cosa esce dalla cache e cosa viene respinto già sulla porta. Ecco come lo configura il nostro team, con la configurazione a dimostrarlo.

Funziona su e costruito con
  • NGINX
  • PHP
  • Redis

Una richiesta, dalla porta al tuo codice

La maggior parte delle richieste non ha alcun bisogno di PHP. Ogni tappa qui sotto può rispondere da sola, e solo ciò che resta prosegue verso la successiva.

  1. La connessione

    Cifrata con TLS 1.3, e un'unica connessione porta tutti i file della pagina su HTTP/2 o HTTP/3.

  2. Limiti

    Troppe richieste da un solo indirizzo, troppe connessioni aperte o un corpo più grande di quanto il sito accetti: la risposta arriva qui.

    429 o 413: si ferma qui
  3. Percorsi vietati

    File nascosti, backup e qualsiasi script dentro una cartella di upload vengono rifiutati prima che PHP ne sappia nulla.

    403 o 404: si ferma qui
  4. File statici

    Immagini, fogli di stile e script partono direttamente dal disco, con lunghe durate di cache.

    Servito: si ferma qui
  5. Cache di pagina

    Un visitatore non autenticato riceve una pagina finita dalla memoria.

    Trovata in cache: si ferma qui
  6. Il bilanciatore

    Sceglie il server e il pool PHP che hanno posto per la richiesta.

  7. Il pool dell'account

    PHP gira con l'utente dell'account, dentro le sue cartelle, con il suo php.ini.

Bilanciamento del carico tra server

Quando un server non basta, Nginx si mette davanti a più server e distribuisce il traffico. Il metodo decide chi prende la richiesta successiva.

Un esempio: indirizzi e valori sono illustrativi

Il predefinito. Ogni server prende a turno la richiesta successiva: la scelta giusta quando i server sono simili e le richieste costano più o meno lo stesso.

La richiesta successiva va al server con meno connessioni aperte: la scelta giusta quando alcune richieste durano a lungo, come report, esportazioni e upload.

L'indirizzo del visitatore sceglie il server, così lo stesso visitatore finisce sempre sullo stesso. Serve alle applicazioni che tengono le sessioni sul disco locale; meglio ancora, spostiamo le sessioni su Redis e il vincolo sparisce.

conf.d/app.conf
upstream app_servers {
    # round robin: the default, no line needed    least_conn;    ip_hash;
    server 10.0.0.11:8080 max_fails=3 fail_timeout=30s;
    server 10.0.0.12:8080 max_fails=3 fail_timeout=30s;
    server 10.0.0.13:8080 max_fails=3 fail_timeout=30s;
    server 10.0.0.14:8080 backup;    # no backup server with ip_hash
    keepalive 32;
}

server {
    server_name example.com;
    location / {
        proxy_pass http://app_servers;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_next_upstream error timeout http_502 http_503;
    }
}
max_fails · fail_timeout
Controlli di salute sul traffico reale: dopo tre tentativi falliti in trenta secondi, un server viene escluso per trenta secondi, poi riprovato.
backup
Un server di riserva che riceve traffico solo quando tutti gli altri non sono disponibili.
proxy_next_upstream
Una richiesta che incontra un errore o un timeout viene ritentata sul server successivo. Una richiesta che modifica dati non viene inviata due volte, così un ordine non viene mai registrato due volte.
keepalive
Le connessioni verso i server restano aperte tra una richiesta e l'altra, invece di essere riaperte ogni volta.
weight
A un server più grande si può assegnare una quota maggiore delle richieste.
down
Un server viene tolto per manutenzione con un reload, mentre gli altri reggono il traffico.

Bilanciamento del carico tra pool PHP

La stessa idea dentro un singolo server. Un sito può avere alle spalle più di un pool PHP-FPM, così un pool può riavviarsi mentre l'altro risponde, e il lavoro lento non sottrae mai i worker che servono al negozio.

Nginx ~ \.php$^~ /admin/
shop-a.sock Negozio, pool A Risponde Risponde, e prende anche le richieste di B
shop-b.sock Negozio, pool B Risponde Si riavvia con una nuova impostazione: nessun visitatore deve aspettare
shop-admin.sock Amministrazione e report Worker propri, separati dal negozio
sites/example.com.confUn esempio
upstream php_shop {
    server unix:/run/php/shop-a.sock max_fails=2 fail_timeout=10s;
    server unix:/run/php/shop-b.sock max_fails=2 fail_timeout=10s;
    keepalive 8;
}

# server {}
location ~ \.php$ {
    try_files $uri =404;
    include fastcgi.conf;
    fastcgi_pass php_shop;
    fastcgi_keep_conn on;
    fastcgi_next_upstream error timeout;
}

location ^~ /admin/ {
    location ~ \.php$ {
        try_files $uri =404;
        include fastcgi.conf;
        fastcgi_pass unix:/run/php/shop-admin.sock;
    }
}
  • Due pool, un sito

    Entrambi i pool eseguono lo stesso codice con lo stesso account. Una nuova impostazione PHP arriva a un pool, poi all'altro, e i visitatori continuano a ricevere risposta per tutto il tempo.

  • Subentro automatico

    fastcgi_next_upstream sposta una richiesta sull'altro pool quando il suo socket rifiuta la connessione o va in timeout. Una richiesta che modifica dati non viene inviata due volte.

  • Una corsia per il lavoro lento

    L'area di amministrazione, le esportazioni e i report hanno un pool tutto loro, così un report di un'ora non può togliere i worker di cui un cliente ha bisogno per completare l'acquisto.

Le regole sulla porta

La protezione comincia prima che PHP esegua una sola riga. Queste regole entrano in ogni blocco server che scriviamo, adattate al sito.

sites/example.com.confUn esempio: i valori si fissano sito per sito
# http {}
limit_req_zone  $binary_remote_addr zone=perip:10m rate=10r/s;
limit_req_zone  $binary_remote_addr zone=login:10m rate=5r/m;
limit_conn_zone $binary_remote_addr zone=conns:10m;server {
    listen 443 ssl;
    http2 on;
    server_name example.com;
    limit_req  zone=perip burst=40 nodelay;
    limit_conn conns 30;
    limit_req_status 429;
    location = /wp-login.php {
        limit_req zone=login burst=5 nodelay;
        include fastcgi.conf;
        fastcgi_pass php_shop;
    }    client_max_body_size 64m;
    client_header_timeout 15s;
    client_body_timeout 15s;    location ~ /\.(?!well-known/) { deny all; }
    location ~* \.(?:bak|old|orig|sql|swp)$ { deny all; }    location ~* ^/(?:wp-content/uploads|uploads|files)/.*\.php$ {
        deny all;
    }    location ~ \.php$ {
        try_files $uri =404;
        include fastcgi.conf;
        fastcgi_pass php_shop;
    }    ssl_protocols TLSv1.2 TLSv1.3;
    server_tokens off;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;}
  1. Limiti di frequenza e di connessione

    Ogni indirizzo ha un ritmo costante con margine per un breve picco; oltre quello, Nginx risponde 429 e PHP non viene mai svegliato. La pagina di login ha un limite tutto suo, molto più stretto, che toglie il fiato a chi prova a indovinare le password.

  2. Dimensione del corpo e timeout

    Un upload più grande di quanto il sito accetti viene rifiutato subito, e un client che invia la richiesta molto lentamente viene disconnesso invece di tenere aperta una connessione.

  3. File nascosti e avanzi

    Tutto ciò il cui nome inizia con un punto, come .git o .env, e le copie di backup dei file non vengono mai serviti. L'unica eccezione è la cartella usata per le verifiche dei certificati.

  4. Gli upload non eseguono mai PHP

    Una cartella che accetta file dai visitatori li serve come file. Uno script messo lì viene rifiutato, qualunque sia il suo nome.

  5. Solo un file PHP che esiste davvero

    try_files $uri =404 passa una richiesta a PHP solo se quel file esatto si trova sul disco. Questo chiude la classica trappola disegnata qui sotto.

  6. Header e TLS

    HSTS tiene i browser su HTTPS, gli altri header impediscono che il tipo di una pagina venga indovinato o che la pagina venga inserita in un frame da un altro sito, e il server non dichiara la sua versione. Solo TLS 1.2 e 1.3.

    Come proteggiamo il server stesso

La trappola che apre una riga mancante

/uploads/avatar.jpgUn file vero: un'immagine con codice PHP nascosto dentro /shell.phpNon esiste
Senza la regola

Su una configurazione vecchia o trascurata, PHP risale il percorso all'indietro, trova l'immagine ed esegue il codice che contiene.

Con la regola

Un 404, e non viene eseguito nulla. cgi.fix_pathinfo=0 in PHP e security.limit_extensions nel pool sono la seconda e la terza serratura sulla stessa porta.

Ogni account è una stanza murata

Su un server condiviso da più clienti, un sito non deve mai poterne leggere un altro. Costruiamo quel muro a strati, così nessuna singola impostazione lo regge da sola.

shopL'account
Utente Linux
shop
Pool PHP
/run/php/shop.sock
Cartelle consentite
/home/shop
Impostazioni
pool.d/shop.conf
clinicL'account
Utente Linux
clinic
Pool PHP
/run/php/clinic.sock
Cartelle consentite
/home/clinic
Impostazioni
pool.d/clinic.conf
blogL'account
Utente Linux
blog
Pool PHP
/run/php/blog.sock
Cartelle consentite
/home/blog
Impostazioni
pool.d/blog.conf
Il codice del negozio che prova a raggiungere i file della clinica file_get_contents('/home/clinic/public_html/config.php') Permission deniedopen_basedir restriction in effect
  1. Un utente Linux proprio

    Ogni sito gira con il proprio utente, e ogni cartella home è chiusa agli altri account. È il muro che regge anche se PHP resta fuori dal quadro.

  2. Un pool PHP-FPM proprio

    Un pool per account, raggiungibile solo attraverso il suo socket. Un sito sotto carico o guasto consuma i propri worker, non quelli dei vicini.

  3. open_basedir e include_path

    PHP può aprire file solo dentro le cartelle dell'account, e cerca i file inclusi soltanto lì. Uno script che chiede un percorso al di fuori viene respinto prima ancora di toccare il disco.

  4. File temporanei e sessioni propri

    Gli upload in corso e i file di sessione restano dentro l'account, non in una cartella /tmp condivisa dove qualunque sito potrebbe leggere le sessioni di login di un altro.

  5. Nessuna funzione che al sito non serve

    open_basedir non è una sandbox: non ferma un programma avviato da PHP. Per questo le funzioni che eseguono comandi di shell vengono disattivate account per account, a meno che il sito non ne abbia davvero bisogno.

php/8.3/fpm/pool.d/shop.confUn esempio
[shop]
user  = shop
group = shop
listen = /run/php/shop.sock
listen.owner = www-data
listen.group = www-data
listen.mode  = 0660

pm = ondemand
pm.max_children = 12
pm.process_idle_timeout = 10s

php_admin_value[open_basedir] = /home/shop/public_html:/home/shop/lib:/home/shop/tmp
php_admin_value[include_path] = .:/home/shop/lib
php_admin_value[upload_tmp_dir] = /home/shop/tmp
php_admin_value[sys_temp_dir] = /home/shop/tmp
php_admin_value[session.save_path] = /home/shop/tmp/sessions
php_admin_value[disable_functions] = exec,passthru,shell_exec,system,proc_open,popen
php_admin_flag[allow_url_include] = off

Un php.ini per ogni account

Ogni account ha le sue impostazioni, scritte nel suo pool. Alcune le fissiamo noi, altre il sito può cambiarle, e la differenza sta in una parola.

php_admin_value: fisso

Impostato per l'account. Né ini_set() né un file .user.ini possono cambiarlo.

memory_limit = 256M
max_execution_time = 60
open_basedir = /home/shop/…
disable_functions = exec,…

php_value: un valore di partenza

Il nostro valore predefinito per l'account. Il sito può alzarlo o abbassarlo dove PHP lo consente.

upload_max_filesize = 32M
post_max_size = 32M
max_input_vars = 3000

.user.ini: quello del sito

Un file nella cartella del sito per le impostazioni che gli è permesso cambiare. Per impostazione predefinita PHP lo rilegge ogni cinque minuti.

upload_max_filesize = 64M
post_max_size = 64M
display_errors = Off

Un esempio: i valori si fissano sito per sito

Limiti che devono andare d'accordo

Dimensione degli upload
upload_max_filesizePHP≤ post_max_sizePHP≤ client_max_body_sizeNginx

Se Nginx consente meno, l'upload viene rifiutato con un 413 prima che PHP lo veda; se è PHP a consentire meno, il file arriva e viene buttato via.

Tempo
max_execution_timePHP< request_terminate_timeoutPHP-FPM≤ fastcgi_read_timeoutNginx

PHP dovrebbe fermarsi per primo, con un errore che può registrare; il pool ferma ciò che PHP non è riuscito a fermare; e Nginx aspetta abbastanza da non mostrare un 504 mentre PHP sta ancora lavorando.

La cache, a strati

Ogni strato conserva una cosa diversa, e una richiesta scende solo fin dove serve. Passa da un visitatore all'altro per vedere dove si ferma ciascuno.

  1. Il browserFogli di stile, script e immagini con una versione nel nome, conservati per un anno.
    Ha già i fileHa già i file
  2. Cache di pagina NginxPagine finite, conservate da pochi secondi a qualche minuto.
    Trovata: la pagina parte da quiSaltata: un cookie di login o di carrello rende la pagina personale
  3. OPcachePHP compilato una volta sola e tenuto in memoria.
    Non raggiuntoIl codice gira senza essere riletto né ricompilato
  4. RedisRisposte ripetute del database, e sessioni.
    Non raggiuntoLa maggior parte delle risposte arriva dalla memoria
  5. Il databaseLa fonte di verità.
    Non raggiuntoInterrogato solo per ciò che Redis non aveva
conf.d/cache.confUn esempio: i valori si fissano sito per sito
# http {}
fastcgi_cache_path /var/cache/nginx/shop levels=1:2
                   keys_zone=shop:50m inactive=60m max_size=2g;

map $http_cookie $skip_cache {
    default                     0;
    ~wordpress_logged_in_       1;
    ~woocommerce_items_in_cart  1;
}

# location ~ \.php$ {}
fastcgi_cache shop;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_valid 200 301 10s;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache     $skip_cache;
fastcgi_cache_use_stale updating error timeout http_500 http_503;
fastcgi_cache_background_update on;
fastcgi_cache_lock on;
La chiave
Schema, metodo, host e indirizzo completo, così due pagine diverse non condividono mai una voce.
L'esclusione
Un login o un carrello salta la cache in entrambe le direzioni: la pagina non viene né servita dalla cache né salvata al suo interno. I moduli inviati con POST non finiscono mai in cache.
Copia vecchia durante l'aggiornamento
Quando una voce scade, una sola richiesta la rinnova in background mentre tutti gli altri ricevono ancora la copia precedente. Se PHP fallisce, viene servita l'ultima pagina buona.
Una regola che molti trascurano
Un add_header dentro un blocco location sostituisce tutti gli header impostati più in alto, perciò teniamo gli header di sicurezza in un unico snippet e lo includiamo ovunque si aggiungano header.

Velocità, e il resto di una buona configurazione

Le impostazioni che rendono più veloce ogni sito sul server, e le abitudini che rendono sicura ogni modifica.

Una modifica applicata senza interrompere nulla
$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
$ sudo systemctl reload nginx

La nuova configurazione viene prima verificata; se il test fallisce, non cambia nulla. Al reload, i nuovi worker partono con essa mentre i vecchi finiscono le richieste che hanno in mano, così nessun visitatore viene tagliato fuori.

log_format timed '$remote_addr "$request" $status $body_bytes_sent '
                 'rt=$request_time urt=$upstream_response_time '
                 'cache=$upstream_cache_status';

Ogni riga di log registra il tempo totale, il tempo impiegato da PHP e cosa ha fatto la cache, così una pagina lenta si attribuisce a Nginx, a PHP o alla rete invece di andare a intuito.

http2 on; listen 443 quic;
Un'unica connessione porta l'intera pagina; HTTP/3 si aggiunge dove la build e la rete lo consentono.
gzip · brotli
Il testo viene compresso prima di partire, con Brotli dove il modulo è incluso nella build. Immagini e video non vengono compressi due volte.
sendfile · tcp_nopush
È il kernel stesso a inviare i file dal disco alla rete, in pacchetti pieni.
open_file_cache
Nginx ricorda quali file esistono e dove si trovano, invece di chiederlo al disco a ogni richiesta.
worker_processes auto
Un worker per core del processore, ognuno con molte connessioni in un unico ciclo di eventi.
Cache-Control: immutable
I file con una versione nel nome restano in cache per un anno e non vengono mai più ricontrollati.

Cosa c'è dietro la porta d'ingresso

Raggruppato per compito di ogni parte, tutto deciso nella configurazione di Nginx.

Connessioni

  • Connessioni cifrateTLS 1.3 cifra la connessione e HTTP/2 porta tutti i file di una pagina su un'unica connessione.
  • Limiti di frequenza e dimensioneTroppe richieste, troppe connessioni aperte o un corpo troppo grande ricevono risposta alla porta.

Consegna

  • File staticiImmagini, stili e script partono direttamente dal disco, con lunghi tempi di cache.
  • Cache di paginaUn visitatore non autenticato riceve una pagina finita dalla memoria.
  • Bilanciamento del caricoLe richieste vanno al server e al pool PHP che hanno posto per loro.

Protezione

  • Percorsi vietatiFile nascosti, backup e script nelle cartelle di upload vengono rifiutati prima che PHP ne sappia nulla.
  • Header di sicurezzaTenuti in un solo frammento e inclusi ovunque si aggiungano header.
  • Account isolatiOgni account esegue PHP come proprio utente, dentro le proprie cartelle.

Cambiare la configurazione senza perdere un visitatore

Nginx applica una nuova configurazione sul posto. Ecco come la sua documentazione descrive l'ordine degli eventi.

  1. Testare il file

    Il comando nginx -t legge tutta la configurazione e si ferma al primo errore, prima che qualcosa ci giri sopra.

  2. Inviare il segnale di ricarica

    Il processo master riceve il segnale e controlla la nuova configurazione. Se il controllo fallisce, torna indietro e la vecchia configurazione continua a servire.

  3. Partono nuovi worker

    Se il controllo riesce, il master avvia nuovi processi worker che usano la nuova configurazione.

  4. I vecchi worker finiscono

    I vecchi worker smettono di accettare nuove connessioni e finiscono le richieste che hanno già.

  5. I vecchi worker escono

    Quando l'ultimo visitatore è servito si spengono e restano solo worker con la nuova configurazione.

Documentazione Nginx: controllare nginx

Portaci il sito. Noi costruiremo la sua porta d'ingresso.

Dicci dove gira oggi e di cosa ha bisogno. Prima leggiamo la configurazione attuale, poi proponiamo quella per Nginx e PHP, e prepariamo il preventivo dopo quella conversazione.

MickeyRisponde in pochi secondi