🛡️ Sécurisation de l'accès à Teleport
🛡️ Sécurisation avec Crowdsec
Je vais partir du principe qu'il n'existe pas de serveur Crowdsec déjà installé. L'installation de Crowdsec se fera uniquement avec Docker.
---
networks:
crowdsec:
external: true
services:
crowdsec-agent:
image: crowdsecurity/crowdsec:latest-debian
container_name: crowdsec
networks:
- crowdsec
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./crowdsec:/etc/crowdsec
- ./crowdsec/data:/var/lib/crowdsec/data
# log bind mounts into crowdsec
- /srv/docker/traefik/logs/:/var/log/traefik/:ro
- /srv/docker/teleport/data/log/:/var/log/teleport/:ro
environment:
COLLECTIONS: "crowdsecurity/traefik crowdsecurity/teleport crowdsecurity/http-cve crowdsecurity/http-dos crowdsecurity/base-http-scenarios crowdsecurity/appsec-generic-rules crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-crs"
GID: "${GID-1000}"
TZ: "Europe/Paris"
BOUNCER_KEY_traefik: ga89tRRpn9HDPORyQrvV4KuCngm4UXuaLkIuya/Kc2LwqJFET0xJwH1+hA== # <- Voir ci-dessous pour générer la clé
restart: unless-stopped
openssl rand -base64 43
# Sortie
ga89tRRpn9HDPORyQrvV4KuCngm4UXuaLkIuya/Kc2LwqJFET0xJwH1+hA==
Configuration des "acquis" Crowdsec
listen_addr: 0.0.0.0:7422
appsec_config: crowdsecurity/appsec-default
name: myAppSecComponent
source: appsec
labels:
type: appsec
poll_without_inotify: false
filenames:
- /var/log/traefik/*.log
labels:
type: traefik
🛠️ Configuration de Traefik
Se placer dans le répertoire de travail docker de Traefik.
Configuration du conteneur Traefik
mkdir config/crowdsec
wget -O config/crowdsec/ban.html https://raw.githubusercontent.com/maxlerebourg/crowdsec-bouncer-traefik-plugin/refs/heads/main/examples/custom-ban-page/ban.html
# traefik/compose.yml
---
networks:
traefik:
external: true
crowdsec:
external: true
(...)
services:
traefik:
image: "traefik:v3"
networks:
- traefik
- crowdsec
(...)
volumes:
(...)
# Mapping du fichier ban de crowdsec
- "./traefik/crowdsec/ban.html:/ban.html"
(...)
Redémarrage du conteneur Traefik : docker compose down && docker compose up -d
Une fois relancé, lancez le conteneur crowdsec.
Si on vérifie le bouncer créé précédemment nous verrons que ce dernier est valide sur Crowdsec mais qu'il n'a pas encore communiqué.
docker exec crowdsec cscli bouncers list
---------------------------------------------------------------------
Name IP Address Valid Last API pull Type Version Auth Type
---------------------------------------------------------------------
traefik ✔️ api-key
---------------------------------------------------------------------
Il nous reste donc à configurer le bouncer sur Traefik.
Création du middleware Crowdsec
---
http:
middlewares:
crowdsec:
plugin:
bouncer:
enabled: true
logLevel: DEBUG # Pour vérifier au début le bon fonctionnement avec docker logs traefik -f / Passer à INFO si OK
defaultDecisionSeconds: 60
crowdsecMode: live
crowdsecAppsecEnabled: true # <--- activation du waf appsec
crowdsecAppsecHost: crowdsec:7422
crowdsecAppsecFailureBlock: true
crowdsecAppsecUnreachableBlock: true
crowdsecLapiKey: ga89tRRpn9HDPORyQrvV4KuCngm4UXuaLkIuya/Kc2LwqJFET0xJwH1+hA== # key du bouncer
crowdsecLapiHost: crowdsec:8080
crowdsecLapiScheme: http
crowdsecLapiTLSInsecureVerify: false
banHTMLFilePath: ./ban.html # Ce fichier sera mappé dans la stack de Traefik
🛠️ Configuration du middleware sur Teleport
Modifiez la configuration du compose.yml de Teleport en ajoutant la ligne traefik.http.routers.teleport-prod-https.middlewares
---
---
networks:
traefik:
external: true
services:
teleport:
image: public.ecr.aws/gravitational/teleport-distroless:17.4.2
container_name: teleport
volumes:
- ./config:/etc/teleport
- ./data:/var/lib/teleport
labels:
- "traefik.enable=true"
- "traefik.http.services.teleport-prod.loadbalancer.server.port=3080"
- "traefik.http.services.teleport-prod.loadbalancer.server.scheme=https"
- "traefik.http.routers.teleport-prod-https.entrypoints=websecure"
- "traefik.http.routers.teleport-prod-https.rule=Host(`teleport.prod-1.domaine.fr`) || HostRegexp(`^.+.teleport.prod-1.domaine.fr$`)"
- "traefik.http.routers.teleport-prod-https.middlewares=crowdsec@file"
- "traefik.http.routers.teleport-prod-https.tls=true"
- "traefik.http.routers.teleport-prod-https.tls.certresolver=letsencrypt"
- "traefik.http.routers.teleport-prod-https.tls.domains[0].main=teleport.prod-1.domaine.fr"
- "traefik.http.routers.teleport-prod-https.tls.domains[0].sans=*.teleport.prod-1.domaine.fr"
networks:
- traefik
restart: unless-stopped
Redémarrage du conteneur : docker compose down && docker compose up -d
📝 Vérifications
En utilisant la commande suivante vous devez voir la dernière connexion du bouncer traefik à Crowdsec:
docker exec crowdsec cscli bouncer list
------------------------------------------------------------------------------------------------------------------
Name IP Address Valid Last API pull Type Version Auth Type
------------------------------------------------------------------------------------------------------------------
traefik@172.20.0.2 172.20.0.2 ✔️ 2025-04-12T12:28:03Z Crowdsec-Bouncer-Traefik-Plugin 1.X.X api-key
------------------------------------------------------------------------------------------------------------------

Et une alerte dans les logs de Crowdsec (docker logs crowdsec -f
) :
time="2025-04-12T15:11:06+02:00" level=info msg="AppSec block: crowdsecurity/vpatch-env-access from 192.168.1.100 (172.20.0.2)"
time="2025-04-12T15:11:07+02:00" level=info msg="(localhost) alert : crowdsecurity/vpatch-env-access by ip 192.168.1.100 (/0)"
time="2025-04-12T15:11:07+02:00" level=info msg="127.0.0.1 - [Sat, 12 Apr 2025 15:11:07 CEST] \"POST /v1/alerts HTTP/1.1 201 666.631104ms \"crowdsec/v1.6.8-f209766e-docker\" \""
time="2025-04-12T15:11:13+02:00" level=info msg="Signal push: 1 signals to push"
🐞 Bug
Le parser Teleport fourni par Crowdsec ne semble pas fonctionner correctement avec la version Docker de Teleport comme visible dans les logs de Crowdsec remontés par la partie agent de ce dernier :
time="2025-04-12T15:15:30+02:00" level=warning msg="failed to run RunTimeValue : interface conversion: interface {} is nil, not bool (1:1)\n | evt.Unmarshaled.teleport.success ? 'true' : 'false'\n | ^" id=winter-paper name=crowdsecurity/teleport-logs stage=s01-parse
time="2025-04-12T15:15:30+02:00" level=warning msg="failed to run RunTimeValue : interface conversion: interface {} is nil, not bool (1:1)\n | evt.Unmarshaled.teleport.success ? 'auth_success' : 'auth_failed'\n | ^" id=winter-paper name=crowdsecurity/teleport-logs stage=s01-parse
time="2025-04-12T15:15:30+02:00" level=warning msg="failed to run RunTimeValue : interface conversion: interface {} is nil, not string (1:1)\n | Split(evt.Unmarshaled.teleport[\"addr.remote\"], ':')[0]\n | ^" id=winter-paper name=crowdsecurity/teleport-logs stage=s01-parse
🏁 Conclusion
Avec le bug du parser teleport de Crowdsec, nous perdons le bénéfice de la détection même si nous conservons tout de même le bénéfice de la prévention par le biais de la blacklist communautaire de Crowdsec et de son mode WAF avec AppSec. Il me faut creuser la question (mauvaise config, parser non adapté...?)