Restreindre l'accès à un serveur et placer un WAF devant votre application

Dès le premier déploiement, erpc worker n'autorise SSH que depuis votre adresse et ignore toute autre connexion entrante. Une table [waf] place un proxy de filtrage devant votre API.
Cette page décrit ERPC CLI v0.1.0-rc.4, une préversion.

Le pare-feu

Un déploiement configure le pare-feu du serveur avant le premier démarrage de l'application :
  • SSH uniquement depuis firewall.ssh_allow et l'adresse d'où provient le déploiement ;
  • toute autre connexion entrante ignorée, sauf run.port depuis firewall.app_allow ; pour une application derrière un WAF, le port du WAF à sa place, et seulement là où le WAF du serveur applique déjà les règles que [waf] définit désormais (plus bas).
Chaque modification s'exécute sous un minuteur de rollback de 120 secondes et n'est conservée qu'une fois qu'une nouvelle connexion parvient à entrer, si bien qu'une modification erronée ne peut pas vous enfermer dehors.
La liste blanche survit toutefois à l'adresse depuis laquelle elle a été créée : si l'adresse de votre domicile change, pour revenir sur le serveur, il faut passer par le support ERPC pour un serveur ERPC, ou par la console du fournisseur pour votre propre serveur. Gardez dans firewall.ssh_allow une seconde adresse depuis laquelle vous pouvez joindre le serveur (un bureau, une sortie VPN) avant de vous fier à celle de votre domicile.
Si votre adresse change, ajoutez toutes les adresses fixes qu'utilisent vos administrateurs :
bash
erpc worker firewall allow --to ssh 203.0.113.10 198.51.100.7
CommandeCe qu'elle fait
erpc worker firewall listIndique qui peut joindre le serveur : erpc.toml en regard de la table du serveur
erpc worker firewall allow --to ssh|app <addresses>Autorise des adresses et applique la modification. Pour SSH, on autorise des adresses individuelles ; un réseau n'est accepté qu'avec --to app
erpc worker firewall deny --to ssh|app <addresses>Ne les laisse plus entrer
erpc worker firewall syncRemet le pare-feu du serveur en conformité avec ce que dit erpc.toml

Placer un WAF devant votre application

Ajoutez une table à erpc.toml :
toml
[waf]
port = 8080
erpc worker deploy exécute alors un proxy inverse HTTP sur le serveur, dans sa propre unité systemd (erpc-waf-<app>-<env>) : un utilisateur dynamique non privilégié, sans clé, sans secret et sans rien qui appartienne au worker, limité à 1 GiB de mémoire. Il écoute sur waf.port, lit chaque requête avant l'application, refuse ce que ses règles désignent et transmet le reste à l'application sur 127.0.0.1:<run.port>.
  • L'application reste sur le loopback. Son propre port n'est jamais ouvert depuis l'extérieur. erpc worker api et le contrôle de santé joignent l'application sur le loopback, sans passer par le WAF.
  • Qui peut se connecter reste défini par la liste du pare-feu, et le port du WAF est le seul endroit où cette liste peut indiquer tout le monde :
    sh
    erpc worker firewall allow --to app 0.0.0.0/0     # and ::/0
    Si personne n'est listé, le WAF écoute mais personne n'est admis, et le déploiement le signale.
  • Le port n'est ouvert qu'une fois que le WAF y écoute, jamais avant. Un déploiement dont le WAF ne démarre pas échoue et en indique la raison, tandis que l'application tourne toujours.
  • Lorsque vous modifiez [waf], le port n'est ouvert à personne d'autre tant qu'un déploiement n'a pas mis en place les nouvelles règles.
  • Retirer [waf] de erpc.toml puis déployer retire le WAF du serveur.

Paramètres du WAF

