Skip to content

Preparations for crowdsec-firewall-bouncer-nftables install#41

Open
the-buezi wants to merge 3 commits into
livefrom
crowdsec-firewall-bouncer-nftables
Open

Preparations for crowdsec-firewall-bouncer-nftables install#41
the-buezi wants to merge 3 commits into
livefrom
crowdsec-firewall-bouncer-nftables

Conversation

@the-buezi

Copy link
Copy Markdown

Protect host SSH with CrowdSec firewall bouncer while reusing existing CrowdSec Docker container

Problem

The current Docker setup already runs CrowdSec as a containerized WAF/threat-intelligence engine and integrates it with Traefik. Web traffic is routed through Traefik and protected by the CrowdSec Traefik bouncer/AppSec setup.

However, the host SSH service is outside Traefik and is therefore not protected by the Traefik/WAF bouncer. UFW allows SSH on the host, but UFW is a static allow/deny firewall and does not dynamically block IPs that CrowdSec has identified as abusive.

Goal: protect the host SSH service with CrowdSec decisions without installing a second full CrowdSec Security Engine on the host.

Current compose observations

The waf service already runs crowdsecurity/crowdsec:${CROWDSEC_VERSION:-latest} and persists its configuration/database via named volumes:

waf:
  image: crowdsecurity/crowdsec:${CROWDSEC_VERSION:-latest}
  container_name: ${COMPOSE_PROJECT_NAME:+${COMPOSE_PROJECT_NAME}-}crowdsec
  volumes:
    - waf-config:/etc/crowdsec
    - waf-db:/var/lib/crowdsec/data

It exposes LAPI and AppSec only inside Docker networks:

expose:
  - "8080" # LAPI
  - "7422" # AppSec

It already registers the Traefik bouncer key at container startup:

environment:
  - BOUNCER_KEY_traefik=${CROWDSEC_BOUNCER_KEY:-changeme-in-dotenv}

It currently installs web/AppSec-oriented collections only:

- COLLECTIONS=crowdsecurity/traefik crowdsecurity/http-cve crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules

It currently mounts Traefik logs and WAF configuration files, but does not mount host SSH/auth logs:

- "${HOST_PATH_LOGS:-./logs}/reverseproxy:/var/log/traefik:ro"
- "${HOST_PATH_WAF_CONFIG_FILES:-${HOST_DOCKER_WORKING_DIR:-.}/resources/waf}/acquis-traefik.yaml:/etc/crowdsec/acquis.d/traefik.yaml:ro"

Recommended concept

Reuse the existing waf container as the single CrowdSec Security Engine/LAPI.

Install only the firewall bouncer on the host:

sudo apt install crowdsec-firewall-bouncer-nftables

The host firewall bouncer should connect to the containerized CrowdSec LAPI and enforce decisions in host nftables. This keeps one central decision database and avoids running a second CrowdSec engine on the host.

Target architecture:

Host SSH logs
  -> mounted read-only into CrowdSec container
  -> CrowdSec container parses sshd logs and creates decisions
  -> host crowdsec-firewall-bouncer-nftables pulls decisions from container LAPI
  -> host nftables blocks abusive IPs before they reach sshd

Required compose changes

1. Expose CrowdSec LAPI to the host only

The host-installed firewall bouncer needs to reach LAPI. Currently the waf service only uses expose, which is visible to other Docker containers but not to host services.

Add a localhost-only port mapping to the waf service:

ports:
  - "127.0.0.1:${CROWDSEC_LAPI_PORT:-8080}:8080/tcp"

Do not expose this port publicly.

Recommended result:

waf:
  expose:
    - "8080"
    - "7422"
  ports:
    - "127.0.0.1:${CROWDSEC_LAPI_PORT:-8080}:8080/tcp"

2. Add SSH collection to the CrowdSec container

Extend the COLLECTIONS variable:

- COLLECTIONS=crowdsecurity/traefik crowdsecurity/http-cve crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules crowdsecurity/sshd

3. Mount host SSH logs read-only

On Debian, SSH authentication events are usually available in /var/log/auth.log if rsyslog is active.

Add:

- /var/log/auth.log:/var/log/auth.log:ro

