J’ai mis en place crowdsec sur mon cluster auto-hébergé. C’est diablement efficace, même si ça n’a pas été si simple à mettre en place.
A propos de Crowdsec
J’utilise fail2ban depuis très longtemps, qui m’a protégé de beaucoup d’attaques. Quand je suis allé au FOSDEM 2023, j’y ai vu une présentation de Crowdsec, où ils le définissaient comme un fail2ban amélioré, avec un aspect communautaire en plus.
La promesse de l’outil est de partager des listes d’IP à bannir, à partir des attaques constatées chez chaque utilisateur. Et de savoir détecter tout un tas de patterns d’attaques (appelés scenarios), y compris des CVE récentes.
Crowdsec n’est pas un outil simple: je vous encourage à lire ses principaux concepts avant de démarrer (j’avais cru pouvoir en faire l’économie, mais m’y suis résolu après avoir perdu pas mal de temps à faire des configurations qui ne fonctionnaient pas)
D’autre part, Crowdsec n’est pas un outil conçu spécifiquement pour Kubernetes, ce qui en complique parfois un peu l’utilisation (voir les petits pièges à la fin).
Configuration avec crowdsec-bouncer-traefik-plugin
Pour que Crowdsec bloque des IPs, il faut ce qu’ils appellent un Bouncer installé sur un élément par lequel passerait tout le trafic. Dans mon cas de figure, j’ai choisi l’Ingress de mon cluster k3s, donc Traefik.
J’ai utilisé ce bouncer: https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
C’est un « middleware » au sens Traefik, de type plugin. Il a été conçu pour garder en mémoire des informations, de sorte de ne pas interroger crowdsec lapi à chaque requête http (voir en annexe mon premier essai avec un autre bouncer, qui avait ce défaut).
Et il y a un exemple pour l’utiliser dans kubernetes: https://github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/tree/main/examples/kubernetes
J’ai pourtant eu un peu de mal à trouver comment le faire marcher. Voici ce que j’ai ajouté dans ma conf traefik pour activer ce plugin:
experimental:
plugins:
enabled: true
additionalArguments:
# To enable crowdsec traefik plugin globally
- "--entrypoints.web.http.middlewares=crowdsec-crowdsec-bouncer-traefik-plugin@kubernetescrd"
- "--entrypoints.websecure.http.middlewares=crowdsec-crowdsec-bouncer-traefik-plugin@kubernetescrd"
- "--experimental.plugins.crowdsec-bouncer-traefik-plugin.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
- "--experimental.plugins.crowdsec-bouncer-traefik-plugin.version=v1.2.0"
Et il a fallu déployer ce manifest de middleware Traefik:
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: crowdsec-bouncer-traefik-plugin
namespace: crowdsec
spec:
plugin:
crowdsec-bouncer-traefik-plugin:
crowdsecLapiKey: xxx
enabled: "true"
# I suggest to start in "live" mode for the first tests, then to switch to "stream" mode
crowdsecMode: live
# Useful in stream mode to reduce the risk of Traefik blocking requests if lapi is unavailable
updateIntervalSeconds: 300
crowdsecLapiHost: crowdsec-service.crowdsec.svc.cluster.local:8080
crowdsecLapiScheme: http
# Increase timeout because my lapi pod can sometimes be slow
httpTimeoutSeconds: 30
# Useful only in case there is another reverse-proxy in front of Traefik
#forwardedHeadersTrustedIPs:
# - 192.168.1.0/24
# - 10.0.0.0/8
# We don't want to block local IPs
clientTrustedIPs:
- 192.168.1.0/24
et ce fichier values pour le chart Helm de crowdsec (c’était ma première version, que j’ai enrichie ensuite avec d’autres acquisitions, collections etc):
container_runtime: containerd
agent:
acquisition:
- namespace: kube-system
podName: traefik-*
program: traefik
env:
- name: TZ
value: "Europe/Paris"
- name: PARSERS
value: "crowdsecurity/cri-logs"
# The traefik collection deploys the parser, and many scenarios working on HTTP logs, see https://app.crowdsec.net/hub/author/crowdsecurity/collections/traefik
- name: COLLECTIONS
value: "crowdsecurity/traefik"
# We disable the default whitelist, so that it also detects potential attacks from the local network
- name: DISABLE_PARSERS
value: "crowdsecurity/whitelists"
persistentVolume:
config:
enabled: false
lapi:
dashboard:
enabled: false
env:
- name: TZ
value: "Europe/Paris"
- name: ENROLL_KEY
value: "yyyy"
- name: ENROLL_INSTANCE_NAME
value: "k3s_autohebergement"
- name: ENROLL_TAGS
value: "k8s linux"
persistentVolume:
data:
enabled: true
storageClassName: my-storage-class
config:
enabled: true
storageClassName: my-storage-class
Configuration pour Nextcloud
Peu de temps après la mise en route, j’ai eu des clients Nextcloud (notamment celui intégré dans les smartphones /e/ OS) qui déclenchaient le scenario http-probing de crowdsec, ce qui faisait bannir leurs IP.
La raison était qu’ils parcourent parfois les répertoires synchronisés avec leur compte nextcloud (en webdav), et que certaines de ces requêtes HTTP renvoient une erreur 404. Crowdsec trouve cela louche, l’assimilant à un attaquant qui essaierait de trouver des fichiers php exploitables.
Heureusement, il est possible d’ajouter des whitelists. Au départ, j’avais ajouté la suivante:
name: mossroy/nextcloud-whitelist
description: my custom nextcloud whitelist
whitelist:
reason: do not ban nextcloud clients for browsing on webdav
expression:
- "evt.Parsed.request matches '^/remote.php/dav/.*' and evt.Parsed.verb == 'PROPFIND'"
A noter que la commande « cscli explain » m’a été précieuse pour tester les whitelists. Il faut la lancer depuis un agent crowdsec. Elle permet facilement de tester une ligne de log, et voir comment elle est analysée par crowdsec. Exemple:
cscli explain --log 'x.x.x.x - - [01/Feb/2024:19:09:16 +0100] "PROPFIND /remote.php/dav/calendars/... HTTP/2.0" 404 281 "-" "-" 130334 "websecure-nextcloud-nextcloud-...@kubernetes" "http://10.42.2.43:80" 109ms' --type traefik --verbose
Ca affiche la manière dont il parse la ligne (avec les différents parsers disponibles), et si les whitelists matchent ou pas.
Mais c’est ensuite que je me suis rendu compte qu’il y avait une whitelist toute prête: https://app.crowdsec.net/hub/author/crowdsecurity/configurations/nextcloud-whitelist. Pour l’activer, il suffit d’ajouter crowdsecurity/nextcloud-whitelist
dans la variable d’environnement PARSERS des agents, dans mon values.yaml. C’est bien plus complet que la whitelist que j’avais fait à la main.
Mais surtout, il y a encore plus simple: utiliser la « collection » nextcloud, qui permet d’un coup de déployer la whitelist, un parser spécifique pour le nextcloud.log, et un scenario de détection d’attaque bruteforce sur nextcloud. Il suffit pour cela d’ajouter crowdsecurity/nextcloud
dans la variable d’environnement SCENARIOS des agents (au lieu de tout le reste de ce paragraphe).
NB: avec une installation par défaut de nextcloud sur kubernetes, le fichier nextcloud.log est écrit dans le PV de données, et pas parsé par Crowdsec. J’en parlerai probablement dans un autre article.
Whitelisting pour d’autres applications
J’ai aussi eu des bans pour freshrss et kanboard, en http-probing. Pour eux, il n’y a pas de whitelist existante dans le « hub » de Crowdsec, donc j’ai dû les créer moi-même.
Pour les déployer, je les ai simplement ajoutées dans le values.yaml:
config:
parsers:
s02-enrich:
mossroyfreshrsswhitelist.yaml: |
name: mossroy/freshrss-whitelist
description: my custom freshrss whitelist
whitelist:
reason: do not ban freshrss clients for browsing the API (the authentication is on another path)
expression:
- "evt.Parsed.request matches '^/api/greader.php/reader/api/.*'"
mossroykanboardwhitelist.yaml: |
name: mossroy/kanboard-whitelist
description: my custom kanboard whitelist
whitelist:
reason: do not ban kanboard clients for HTTP 401 on jsonrpc
expression:
- "evt.Parsed.request matches '^/jsonrpc.php$' and evt.Meta.http_status == '401'"
Mode stream
Par défaut, le plugin est en mode « live », c’est-à-dire qu’il interroge le pod lapi avant de décider s’il bloque ou pas. Il garde des informations en cache, ce qui permet de limiter un peu l’overhead de performance.
Mais il supporte aussi le mode « stream », pour lequel Traefik garde en mémoire l’ensemble des informations nécessaires à la prise de décision: c’est donc plus performant. Et il met à jour ces informations depuis le pod lapi au démarrage, puis toutes les minutes (par défaut). Une indisponibilité de ce pod lapi ne bloque plus tout le trafic entrant, sauf si ça tombe au moment de sa mise à jour. Pour limiter ce risque, j’ai ajouté updateIntervalSeconds: 300
dans la configuration du plugin Traefik, pour qu’il mette à jour ses informations toutes les 5 minutes plutôt que toutes les minutes. C’est un compromis: il sera moins réactif sur le blocage d’une attaque ciblée, mais les redémarrages du pod lapi feront moins d’indisponibilité.
Au départ, quand je choisissais ce mode « stream », traefik semblait tout bloquer. La raison était que l’interrogation du pod lapi au démarrage le sollicite beaucoup, notamment niveau consommation mémoire, ce qui dépassait la limite de mémoire fixée au pod. L’augmenter (à 200Mo au lieu de 100Mo) a permis de régler le problème:
lapi:
(...)
resources:
requests:
memory: 100Mi
limits:
memory: 200Mi
Ce chargement initial prend un peu plus de temps au démarrage de Traefik, mais ça reste supportable. Si besoin, il est possible d’activer un cache redis pour éviter ça.
Configuration du profil de remediation, et notifications par e-mail
En surchargeant le fichier profiles.yaml par défaut (via le values.yaml), on peut lui indiquer d’augmenter le temps de ban quand il y a récidive.
C’est aussi là qu’on peut lui demander d’éviter de créer des décisions (et donc d’envoyer des notifications) quand il y a déjà une décision récente sur la même IP. L’intérêt principal (dans mon cas de figure) est d’éviter de recevoir des notifications pour les IPs qui font partie d’une blocklist à laquelle je suis abonné (en particulier la community). Source: https://discourse.crowdsec.net/t/how-to-avoid-getting-alerts-for-ips-on-subscribed-blocklists/1648
D’autre part, pour être prévenu par e-mail, il faut mettre un peu de configuration supplémentaire aussi:
config:
(...)
profiles.yaml: |
name: default_ip_remediation
debug: true
filters:
# The GetDecisionsSinceCount avoids duplicate decisions, especially in the case of IPs already in a blocklist
- Alert.Remediation == true && Alert.GetScope() == "Ip" && GetDecisionsSinceCount(Alert.GetValue(), "2h") < 1
decisions:
decisions:
- type: ban
duration: 4h
# To configure an increasing ban time
duration_expr: Sprintf('%dh', (GetDecisionsCount(Alert.GetValue()) + 1) * 4)
notifications:
- email_default # Set the required email parameters in /etc/crowdsec/notifications/email.yaml before enabling this.
on_success: break
notifications:
email.yaml: |
type: email
name: email_default
log_level: warn
smtp_host: mail.infomaniak.com
smtp_port: 587
smtp_username: 'xxxx'
smtp_password: 'xxxx'
sender_email: xxx@mossroy.fr
receiver_emails:
- mossroy@mossroy.fr
encryption_type: starttls
auth_type: plain
email_subject: CrowdSec Notification
format: | # This template receives list of models.Alert objects
<html><body>
{{range . -}}
{{$alert := . -}}
{{range .Decisions -}}
<p><a href="https://www.whois.com/whois/{{.Value}}">{{.Value}}</a> will get <b>{{.Type}}</b> for next <b>{{.Duration}}</b> for triggering <b>{{.Scenario}}</b> on machine <b>{{$alert.MachineID}}</b>.</p> <p><a href="https://app.crowdsec.net/cti/{{.Value}}">CrowdSec CTI</a></p>
{{end -}}
{{end -}}
</body></html>
Si les emails n’arrivent pas, il faut aller voir les logs du pod lapi. Ca m’a été utile pour mettre au point la configuration SMTP.
Petits pièges
Les scenarios et collections ne font rien tant qu’ils n’ont pas de parser et surtout d’acquisition adequats: sous Kubernetes, il faut déclarer ces acquisitions dans le values.yaml, en plus de l’acquisition de traefik. En précisant à chaque fois le namespace (si besoin avec des étoiles pour en matcher plusieurs), le pod (étoiles en général nécessaires), et le nom du process qui les génère. Dans cet article, je n’ai mis que l’acquisition de Traefik, mais prépare un autre article avec d’autres exemples.
Les parsers/scenarios/collections sont injectés dans les pods agents, pas dans le pod lapi. Donc il peut être piégeux de regarder la configuration depuis le pod lapi, ou de la tester avec cscli explain
par exemple. Il faut faire ça plutôt au niveau des agents.
Les documentations trouvées sur Internet (ou sur le forum de crowdsec) vous invitent parfois à lancer des commandes cscli xxx install
(quand ce sont des docs adaptées à des déploiements hors kubernetes). Mais, avec le chart Helm, il ne faut pas faire comme ça: il faut déclarer l’équivalent dans le values.yaml.
Je pensais avoir un bug en constatant que certaines IPs n’étaient pas bloquées chez moi, alors qu’elles font partie de la community blockist. En fait c’est « normal », cela dépend des scenarios configurés: on ne reçoit que les IPs correspondant aux scenarios activés chez soi. Cf https://docs.crowdsec.net/docs/troubleshooting/#i-receive-few-ips-in-the-community-blocklist
Résultat
Avant la mise en place de Crowdsec, j’utilisais fail2ban, et étais submergé d’attaques bruteforce sur les pods wordpress. Quasiment toutes les IPs que fail2ban me bloquait sont maintenant bloquées nativement à travers la community blocklist. C’est un énorme gain pour se prémunir facilement de la grande majorité de ceux qui scannent tout ce qu’ils peuvent.
Je reçois encore des alertes, mais tant mieux, parce qu’elles sont liées à des patterns d’attaque que fail2ban ne détectait pas auparavant (ex: crowdsecurity/http-admin-interface-probing, crowdsecurity/http-bad-user-agent, crowdsecurity/http-crawl-non_statics).
Je compte aussi ajouter des scenarios custom pour d’autres applicatifs (si besoin, je ferai un autre article)
Une évolution possible serait d’utiliser un bouncer sur mon Turris, qui y bloquerait les IPs au niveau firewall (avant même d’arriver sur Traefik). crowdsec-firewall-bouncer est un package openwrt qui serait à tester pour ça.
Annexe: premier essai infructueux avec crowdsec-traefik-bouncer
Au départ, j’étais parti de cet article pour l’installation: https://www.crowdsec.net/blog/how-to-mitigate-security-threats-with-crowdsec-and-traefik
Dans mon cas de figure où j’avais un reverse-proxy frontal devant l’ingress Traefik (dont je me suis débarassé depuis), il a fallu que j’ajoute la ligne suivante dans l’objet Middleware « traefik-bouncer »:
trustForwardHeader: true
J’ai mis un moment à trouver ce paramètre, sans lequel le bouncer considérait toujours que l’IP de l’utilisateur était celle du reverse-proxy frontal, et ne bloquait jamais rien.
J’ai dû l’ajouter à la main car il n’est pas prévu dans le chart Helm du Bouncer (voir https://github.com/crowdsecurity/helm-charts/blob/main/charts/crowdsec-traefik-bouncer/templates/middleware.yaml)
Et ça a fonctionné! Enfin… oui et non. Ca m’a bien bloqué certaines IP d’attaquants, mais au prix d’une énorme dégradation de performances, qu’on peut voir ci-dessous: chaque requête HTTP entrante était ralentie d’au moins 2 secondes!
Le problème, c’est que, à chaque requête HTTP entrante sur Traefik, il contacte le bouncer, qui contacte lapi. Ce dernier appel prend sur mon cluster entre 150 ms et … plus de 5 secondes! Quand ça dépasse 5 secondes, la requête HTTP entrante est refusée (timeout). Symptôme: https://github.com/fbonalair/traefik-crowdsec-bouncer/issues/42.
J’ai essayé pas mal choses pour améliorer les performances (affinities, node plus puissant), sans succès. Il y avait probablement une raison à cette lenteur, mais j’ai arrêté d’investiguer: de mon point de vue, l’architecture de ce bouncer n’est pas adéquate: bloquer chaque requête entrante pour consulter crowdsec ne me parait pas tenable d’un point de vue performances, et n’est pas conforme à ce qui est dit dans la FAQ de crowdsec. Il y a un ticket (et même 2 PRs) pour mettre en place du cache, mais ça n’a pas bougé depuis septembre 2022… D’ailleurs l’ensemble du code de ce bouncer n’a pas bougé depuis octobre 2022.
C’est probablement la raison pour laquelle ce bouncer n’est pas listé dans les bouncers officiels de crowdsec.
Bref, j’ai laissé tomber. Dommage que crowdsec n’ait pas mis à jour son article de blog pour dire que ce bouncer était obsolète… Et dommage qu’ils le proposent à nouveau dans un article plus récent: https://www.crowdsec.net/blog/enhance-docker-compose-security
Tres interessant effectivement. J’avais vu cela aussi https://reaction.ppom.me
Sinon concernant la protection applicative L7, je préfère basculer sur un module comme modsecurite (ou son remplacent Coraza), pour le coup il est beaucoup puissant et complet, c’est un veritable WAF. Il est opencource et comme Crowdsec il dispose de plugin pour WordPress et Netxcloud pour gérer les faux positifs. Encore merci pour ces infos.
Merci de ton retour, et des suggestions d’alternatives.
Ces 2 outils n’ont a priori pas la dimension communautaire de Crowdsec: partage des IPs suspectes (celles bloquées localement), et abonnement à une liste des IPs à bloquer (celles menant des attaques confirmées plusieurs fois). C’est cette fonctionnalité qui m’a convaincu de tester Crowdsec, qui apporte un vrai plus dans mon contexte: elle m’évite une grosse partie des attaques (celles non ciblées spécifiquement contre moi)
Par contre, il est effectivement possible que les solutions que tu cites soient plus adaptées pour contrer des attaques ciblées, je ne sais pas.
D’autre part, je n’ai rien trouvé pour l’intégration de Reaction dans un cluster kubernetes (probablement dû à la jeunesse de cet outil). Pour Coraza, rien non plus dans leur doc, mais c’est probablement possible d’une manière ou d’une autre.