Seul port est obligatoire. Toutes les clés, avec leur valeur par défaut :
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 }
CléCe qu'elle faitCe que reçoit ce qui est refusé
methodsLes méthodes transmises, en majuscules (16 au maximum ; jamais CONNECT)405, avec Allow
allow_pathsLes chemins transmis ; si aucun n'est listé, tous les chemins le sont. Un motif désigne un chemin, ou tout ce qui se trouve sous un préfixe lorsqu'il se termine par * : /api/* correspond à /api et à tout ce qui se trouve en dessous. Comparé au chemin après décodage-pourcent, en tenant compte de la casse404
deny_pathsLes chemins refusés, écrits de la même façon et vérifiés en premier403
max_body_bytesLa taille maximale du corps d'une requête, jusqu'à 64 MiB413
max_header_bytesLa taille maximale de la ligne de requête et des en-têtes réunis, de 1024 à 65536431
rateLes requêtes par seconde depuis une même adresse, et combien peuvent arriver d'un coup avant que le débit ne s'applique429, avec Retry-After
max_connections_per_addressLes connexions qu'une même adresse maintient à la foisLa connexion fermée, sans réponse
banAprès after requêtes bloquées en window_secs, l'adresse est refusée pendant for_secs (jusqu'à 30 jours)Ses connexions fermées, sans réponse
block_probesRefuse ce que demandent les scanners (/.env, /.git, /wp-login.php et autres)403
header_timeout_secsLe délai dont dispose un client pour envoyer l'en-tête d'une requête, de 1 à 120. C'est aussi le délai pendant lequel on attend un client qui s'est arrêté au milieu d'un corps, ou qui a cessé de lire sa réponse408 pour l'en-tête ; 400 pour un corps interrompu ; la connexion fermée pour une réponse non lue
body_timeout_secsLa durée totale que peut prendre le corps d'une requête, de 1 à 600, sans compter le temps pendant lequel le WAF attend l'application408
upstream_timeout_secsLe délai dont dispose l'application pour accepter la connexion et commencer à répondre, de 1 à 600504 ; 502 lorsque l'application n'écoute pas
new_connectionsLa limite propre au pare-feu sur les nouvelles connexions TCP par seconde depuis une même adresse vers le port du WAF, ignorées par nftables avant que le proxy ne les voie. Tant que l'ensemble d'adresses de nftables est plein, une adresse qui n'y figure pas n'est pas comptée, si bien que cette limite ne s'applique pas à elle tant que l'ensemble n'a pas de nouveau de la place ; les limites propres au proxy (rate, max_connections_per_address, ban) s'appliquent toujoursLe paquet ignoré
rate, max_connections_per_address, ban et new_connections se désactivent avec false (ban = false), et une clé omise prend sa valeur par défaut. erpc worker check indique ce que fera la table [waf], et ce qui ne va pas, avant tout déploiement.

Ce que fait le WAF sans aucun paramètre

  • Une requête qui ne se lit pas comme une seule requête HTTP/1 sans ambiguïté reçoit 400, de même qu'un chemin qui n'est pas sous forme normale.
  • Une demande de changement de protocole (Upgrade) reçoit 501.
  • Chaque refus compte pour le bannissement, et chaque refus met fin à sa connexion.
  • Une adresse est une adresse IPv4, ou un /64 IPv6.
  • Le WAF ne transmet pas les en-têtes de la connexion entre le client et le proxy (Connection et les en-têtes qu'il nomme, Keep-Alive, Proxy-Authenticate, Proxy-Authorization, Proxy-Connection, TE, Trailer), ni Expect, ni les en-têtes dont le nom contient un tiret bas, ni les Forwarded, X-Real-IP et X-Forwarded-Proto fournis par le client lui-même. Il définit X-Real-IP sur l'adresse d'où vient la connexion et ajoute cette adresse à la fin de X-Forwarded-For ; seule la dernière entrée de X-Forwarded-For est attestée par le proxy.

Voir ce que fait le WAF

CommandeAffiche
erpc worker statusLe port du WAF, la destination vers laquelle il transmet, depuis quand, et les compteurs depuis ce moment
erpc worker firewall listQui peut joindre le port du WAF, et ce qui a été bloqué, par règle
erpc worker logs --wafChaque requête bloquée : la règle, le statut, l'adresse du client, la méthode et les 200 premiers octets de la cible
Une requête transmise ne laisse aucune ligne de log, et aucun en-tête ni corps n'est jamais journalisé ; la chaîne de requête d'une requête bloquée l'est, dans la limite de ces 200 octets. Ce qu'envoie un client ne peut pas écrire sur votre terminal : le WAF écrit son journal en ASCII imprimable.

Ce que le WAF ne fait pas

  • Pas de TLS. Il parle HTTP/1.1 en clair sur son port.
  • Pas de HTTP/2 : il y répond par 400.
  • Pas de WebSocket, ni aucun autre Upgrade : 501.
  • Aucun examen de l'intérieur des corps. Un corps est mesuré, jamais lu. La liste des sondes est courte et sert de filtre contre le bruit, pas de garantie : la vérification de ses entrées reste l'affaire de l'application.
  • Pas d'authentification. Décider qui peut appeler l'application revient à l'application, et à la liste du pare-feu.
  • Une adresse est un client. Des clients derrière un même NAT, proxy ou CDN partagent un débit et un bannissement ; un client doté de nombreuses adresses dispose d'autant de débits.
  • Les bannissements et les compteurs sont oubliés lorsque le WAF redémarre.
  • Il garde en mémoire au plus 8192 adresses et maintient au plus 1024 connexions à la fois.
  • Il n'écoute qu'en IPv4, si bien qu'une entrée IPv6 dans app_allow ne mène encore à rien.
  • Un chemin est comparé selon les octets qu'il donne une fois décodé. Une application qui lit les chemins de façon plus souple (en ignorant la casse, les paramètres ;, ou en décodant deux fois) peut être atteinte sous un nom que deny_paths ne couvre pas. allow_paths n'a pas cette faille : ce qui n'est pas listé n'est pas transmis.
  • Un lecteur lent est servi jusqu'au bout. Un client qui continue de recevoir une réponse, aussi lentement que ce soit, est servi jusqu'à la fin.
  • Un afflux de requêtes peut interrompre un gros envoi. Le WAF tourne sur un seul thread, si bien qu'un afflux peut interrompre un gros envoi avec 408. Une application qui accepte des corps volumineux augmente body_timeout_secs en même temps que max_body_bytes.
Le guide du worker contient toutes les règles, y compris le cas où une modification de app_allow est mise en attente jusqu'au prochain déploiement.