Skip to content

Setup de CrowdSec avec Ingress Nginx chez Scaleway

This content is not available in your language yet.

Aujourd’hui je voulais vous parler d’une application qui est juste incroyable ! CrowdSec !

Présentation rapide, c’est un outil open source qui va vous permettre de vérifier les logs et de vous informer si des tentatives d’attaques sont en cours, mais il peut aussi y remédier ! Vous avez aussi accès à un dashboard web qui va vous permettre de voir les alertes et l’accès à un open CTI, un espace où les IPs sont notées et tagés en fonction de leurs attaques réalisés

Explication en quelques mots : fail2ban moderne avec des plugins 🚀

Ici, je vais donc vous montrer l’installation de nginx ingress puis l’intégration de CrowdSec !

Pour ce tutoriel, j’ai loué une Kapsule chez Scaleway ! DEV1-M, 2 nodes, 30Go de block storage.

Scaleway Kubernetes

Kapsule ready !

Scaleway Kubernetes

Setup Kubernetes

Je vais d’abord faire l’installation basic de l’ingress via helm, cert manager et lancer un déploiement grafana.

Fenêtre de terminal
helm upgrade --install ingress-nginx ingress-nginx \
--repo https://kubernetes.github.io/ingress-nginx \
--namespace ingress-nginx --create-namespace

Une fois l’igress d’installé, on récupère notre ip publique pour faire nos enregistrements DNS

Ingress

Installation de cert manager avec la documentation https://cert-manager.io/docs/installation/helm/

Activation des CRDs (recommander en production)

Fenêtre de terminal
helm repo add jetstack https://charts.jetstack.io
helm repo update
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.2/cert-manager.crds.yaml
helm install \
cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--version v1.16.2

Une fois installé, création d’un cluster issuer qui utilise la méthode de challenge HTTP01

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt
namespace: cert-manager
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: your_email@example.com
privateKeySecretRef:
name: letsencrypt
solvers:
- http01:
ingress:
class: nginx

Apply issuer

Super, maintenant, on va tester ça ! Mon fichier yaml pour Grafana :

apiVersion: v1
kind: Namespace
metadata:
labels:
name: grafana
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: grafana
namespace: grafana
spec:
replicas: 1
selector:
matchLabels:
app: grafana
strategy:
type: Recreate
template:
metadata:
labels:
app: grafana
spec:
securityContext:
fsGroup: 472
supplementalGroups:
- 0
containers:
- name: grafana
image: grafana/grafana:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 3000
name: http-grafana
protocol: TCP
env:
- name: GF_SERVER_ROOT_URL
value: https://grafana.domain.com
resources:
limits:
memory: 1024Mi
cpu: 500m
volumeMounts:
- mountPath: /var/lib/grafana
name: grafana-pv
volumes:
- name: grafana-pv
persistentVolumeClaim:
claimName: grafana-pvc
---
apiVersion: v1
kind: Service
metadata:
name: grafana-svc
namespace: grafana
spec:
ports:
- port: 3000
protocol: TCP
targetPort: http-grafana
selector:
app: grafana
type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: grafana-ingress
namespace: grafana
annotations:
cert-manager.io/cluster-issuer: "letsencrypt"
spec:
tls:
- hosts:
- grafana.domain.com
secretName: letsencrypt-grafana
ingressClassName: nginx
rules:
- host: grafana.domain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: grafana-svc
port:
number: 3000
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: grafana-pvc
namespace: grafana
labels:
app: grafana
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi

Et voici le résultat !

certificate

En quelques minutes nous avons notre ingress + cert manager d’installés !

C’est parti pour préparer l’ingress pour CrowdSec 🛫

Nous allons modifier la configuration de l’ingress pour garder l’adresse ip publique du client, par défaut, c’est les IPs privées à l’intérieures du cluster qui sont dans les logs.

Pour cela, rien de mieux que de ce fier à la documentation de Scaleway !

Using the Proxy protocol v2 with Load Balancer | Scaleway Documentation la partie qui nous intéresse est tout en bas.

