--- # CrowdSec → NGINX Ingress IP Block Sync # # Runs every minute, reads all active CrowdSec ban decisions, # and updates the NGINX Ingress Controller ConfigMap's block-cidrs field. # This propagates CrowdSec's automated threat detection into NGINX's global IP blocklist. # VPN/client IPs in ALLOWLIST below are excluded from the NGINX blocklist. apiVersion: v1 kind: ServiceAccount metadata: name: crowdsec-nginx-sync namespace: crowdsec --- # Allow the sync job to read CrowdSec LAPI pod exec (to run cscli) apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: crowdsec-nginx-sync rules: # Exec into CrowdSec LAPI pod to run cscli - apiGroups: [""] resources: ["pods/exec"] verbs: ["create"] - apiGroups: [""] resources: ["pods"] verbs: ["get", "list"] # Read/patch the NGINX Ingress ConfigMap - apiGroups: [""] resources: ["configmaps"] verbs: ["get", "patch"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: crowdsec-nginx-sync roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: crowdsec-nginx-sync subjects: - kind: ServiceAccount name: crowdsec-nginx-sync namespace: crowdsec --- apiVersion: batch/v1 kind: CronJob metadata: name: crowdsec-nginx-sync namespace: crowdsec spec: schedule: "* * * * *" # every minute concurrencyPolicy: Forbid # skip if previous run is still going successfulJobsHistoryLimit: 3 failedJobsHistoryLimit: 3 jobTemplate: spec: activeDeadlineSeconds: 55 # must finish before next run template: spec: serviceAccountName: crowdsec-nginx-sync restartPolicy: Never containers: - name: sync image: bitnami/kubectl:latest command: - /bin/sh - -c - | set -e # Find the CrowdSec LAPI pod LAPI_POD=$(kubectl get pods -n crowdsec \ -l "type=lapi" \ -o jsonpath='{.items[0].metadata.name}') if [ -z "$LAPI_POD" ]; then echo "ERROR: CrowdSec LAPI pod not found" exit 1 fi echo "Using LAPI pod: $LAPI_POD" # Fetch all active ban decisions in raw CSV format: # id,source,ip,reason,action,country,as,events_count,expiration,simulated,alert_id # The ip column has format "Ip:1.2.3.4" or "Range:1.2.3.0/24" RAW=$(kubectl exec -n crowdsec "$LAPI_POD" -- \ cscli decisions list -t ban -o raw 2>/dev/null || true) # Skip CSV header, extract column 3, strip the "Ip:" / "Range:" prefix CIDRS=$(echo "$RAW" | \ tail -n +2 | \ cut -d',' -f3 | \ sed 's/^[^:]*://g' | \ grep -v '^$' | \ tr '\n' ',' | \ sed 's/,$//') echo "Active ban count: $(echo "$RAW" | tail -n +2 | grep -c '.' || echo 0)" # Read any manually managed IPs from block-cidrs-manual key in the same ConfigMap. # Edit that key to permanently block IPs that CrowdSec won't remove. MANUAL=$(kubectl get configmap ingress-nginx-controller \ -n ingress-nginx \ -o jsonpath='{.data.block-cidrs-manual}' 2>/dev/null || true) # VPN/client IPs that should never be written into NGINX block-cidrs. ALLOWLIST="195.86.27.8,14.137.198.99" # Merge CrowdSec bans + manual list, remove allowlisted entries, deduplicate ALL=$(printf '%s,%s' "$CIDRS" "$MANUAL" | \ tr ',' '\n' | \ grep -v '^$' | \ grep -v '^140\.82\.115\.' | \ awk -v allowlist="$ALLOWLIST" 'BEGIN { split(allowlist, ips, ","); for (i in ips) allow[ips[i]] = 1 } !allow[$0]' | \ sort -u | \ tr '\n' ',' | \ sed 's/,$//') echo "CrowdSec bans: ${CIDRS:-}" echo "Manual bans: ${MANUAL:-}" echo "Allowlist: ${ALLOWLIST:-}" echo "Merged total: ${ALL:-}" # Patch the NGINX Ingress ConfigMap (JSON merge patch — only updates block-cidrs) kubectl patch configmap ingress-nginx-controller \ -n ingress-nginx \ --type merge \ -p "{\"data\":{\"block-cidrs\":\"${ALL}\"}}" echo "ConfigMap patched successfully" resources: requests: cpu: 10m memory: 32Mi limits: cpu: 100m memory: 64Mi