Skip to content

HAProxy Tarpit

Context

Depuis maintenant plusieurs mois, nous sommes très souvent "testés" par des Hackers Ethique sous le format bug bounty. Pour faire très simple, s'ils trouvent une faille de sécurité et qu'ils nous la remontent, ils peuvent avoir une compensation financière de notre part.

Malgré notre politique de Bug Bounty qui est très stricte sur le DOS, nous avions très souvent des périodes où le nombre de connexion augmentait anormalement, sans aucun lien avec le produit. En investiguant les logs de la machine, nous constations facilement que c'est du bug bounty qui était en cause.

Note

Un tarpit est une technique utilisée pour ralentir les attaques de déni de service distribué (DDoS) en rendant la communication avec les machines malveillantes plus lente. Dans HAProxy, cela peut être accompli en utilisant la configuration "tarpit" qui retient temporairement les connexions entrantes d'une adresse IP spécifique. Cela peut aider à réduire la charge sur le serveur en limitant le nombre de requêtes qu'un attaquant peut envoyer simultanément.

La stratégie

Nous utilisons HAProxy community et sommes novices pour la plupart de l'équipe sur cette technologie. Nous avions, bien évidemment, suivi au mieux les recommandations de déploiement et d'observabilité.

C'est d'ailleurs à partir de l'exporter Prometheus d'HAProxy que nous avons pu identifier l'histoire que je vous raconte aujourd'hui.

Malgré notre vigilance et la mise en place de cet exporter, il nous manquait davantage de finesse. Vous pourriez me dire que l'on aurait pu se baser sur les logs HAProxy afin d'avoir plus de détails sur le trafic entrant et vous auriez tout à fait raison. Spoiler alert, nous avons pris une décision tout autre.

Ce qui fonctionnait bien dans l'équipe c'était notre stack de monitoring basé sur Prometheus Thanos. Nous avions beaucoup investi de temps et d'energie pour stabiliser la stack au détriment de celle de logging qui était en friche.

Le plus important était que ce problème de connexion devait être réglé ASAP au regard de la fréquence à laquelle les pics de connexions arrivaient de manière exponentielle.

Notre stratégie fut la suivante :

  1. Développer de l'outillage pour récupérer des métriques clés (nombre d'appels par IP, path rate-limité)
  2. Mettre de l'alerting sur le nombre d'appels par IP et maintenir manuellement une liste de blacklist IP
  3. Définir une stratégie de rate-limiting automatique

1- L'outillage

La première étape a été de déployer un nouveau backend haproxy qui avait pour unique but de porter une stick-table. Cette table était en charge de stocker le nombre d'appel / minute par IP avec un expiration des clés de 24 heures.

Pour cela, on a juste créé un backend avec la configuration suivante :

backend global_ip_monitoring
    stick-table type ip size 1m expire 24h store http_req_rate(60s)

Et dans chacun de nos frontend ajouter l'instruction suivante :

http-request track-sc0 src table global_ip_monitoring

Bingo, avec cela nous étions capable de stocker les informations des IPs directement sur HAProxy et nous pouvions les voir sur le serveur !

watch -n 1 'echo "show table global_ip_monitoring" | socat unix:/var/run/haproxy.sock -'

Pour l'outillage ce fut très simple et rapide de le mettre en place. Un petit script python qui communiquait avec la dataplane API de HAProxy pour extraire les entrées de notre stick table tout en exposant le résultat au format prometheus. En quelque sorte, nous avions développé un mini exporter très naif mais qui faisait le boulot correctement.

Note

Pour plus d'infos sur la data plane API, c'est ici

2- L'alerting

Une fois nos métriques récupérées par Prometheus, il nous restait plus qu'à le brancher à notre alert manager pour pouvoir réagir en cas de potentiel DOS.

Cette partie fut très frustrante et fatiguante pour l'équipe. Et il y avait de quoi: il ne se passait pas une nuit/journée où nous nous faisions alertés.

La meilleure dans tout ça, c'était qu'en cas d'alerte, il fallait :

  • Récupérer l'IP
  • Ajouter une entrée dans notre liste de black-list géré par Terraform
  • Demander une review pour merge la branche / Appliquer le change
  • Merge la branche

Le travail était laborieux. Il était clair pour nous de prendre rapidement une décision pour ne pas laisser trainer ce TOIL et de passer à la phase 3 de notre plan.

3- Le rate-limiting automatique

C'est sûrement la partie sur laquelle nous avons tous le plus appris ( d'où la raison de cet article ).

Lorsque nous avons étudié les possibilités qui s'offraient à nous, nous avions plusieurs choix/solutions :

  • faire un tcp-reject
  • retourner un code HTTP 429
  • retourner un code HTTP 429 avec un tarpit

Le premier choix n'est pas celui que nous avons retenu pour la simple et bonne raison de ne pas trop perturber l'expérience utilisateur et le process de support. En effet, si un client pour une raison X ou Y rentre dans notre politique de rate-limiting, il n'aura pas de retour.

Nous avions le choix entre la solution 2 et la solution 3. Naïvement, on pensait que la solution 3 était la meilleure puisque nous pouvions "contrôler" le rythme du DOS en répondant qu'après un delai de 2-3 secondes un code HTTP 429, ce qui était assez parlant.

Suite à cela, nous avons appliqué un rate-limiting qui ressemblait à celui-ci :

# 1
http-request track-sc0 src table global_ip_monitoring
# 2
timeout tarpit 10s
#3
http-request tarpit deny_status 429 if { sc_http_req_rate(0) gt 60 }

Ce bloc de code veut dire :

  • #1 Je traque et stocke le nombre d'appel par IP en utilisant la table global_ip_monitoring
  • #2 Je répondrais à la requête en cas de tarpit dans 10 secondes
  • #3 Si une IP dépasse 60 appels par IP je réponds une erreur 429 au bout du temps du tarpit ( 10 secondes )

Merveilleux, nous pensions que le problème avait été résolu. Devinez quoi ? C'était vraiment terrible en terme de nombre de connexion.

Pourquoi cela ? Eh bien, la raison est simple et facilement reproductible: HAProxy va laisser la connexion TCP ouverte pendant tout le temps du tarpit. Alors oui, le service derrière ne recevait aucune requête mais par contre notre nombre de connexion ne s'était pas amélioré voire pire, il s'était même légèrement empiré.

Ci-dessous le graph du nombre de connexion en utilisant un mécanisme de tarpit:

Si nous regardions le nombre de socket TCP, nous voyons que cela ne fait que monter pendant le test.

En résumé, avec cette méthode, nous avions aucun gain !

Puis en discutant tous ensemble, on s'est rendu compte que cette décision avait été prise en se basant sur une croyance, une logique, uniquement sans étudier en profondeur les tenants et les aboutissants du tarpit.

Après avoir repris le problème dans le bon sens, on décide de partir sur la solution 2 qui est de retourner un code HTTP 429 le plus rapidement possible, tout simplement.

Et là.... magie ! Tous nos problèmes de connexions avaient disparus. Comme quoi, parfois la solution la plus évidente peut passer à la trappe...

Conclusion

Pour conclure, nous avons tiré trois leçons de cette histoire. La première est que baser ses décisions sur des croyances et non sur des faits peut souvent être préjudiciable. La deuxième, c'est qu'une stratégie de rate-limiting ne doit pas être générique mais doit s'adapter aux produits et aux exigences du métier. Pour finir, certes notre système de rate-limiting répondait au problème de connexion mais il ne résoud pas tous les problèmes possible lors d'un DOS ou même DDOS...

Tip

Pour avoir une vue plus complète des solutions, je recommande cet article