If /var/log/auth.log does not exist or does not contain SSH events, use journald acquisition instead or enable rsyslog-based auth logging first.

4. Add SSH acquisition config

Create a new file, for example:

resources/waf/acquis-sshd.yaml

Suggested content:

filenames:
  - /var/log/auth.log
labels:
  type: syslog

Mount it into the CrowdSec container:

- "${HOST_PATH_WAF_CONFIG_FILES:-${HOST_DOCKER_WORKING_DIR:-.}/resources/waf}/acquis-sshd.yaml:/etc/crowdsec/acquis.d/sshd.yaml:ro"

5. Register a dedicated firewall-bouncer API key

Do not reuse the Traefik bouncer key. Add a separate key for the host firewall bouncer.

Preferred: create a new secret in .env:

CROWDSEC_FIREWALL_BOUNCER_KEY=<long-random-secret>

Then add to the waf service environment:

- BOUNCER_KEY_host_firewall=${CROWDSEC_FIREWALL_BOUNCER_KEY:-changeme-in-dotenv}

The official CrowdSec Docker image supports automatic bouncer registration with environment variables of the format BOUNCER_KEY_<name>=<key>.

Host-side firewall bouncer configuration

Install the nftables bouncer on the host, not in Docker:

sudo apt install crowdsec-firewall-bouncer-nftables

Configure the bouncer to use the containerized LAPI:

api_url: http://127.0.0.1:8080/
api_key: <CROWDSEC_FIREWALL_BOUNCER_KEY>
mode: nftables

Recommended nftables scope for the first rollout:

nftables_hooks:
  - input

Do not enable forward initially. The goal of this issue is SSH protection, not Docker-forwarded service filtering. Docker/Traefik traffic should remain protected by the Traefik/WAF bouncer for now.

Validation steps

Before change:

sudo nft list ruleset > ~/nft-before-crowdsec-firewall-bouncer.txt
sudo ufw status verbose > ~/ufw-before-crowdsec-firewall-bouncer.txt
docker compose exec waf cscli bouncers list
docker compose exec waf cscli collections list
docker compose exec waf cscli metrics

After compose change:

docker compose up -d waf
curl http://127.0.0.1:8080/ || true
docker compose exec waf cscli collections list | grep sshd
docker compose exec waf cscli metrics

After host bouncer install:

sudo systemctl status crowdsec-firewall-bouncer --no-pager
sudo nft list ruleset | grep -i crowdsec -A 80 -B 20
docker compose exec waf cscli bouncers list

Functional test from a non-critical test IP:

docker compose exec waf cscli decisions add --ip <TEST_CLIENT_IP> --duration 5m --reason "ssh firewall bouncer test"

Then attempt SSH from <TEST_CLIENT_IP>. Expected result: SSH is blocked while the decision exists.

Remove test decision:

docker compose exec waf cscli decisions delete --ip <TEST_CLIENT_IP>

Rollout precautions

  • Keep an existing SSH session open during testing.
  • Ensure console access is available before testing firewall enforcement.
  • Do not expose CrowdSec LAPI publicly; bind it only to 127.0.0.1.
  • Do not install a second full CrowdSec Security Engine on the host.
  • Do not run the firewall bouncer as a highly privileged container unless there is a strong reason. A host systemd service is simpler and safer for nftables enforcement.
  • Do not modify Docker-owned nftables/iptables chains manually.

Open checks before implementation

  • Confirm /var/log/auth.log exists on the host and contains SSH login/failure events.
  • Confirm the CrowdSec container can read the mounted auth log.
  • Confirm the containerized CrowdSec LAPI listens on 0.0.0.0:8080 inside the container so the host localhost port mapping works.
  • Confirm no host service already listens on 127.0.0.1:8080.
  • Confirm the existing profiles file creates ban decisions for sshd alerts.

Expected result

After implementation, SSH remains allowed by UFW for normal clients, but IPs banned by CrowdSec are dropped by nftables before they reach sshd.

Web traffic continues to be handled by Traefik and the existing CrowdSec WAF/AppSec bouncer. SSH protection is added without introducing a second CrowdSec Security Engine.

@the-buezi the-buezi requested a review from oliveratgithub June 11, 2026 22:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant