Blindare un server e mettere un WAF davanti alla tua app

Dalla prima distribuzione, erpc worker lascia entrare via SSH solo dal tuo indirizzo e scarta ogni altra connessione in entrata. Una tabella [waf] mette un proxy di filtraggio davanti alla tua API.
Questa pagina descrive ERPC CLI v0.1.0-rc.4, una versione pre-release.

Il firewall

Una distribuzione imposta il firewall del server prima del primo avvio dell'app:
  • SSH solo da firewall.ssh_allow e dall'indirizzo da cui parte la distribuzione;
  • ogni altra connessione in entrata scartata, tranne run.port da firewall.app_allow; per un'app dietro un WAF, al suo posto la porta del WAF, e solo dove il WAF sul server esegue già le regole che [waf] ora definisce (più avanti).
Ogni modifica viene applicata sotto un timer di rollback di 120 secondi e viene mantenuta solo quando una nuova connessione riesce a entrare, così una modifica sbagliata non può chiuderti fuori.
L'allowlist però sopravvive all'indirizzo da cui è stata creata: se il tuo indirizzo di casa cambia, per rientrare passi dal supporto ERPC per un server ERPC, o dalla console del provider per un server tuo. Prima di fidarti di un indirizzo di casa, tieni in firewall.ssh_allow un secondo indirizzo da cui puoi raggiungere il server (un ufficio, un'uscita VPN).
Se il tuo indirizzo cambia, aggiungi tutti gli indirizzi fissi usati dai tuoi amministratori:
bash
erpc worker firewall allow --to ssh 203.0.113.10 198.51.100.7
ComandoCosa fa
erpc worker firewall listMostra chi può raggiungere il server: erpc.toml accanto alla tabella del server
erpc worker firewall allow --to ssh|app <addresses>Lascia entrare gli indirizzi e applica la modifica. SSH è consentito da singoli indirizzi; una rete è accettata solo con --to app
erpc worker firewall deny --to ssh|app <addresses>Smette di lasciarli entrare
erpc worker firewall syncRiporta il firewall del server a ciò che dice erpc.toml

Mettere un WAF davanti alla tua app

Aggiungi una tabella a erpc.toml:
toml
[waf]
port = 8080
erpc worker deploy esegue quindi un reverse proxy HTTP sul server, in un'unità systemd tutta sua (erpc-waf-<app>-<env>): un utente dinamico senza privilegi, senza chiavi, senza secret e senza nulla del worker, limitato a 1 GiB di memoria. Resta in ascolto su waf.port, legge ogni richiesta prima dell'app, rifiuta ciò che le sue regole indicano e inoltra il resto all'app su 127.0.0.1:<run.port>.
  • L'app resta sul loopback. La sua porta non viene mai aperta dall'esterno. erpc worker api e l'health check raggiungono l'app sul loopback, senza passare dal WAF.
  • Chi può connettersi lo decide comunque la lista del firewall, e la porta del WAF è l'unico punto in cui quella lista può dire tutti:
    sh
    erpc worker firewall allow --to app 0.0.0.0/0     # and ::/0
    Se nella lista non c'è nessuno, il WAF è in ascolto e nessuno viene lasciato entrare, e la distribuzione lo segnala.
  • La porta viene aperta solo quando il WAF è in ascolto su di essa, mai prima. Una distribuzione il cui WAF non si avvia fallisce e ne spiega il motivo, con l'app ancora in esecuzione.
  • Quando modifichi [waf], la porta non viene aperta a nessuno in più finché una distribuzione non ha messo in atto le nuove regole.
  • Togliere [waf] da erpc.toml e distribuire rimuove il WAF dal server.

Impostazioni del WAF

Solo port è obbligatoria. Ogni chiave, con il suo valore predefinito:
toml
[waf]
port = 8080          # the public port, 1024 or above; the app keeps [run] port
methods = ["GET", "POST", "PUT", "DELETE"]
allow_paths = []     # every path
deny_paths = []
max_body_bytes = 1048576
max_header_bytes = 16384
rate = { per_second = 50, burst = 100 }
max_connections_per_address = 32
ban = { after = 100, window_secs = 60, for_secs = 600 }
block_probes = true
header_timeout_secs = 10
body_timeout_secs = 30
upstream_timeout_secs = 30
new_connections = { per_second = 50, burst = 100 }
ChiaveCosa faCosa riceve ciò che viene rifiutato
methodsI metodi inoltrati, in maiuscolo (al massimo 16; mai CONNECT)405, con Allow
allow_pathsI percorsi inoltrati; se non ne elenchi nessuno, tutti i percorsi. Un pattern è un singolo percorso, oppure tutto ciò che sta sotto un prefisso quando termina con *: /api/* è /api e tutto ciò che sta sotto. Confrontato con il percorso dopo la decodifica percentuale, distinguendo maiuscole e minuscole404
deny_pathsI percorsi rifiutati, scritti allo stesso modo e controllati per primi403
max_body_bytesIl corpo di richiesta più grande, fino a 64 MiB413
max_header_bytesLa dimensione massima di riga di richiesta e header insieme, da 1024 a 65536431
rateLe richieste al secondo da un indirizzo, e quante insieme prima che il limite si applichi429, con Retry-After
max_connections_per_addressLe connessioni che un indirizzo mantiene contemporaneamenteLa connessione viene chiusa, senza risposta
banDopo after richieste bloccate entro window_secs, l'indirizzo viene rifiutato per for_secs (fino a 30 giorni)Le sue connessioni vengono chiuse, senza risposta
block_probesRifiuta ciò che chiedono gli scanner (/.env, /.git, /wp-login.php e simili)403
header_timeout_secsQuanto tempo ha un client per inviare l'intestazione di una richiesta, da 1 a 120. È anche quanto viene atteso un client che si è fermato a metà di un corpo, o che ha smesso di ricevere la risposta408 per l'intestazione; 400 per un corpo interrotto; la connessione chiusa per una risposta non ricevuta
body_timeout_secsQuanto può durare in tutto il corpo di una richiesta, da 1 a 600, senza contare il tempo in cui il WAF attende l'app408
upstream_timeout_secsQuanto tempo ha l'app per accettare la connessione e iniziare a rispondere, da 1 a 600504; 502 quando l'app non è in ascolto
new_connectionsIl limite proprio del firewall sulle nuove connessioni TCP al secondo da un indirizzo verso la porta del WAF, scartate da nftables prima che il proxy le veda. Mentre l'insieme di indirizzi di nftables è pieno, un indirizzo che non vi compare non viene conteggiato, quindi questo limite non gli si applica finché l'insieme non ha di nuovo spazio; i limiti propri del proxy (rate, max_connections_per_address, ban) continuano ad applicarsiIl pacchetto viene scartato
rate, max_connections_per_address, ban e new_connections si disattivano con false (ban = false), e una chiave omessa assume il suo valore predefinito. erpc worker check dice cosa farà la tabella [waf], e cosa c'è di sbagliato, prima di qualsiasi distribuzione.

Cosa fa il WAF senza bisogno di impostazioni

  • Una richiesta che non si legge come un'unica richiesta HTTP/1 non ambigua riceve 400, e lo stesso vale per un percorso che non è in forma normale.
  • Una richiesta di cambio di protocollo (Upgrade) riceve 501.
  • Ogni rifiuto conta per il ban, e ogni rifiuto chiude la sua connessione.
  • Un indirizzo è un indirizzo IPv4, oppure un /64 IPv6.
  • Il WAF non inoltra gli header della connessione tra il client e il proxy (Connection e gli header che nomina, Keep-Alive, Proxy-Authenticate, Proxy-Authorization, Proxy-Connection, TE, Trailer), Expect, gli header il cui nome contiene un trattino basso, né gli header Forwarded, X-Real-IP e X-Forwarded-Proto inviati dal client stesso. Imposta X-Real-IP sull'indirizzo da cui è arrivata la connessione e aggiunge quell'indirizzo in fondo a X-Forwarded-For; solo l'ultima voce di X-Forwarded-For è dichiarata dal proxy.

Vedere cosa fa il WAF

ComandoMostra
erpc worker statusLa porta del WAF, dove inoltra, da quando, e i conteggi da allora
erpc worker firewall listChi può raggiungere la porta del WAF, e cosa è stato bloccato, per regola
erpc worker logs --wafOgni richiesta bloccata: la regola, lo stato, l'indirizzo del client, il metodo e i primi 200 byte del target
Una richiesta inoltrata non lascia alcuna riga di log, e nessun header o corpo viene mai registrato nel log; la query di una richiesta bloccata sì, fin dove arrivano i 200 byte. Ciò che un client invia non può scrivere sul tuo terminale: il WAF scrive il suo journal in ASCII stampabile.

Cosa non fa il WAF

  • Niente TLS. Parla HTTP/1.1 in chiaro sulla sua porta.
  • Niente HTTP/2: gli risponde 400.
  • Niente WebSocket, né alcun altro Upgrade: 501.
  • Nessuna ispezione dei corpi. Un corpo viene misurato, mai letto. L'elenco delle sonde è breve ed è un filtro contro il rumore, non una garanzia: l'app controlla comunque il proprio input.
  • Nessuna autenticazione. Chi può chiamare l'app spetta all'app deciderlo, e alla lista del firewall.
  • Un indirizzo è un client. I client dietro uno stesso NAT, proxy o CDN condividono un limite di richieste e un ban; un client con molti indirizzi ha altrettanti limiti di richieste.
  • Ban e conteggi vengono dimenticati quando il WAF si riavvia.
  • Ricorda al massimo 8192 indirizzi e mantiene al massimo 1024 connessioni contemporaneamente.
  • È in ascolto solo su IPv4, quindi una voce IPv6 in app_allow per ora non raggiunge nulla.
  • Un percorso viene confrontato come i byte in cui si decodifica. Un'app che legge i percorsi in modo più permissivo (ignorando maiuscole e minuscole, i parametri ;, decodificando due volte) può essere raggiunta con un nome che deny_paths non copre. allow_paths non ha questa lacuna: ciò che non è elencato non viene inoltrato.
  • Un lettore lento viene servito fino alla fine. Un client che continua a ricevere una risposta, per quanto lentamente, viene servito fino alla fine.
  • Un flood può interrompere un upload di grandi dimensioni. Il WAF gira su un solo thread, quindi un flood può interrompere un upload di grandi dimensioni con 408. Un'app che accetta corpi grandi aumenta body_timeout_secs insieme a max_body_bytes.
La guida del worker contiene ogni regola, compreso quando una modifica ad app_allow viene trattenuta fino alla distribuzione successiva.