controller:
# Do not use Deployment if using `externalTrafficPolicy: "Local"`
kind: DaemonSet
service:
# Your LB IP here if you want (optional)
#loadBalancerIP: "X.X.X.X"
# This means that each node will deliver traffic only to the node-local endpoints of the service,
# without masquerading the client source IP. (Traffic mistakenly sent to a node with no endpoints will be dropped.)
externalTrafficPolicy: "Local"
annotations:
# Configure the Scaleway LB to use Proxy-Protocol
service.beta.kubernetes.io/scw-loadbalancer-proxy-protocol-v2: "true"
service.beta.kubernetes.io/scw-loadbalancer-use-hostname: "true"
config:
# Configure the ingress controller to use Proxy-Protocol
use-forwarded-headers: "true"
compute-full-forwarded-for: "true"
use-proxy-protocol: "true"

Une fois, le fichier crée, on modifie la configuration de l’ingress !

Modifie_Nginx

Sur la console Scaleway, dans la partie Network, Load Balancers, on peut vérifier la configuration

lbchecker

Maintenant on va passer au sérieux, CrowdSec !

Installation de CrowdSec

Avant tout, on va directement utiliser la console web fournis par CrowdSec, je vous laisse donc créer un compte ;)

Petite explication : nous allons déployer un agent sur chaque noeud du cluster (DaemonSet) pour parser les logs des controlleurs nginx. L’agent va analyser la trame réseau et envoyer une alerte à LAPI, lapi lui fera une demande à un bouncer pour savoir si l’ip autoriser à accéder à la ressource.

Voici un schéma que nous pouvons retrouver sur la documentation de CrowdSec :

cschema

Voici une url qui vous sera utile si vous voulez personnaliser votre déploiement helm : https://github.com/crowdsecurity/helm-charts/blob/main/charts/crowdsec/README.md

Ajout du repos helm :

Fenêtre de terminal
helm repo add crowdsec https://crowdsecurity.github.io/helm-charts
helm repo update

Voici mon crowdsec-values.yaml :

container_runtime: containerd
agent:
# To specify each pod you want to process it logs (pods present in the node)
acquisition:
# The namespace where the pod is located
- namespace: ingress-nginx
# The pod name
podName: ingress-nginx-controller-*
# as in crowdsec configuration, we need to specify the program name so the parser will match and parse logs
program: nginx
resources:
limits:
memory: 100Mi
requests:
cpu: 150m
memory: 100Mi
env:
- name: COLLECTIONS
value: "crowdsecurity/nginx"
lapi:
resources:
limits:
memory: 100Mi
requests:
cpu: 150m
memory: 100Mi
persistentVolume:
data:
enabled: true
accessModes:
- ReadWriteOnce
storageClassName: ""
existingClaim: ""
size: 1Gi
config:
enabled: true
accessModes:
- ReadWriteOnce
storageClassName: "scw-bssd"
existingClaim: "lapi-crowdsec-pvc"
size: 1Gi

Avant de déployer le helm, je vais créer un fichier pour créer un volume et l’appliquer.

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: lapi-crowdsec-pvc
namespace: crowdsec
labels:
app: crowdsec
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage 1Gi

On déploie notre CrowSec !

Fenêtre de terminal
helm install crowdsec crowdsec/crowdsec -f crowdsec-values.yaml -n crowdsec

helm install crowdsec

On check nos workloads, tout va bien !

workloads

Une fois le déploiement réalisé, on peut examiner les logs des agents et lapi pour s’assurer que tout est bien interconnectés !

Les agents ont bien chargé les scénarios (CVEs) et sont bien greffés aux logs des controllers nginx

log cs

Maintenant, nous allons connecter notre cluster à la console de management.

Une fois connecter sur le site de CrowdSec, on peut faire un « enroll » d’un agent.

Copier la commander et allez dans le pod LAPI

CAPI

Voila, notre déploiement lapi est lié à la console de management, je vous laisse faire un restart du déploiement pour appliquer la configuration

enroll

Allons accepter l’enroll et vérifier sur la console de management que notre cluster est présent 😎

console accept

engine

À présent, nous allons ajouter un « bouncer » pour ingress nginx.

Le bouncer vous pouvez le voir comme un plugin qui va analyser les logs d’une application spécifique.

Pour activer le bouncer il faut retourner en cli dans le pod lapi et générer un jeton pour ajouter le bouncer.

