Initial commit

This commit is contained in:
Scooby Husky
2026-03-09 20:21:35 -05:00
commit aacb8eebbe
314 changed files with 21766 additions and 0 deletions
@@ -0,0 +1,41 @@
# CrowdSec secrets from Vault
# - CAPI enrollment key (user registers at app.crowdsec.net)
# - Bouncer API key (generated by postsync hook via cscli)
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: crowdsec-capi
namespace: crowdsec
spec:
refreshInterval: 1h
secretStoreRef:
kind: ClusterSecretStore
name: vault-backend
target:
name: crowdsec-capi-key
creationPolicy: Owner
data:
- secretKey: enrollment-key
remoteRef:
key: crowdsec-capi
property: enrollment-key
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: crowdsec-bouncer
namespace: crowdsec
spec:
refreshInterval: "0"
secretStoreRef:
kind: ClusterSecretStore
name: vault-backend
target:
name: crowdsec-bouncer-key
creationPolicy: Owner
data:
- secretKey: api-key
remoteRef:
key: crowdsec-bouncer
property: api-key
@@ -0,0 +1,164 @@
# CrowdSec Firewall Bouncer - nftables enforcement on each node
# Blocks malicious IPs before they reach Cilium eBPF processing
# Requires: hostNetwork + privileged (nftables management)
# Image: community Docker build of cs-firewall-bouncer (no official image exists)
---
apiVersion: v1
kind: ConfigMap
metadata:
name: crowdsec-firewall-bouncer-config
namespace: crowdsec
data:
crowdsec-firewall-bouncer.yaml: |
mode: nftables
update_frequency: 10s
log_mode: stdout
log_level: info
api_url: http://crowdsec-service.crowdsec.svc:8080/
api_key: ${API_KEY}
insecure_skip_verify: false
disable_ipv6: false
deny_action: DROP
deny_log: true
deny_log_prefix: "crowdsec: "
supported_decisions_types:
- ban
blacklists_ipv4: crowdsec-blacklists
blacklists_ipv6: crowdsec6-blacklists
nftables:
ipv4:
enabled: true
set-only: false
table: crowdsec
chain: crowdsec-chain
priority: -10
ipv6:
enabled: true
set-only: false
table: crowdsec6
chain: crowdsec6-chain
priority: -10
nftables_hooks:
- input
- forward
prometheus:
enabled: true
listen_addr: 0.0.0.0
listen_port: 60601
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: crowdsec-firewall-bouncer
namespace: crowdsec
labels:
app: crowdsec-firewall-bouncer
spec:
selector:
matchLabels:
app: crowdsec-firewall-bouncer
template:
metadata:
labels:
app: crowdsec-firewall-bouncer
spec:
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
tolerations:
- operator: Exists
initContainers:
# Render env vars in config template
- name: render-config
image: docker.io/library/busybox:1.36
securityContext:
runAsUser: 10000
runAsGroup: 10000
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
command: ["/bin/sh", "-c"]
args:
- |
sed "s|\${API_KEY}|${API_KEY}|g" /config-template/crowdsec-firewall-bouncer.yaml > /config/crowdsec-firewall-bouncer.yaml
env:
- name: API_KEY
valueFrom:
secretKeyRef:
name: crowdsec-bouncer-key
key: api-key
volumeMounts:
- name: config-template
mountPath: /config-template
- name: config-rendered
mountPath: /config
containers:
- name: bouncer
image: ghcr.io/shgew/cs-firewall-bouncer-docker:v0.0.34-patch1
command:
- /usr/local/bin/crowdsec-firewall-bouncer
- -c
- /config/crowdsec-firewall-bouncer.yaml
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
memory: 64Mi
securityContext:
privileged: true
capabilities:
add:
- NET_ADMIN
- NET_RAW
volumeMounts:
- name: config-rendered
mountPath: /config
readOnly: true
- name: nftables-lock
mountPath: /run
volumes:
- name: config-template
configMap:
name: crowdsec-firewall-bouncer-config
- name: config-rendered
emptyDir: {}
- name: nftables-lock
hostPath:
path: /run
type: Directory
---
apiVersion: v1
kind: Service
metadata:
name: crowdsec-firewall-bouncer-metrics
namespace: crowdsec
labels:
app: crowdsec-firewall-bouncer
spec:
selector:
app: crowdsec-firewall-bouncer
ports:
- name: metrics
port: 60601
targetPort: 60601
protocol: TCP
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: crowdsec-firewall-bouncer
namespace: crowdsec
labels:
app: crowdsec-firewall-bouncer
release: kube-prometheus-stack
spec:
selector:
matchLabels:
app: crowdsec-firewall-bouncer
endpoints:
- port: metrics
interval: 30s
@@ -0,0 +1,205 @@
# CrowdSec Grafana Dashboard
# Auto-discovered by Grafana sidecar via grafana_dashboard label
apiVersion: v1
kind: ConfigMap
metadata:
name: grafana-dashboard-crowdsec
namespace: grafana
labels:
grafana_dashboard: "1"
data:
crowdsec-overview.json: |
{
"annotations": { "list": [] },
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"links": [],
"panels": [
{
"title": "Active Decisions (Bans)",
"type": "stat",
"gridPos": { "h": 4, "w": 6, "x": 0, "y": 0 },
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [
{
"expr": "cs_active_decisions",
"legendFormat": "Active Bans"
}
],
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"thresholds": {
"steps": [
{ "color": "green", "value": null },
{ "color": "yellow", "value": 10 },
{ "color": "red", "value": 100 }
]
}
}
}
},
{
"title": "Total Alerts",
"type": "stat",
"gridPos": { "h": 4, "w": 6, "x": 6, "y": 0 },
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [
{
"expr": "sum(increase(cs_alerts_total[24h]))",
"legendFormat": "Alerts (24h)"
}
],
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"thresholds": {
"steps": [
{ "color": "green", "value": null },
{ "color": "yellow", "value": 50 },
{ "color": "red", "value": 200 }
]
}
}
}
},
{
"title": "CAPI Blocklist Entries",
"type": "stat",
"gridPos": { "h": 4, "w": 6, "x": 12, "y": 0 },
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [
{
"expr": "cs_active_decisions{origin=\"CAPI\"}",
"legendFormat": "Community Blocklist"
}
]
},
{
"title": "Bouncer Status",
"type": "stat",
"gridPos": { "h": 4, "w": 6, "x": 18, "y": 0 },
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [
{
"expr": "cs_bouncers",
"legendFormat": "Bouncers"
}
],
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"thresholds": {
"steps": [
{ "color": "red", "value": null },
{ "color": "green", "value": 1 }
]
}
}
}
},
{
"title": "Alerts by Scenario (24h)",
"type": "timeseries",
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 4 },
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [
{
"expr": "sum by (scenario) (increase(cs_alerts_total[1h]))",
"legendFormat": "{{ scenario }}"
}
],
"fieldConfig": {
"defaults": {
"custom": { "drawStyle": "bars", "stacking": { "mode": "normal" } }
}
}
},
{
"title": "Parser Hits (24h)",
"type": "timeseries",
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 4 },
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [
{
"expr": "sum by (source) (increase(cs_parser_hits_total[1h]))",
"legendFormat": "{{ source }}"
}
],
"fieldConfig": {
"defaults": {
"custom": { "drawStyle": "line", "fillOpacity": 10 }
}
}
},
{
"title": "Decisions by Origin",
"type": "piechart",
"gridPos": { "h": 8, "w": 8, "x": 0, "y": 12 },
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [
{
"expr": "cs_active_decisions",
"legendFormat": "{{ origin }}"
}
]
},
{
"title": "Bucket Overflows (Triggered Scenarios)",
"type": "timeseries",
"gridPos": { "h": 8, "w": 16, "x": 8, "y": 12 },
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [
{
"expr": "sum by (name) (increase(cs_bucket_overflows_total[1h]))",
"legendFormat": "{{ name }}"
}
],
"fieldConfig": {
"defaults": {
"custom": { "drawStyle": "bars", "stacking": { "mode": "normal" } }
}
}
},
{
"title": "Firewall Bouncer - Processed Decisions",
"type": "timeseries",
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 20 },
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [
{
"expr": "sum by (instance) (increase(cs_bouncer_decisions[1h]))",
"legendFormat": "{{ instance }}"
}
]
},
{
"title": "LAPI Requests",
"type": "timeseries",
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 20 },
"datasource": { "type": "prometheus", "uid": "${DS_PROMETHEUS}" },
"targets": [
{
"expr": "sum by (method, route) (increase(cs_lapi_requests_total[1h]))",
"legendFormat": "{{ method }} {{ route }}"
}
]
}
],
"schemaVersion": 39,
"tags": ["crowdsec", "security", "zero-trust"],
"templating": {
"list": [
{
"current": { "selected": false, "text": "Prometheus", "value": "prometheus" },
"name": "DS_PROMETHEUS",
"type": "datasource",
"query": "prometheus"
}
]
},
"time": { "from": "now-24h", "to": "now" },
"title": "CrowdSec Overview",
"uid": "crowdsec-overview"
}
@@ -0,0 +1,53 @@
apiVersion: batch/v1
kind: Job
metadata:
name: crowdsec-postsync
namespace: crowdsec
annotations:
argocd.argoproj.io/hook: PostSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
backoffLimit: 3
template:
spec:
serviceAccountName: argocd-hook-sa
containers:
- name: postsync
image: bitnami/kubectl:1.31
command:
- /bin/bash
- -c
- |
set -euo pipefail
echo "=== CrowdSec PostSync ==="
# Wait for CrowdSec LAPI
echo "Waiting for CrowdSec LAPI..."
kubectl -n crowdsec rollout status deploy/crowdsec-lapi --timeout=180s 2>/dev/null || \
echo "CrowdSec LAPI not ready yet"
# Register firewall bouncer and store key in Vault
LAPI_POD=$(kubectl -n crowdsec get pod -l app.kubernetes.io/name=crowdsec,app.kubernetes.io/component=lapi -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "")
if [ -n "$LAPI_POD" ]; then
ROOT_TOKEN=$(kubectl -n vault get secret vault-init-keys -o jsonpath='{.data.VAULT_ROOT_TOKEN}' 2>/dev/null | base64 -d)
EXISTING_KEY=""
if [ -n "$ROOT_TOKEN" ]; then
EXISTING_KEY=$(kubectl exec -n vault vault-0 -- env "VAULT_TOKEN=${ROOT_TOKEN}" \
vault kv get -field=api-key secret/crowdsec-bouncer 2>/dev/null || echo "")
fi
if [ -z "$EXISTING_KEY" ]; then
echo "Registering firewall bouncer..."
BOUNCER_KEY=$(kubectl -n crowdsec exec "$LAPI_POD" -- cscli bouncers add firewall-bouncer -o raw 2>/dev/null || echo "")
if [ -n "$BOUNCER_KEY" ] && [ -n "$ROOT_TOKEN" ]; then
kubectl exec -n vault vault-0 -- env "VAULT_TOKEN=${ROOT_TOKEN}" \
vault kv put secret/crowdsec-bouncer api-key="$BOUNCER_KEY" 2>/dev/null && \
echo " Bouncer API key stored in Vault"
fi
else
echo " Firewall bouncer key already in Vault"
fi
fi
echo "=== CrowdSec PostSync Complete ==="
restartPolicy: Never
@@ -0,0 +1,40 @@
apiVersion: batch/v1
kind: Job
metadata:
name: crowdsec-presync
namespace: crowdsec
annotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
backoffLimit: 3
template:
spec:
serviceAccountName: argocd-hook-sa
containers:
- name: presync
image: bitnami/kubectl:1.31
command:
- /bin/bash
- -c
- |
set -euo pipefail
echo "=== CrowdSec PreSync ==="
# Set PodSecurity to privileged (firewall bouncer needs host access)
kubectl label namespace crowdsec pod-security.kubernetes.io/enforce=privileged --overwrite 2>/dev/null || true
# Wait for CNPG cluster
echo "Waiting for CrowdSec PostgreSQL cluster..."
for i in $(seq 1 60); do
PHASE=$(kubectl -n crowdsec get clusters.postgresql.cnpg.io pg-crowdsec -o jsonpath='{.status.phase}' 2>/dev/null || echo "")
if [ "$PHASE" = "Cluster in healthy state" ] || [ "$PHASE" = "Healthy" ]; then
echo " CrowdSec PostgreSQL cluster ready"
break
fi
echo " waiting for pg-crowdsec... (attempt $i/60, phase=$PHASE)"
sleep 5
done
echo "=== CrowdSec PreSync Complete ==="
restartPolicy: Never