Fenêtre de terminal
cscli bouncers add ingress-nginx

bouncer

Il nous reste plus qu’à rajouter le bouncer ! Le bouncer va être intégré directement dans le déploiement de l’ingress.

Je vais donc créer un nouveau fichier crowdsec-ingress-bouncer.yaml

controller:
extraVolumes:
- name: crowdsec-bouncer-plugin
emptyDir: {}
extraInitContainers:
- name: init-clone-crowdsec-bouncer
image: crowdsecurity/lua-bouncer-plugin
imagePullPolicy: IfNotPresent
env:
- name: API_URL
value: "http://crowdsec-service.crowdsec.svc.cluster.local:8080"
- name: API_KEY
value: ""
- name: BOUNCER_CONFIG
value: "/crowdsec/crowdsec-bouncer.conf"
command: ['sh', '-c', "sh /docker_start.sh; mkdir -p /lua_plugins/crowdsec/; cp -R /crowdsec/* /lua_plugins/crowdsec/"]
volumeMounts:
- name: crowdsec-bouncer-plugin
mountPath: /lua_plugins
extraVolumeMounts:
- name: crowdsec-bouncer-plugin
mountPath: /etc/nginx/lua/plugins/crowdsec
subPath: crowdsec
config:
plugins: "crowdsec"
lua-shared-dicts: "crowdsec_cache: 100m"

À la ligne 13 on indique le jeton fourni par LAPI.

Update du déploiement helm avec notre bouncer :

Fenêtre de terminal
helm upgrade ingress-nginx ingress-nginx --repo https://kubernetes.github.io/ingress-nginx --namespace ingress-nginx -f ingress-nginx-scw.yaml -f crowdsec-ingress-bouncer.yaml

Une fois le déploiement finalisé, on va vérifier dans les controllers ingress que le bouncer c’est chargé.

lua check

On check que CrowdSec communique avec le bouncer nginx !

bouncer check

Voici un petit lien sur les commandes crowdsec qui vous sera utile :

https://docs.crowdsec.net/docs/cscli/

Pour que tout se synchronise avec la console de management, je vous conseille de faire un restart de lapi et de supprimer vos pods d’agents. Voici le résultat !

Scenario

bouncer added

Nous avons 36 scénarios de Nginx qui se sont chargés ! (CVEs)

Avant de vos montrer le mécanisme du bouncer, voici quelque chose de très puissant ! LES BLOCKLISTS !

Blocklists

blocklist

Personnellement je ban direct les IPs des blocklists 😀

banip

On est bien là ! (Je vois une alerte aussi 🚨👀)

alertedirect

Regardez-moi ça ! Pendant le tutoriel !

hit

Si on va regarder les logs :

On voit que l’ip y accède plusieurs fois, ce n’est pas un problème, mais les trames sont ressemblante à un des scénarios chargés, l’agent alerte LAPI qui lui, va demander la décision au bouncer qui va finalement bannir l’ip. L’alerte est ensuite envoyée à la console de management.

Bon comme il y a une alerte en live, je vais vous montrer comment le tester soit même, mais en plus activer les notifications avec un webhook teams ! Histoire d’apporter une plus-value 😅

Notifications

Nous allons donc ajouter la config pour créer un fichier http.yaml, ici, je vais utiliser les webhooks via Teams. Vous avez bien sûr d’autres solutions, voici la documentation : https://docs.crowdsec.net/docs/notification_plugins/teams

Nous allons aussi rajouter un profil, qui va envoyer la notification quand une IP est bannie.

Voici le fichier de configuration complet :

(Ligne 122 vous pouvez modifier l’url du webhook)

container_runtime: containerd
config:
profiles.yaml:
|
name: default_ip_remediation
debug: true
filters:
- Alert.Remediation == true && Alert.GetScope() == "Ip"
decisions:
- type: ban
duration: 4h
duration_expr: Sprintf('%dh', (GetDecisionsCount(Alert.GetValue()) + 1) * 4)
notifications:
- http_default
on_success: break
notifications:
http.yaml: |
type: http
name: http_default # this must match with the registered plugin in the profile
log_level: debug # Options include: trace, debug, info, warn, error, off
format: |
{
"type": "message",
"attachments": [
{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": {
"$schema": "https://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.2",
"body": [
{{- range . -}}
{{- $decisions_len := len .Decisions -}}
{{- range $index, $element := .Decisions -}}
{
"type": "TextBlock",
"text": "[Info] CrowdSec",
"wrap": true,
"size": "large",
"weight": "bolder",
"fontType": "Default"
},
{
"type": "FactSet",
"facts": [
{
"title": "IP:",
"value": "{{$element.Value}}"
},
{
"title": "Duration:",
"value": "{{$element.Duration}}"
},
{
"title": "Reason:",
"value": "{{$element.Scenario}}"
},
{
"title": "Origin:",
"value": "{{$element.Origin}}"
},
{
"title": "Simulation:",
"value": "{{$element.Simulated}}"
}
]
},
{
"type": "RichTextBlock",
"inlines": [
{
"type": "TextRun",
"text": "\"{{ $element.Value }}\" got a ban for {{ $element.Duration }}."
}
]
},
{
"type": "ActionSet",
"actions": [
{
"type": "Action.OpenUrl",
"title": "Whois",
"url": "https://www.whois.com/whois/{{ $element.Value }}",
"style": "positive"
},
{
"type": "Action.OpenUrl",
"title": "Shodan",
"url": "https://www.shodan.io/host/{{ $element.Value }}",
"style": "positive"
},
{
"type": "Action.OpenUrl",
"title": "AbuseIPDB",
"url": "https://www.abuseipdb.com/check/{{ $element.Value }}",
"style": "positive"
}
]
},
{
"type": "ActionSet",
"actions": [
{
"type": "Action.OpenUrl",
"title": "Unban IP in CAPI",
"url": "https://crowdsec.net/unban-my-ip/",
"style": "positive"
}
]
}
{{- if lt $index (sub $decisions_len 1) -}},
{{- end -}}
{{- end -}}
{{- end -}}
]
}
}
]
}
url: https://webhook.url.com
Content-Type: application/json
agent:
# To specify each pod you want to process it logs (pods present in the node)
acquisition:
# The namespace where the pod is located
- namespace: ingress-nginx
# The pod name
podName: ingress-nginx-controller-*
# as in crowdsec configuration, we need to specify the program name so the parser will match and parse logs
program: nginx
resources:
limits:
memory: 100Mi
requests:
cpu: 150m
memory: 100Mi
env:
- name: COLLECTIONS
value: "crowdsecurity/nginx"
lapi:
resources:
limits:
memory: 100Mi
requests:
cpu: 150m
memory: 100Mi
persistentVolume:
data:
enabled: true
accessModes:
- ReadWriteOnce
storageClassName: ""
existingClaim: ""
size: 1Gi
config:
enabled: true
accessModes:
- ReadWriteOnce
storageClassName: "scw-bssd"
existingClaim: "lapi-crowdsec-pvc"
size: 1Gi

Configuration appliquée :D

updatehelm2

On va aller voir maintenant dans le pod lapi si nous avons bien notre http.yaml ! Allez hop !

lapi check

Scanner web - Nikto

Maintenant, on va tester ça avec nikto ! Je suis un tricheur car je vais swap sur mon macbook, mais si vous êtes sous Windows vous pouvez toujours utiliser un conteneur ubuntu/debian ou WSL pour réaliser votre scan. Voici le lien github du projet : https://github.com/sullo/nikto

J’ai donc cloné le repos, allez dans le dossier program, switch de branch et j’ai lancé un scan avec l’option -h

scan nikto

Le petit résultat sur le groupe Teams 🤌

teams

On peut aller dans le pod lapi est faire quelques commandes cli, comme cscli alerts list ou cscli decisions list.

cscli

cscli2

Si vous voulez retirer votre ip des bans, voici la commande :

Fenêtre de terminal
cscli decisions delete --ip 1.2.3.4

Conclusion

Et voila, ça sera tout pour cet partie !

J’espère que je vous ai convaincu sur la solution et que vous l’installerais, même sur vos lab exposées sur Internet !

Tutoriel officiel de CrowdSec : https://www.crowdsec.net/blog/kubernetes-crowdsec-integration