Protect the dashboard with CrowdSec AppSec

This guide shows how to protect a self-hosted NetBird dashboard with CrowdSec AppSec when NetBird is deployed with the internal Traefik reverse proxy.

The configuration reuses the CrowdSec LAPI container created by the NetBird quickstart when you enable the NetBird Proxy CrowdSec integration. NetBird Proxy and Traefik use separate bouncer keys against the same CrowdSec service.

For the NetBird Proxy IP reputation integration, see CrowdSec IP Reputation.

Prerequisites

  • A self-hosted NetBird deployment using the internal Traefik reverse proxy.
  • NetBird Proxy enabled with CrowdSec IP reputation blocking.
  • A Linux host reachable on TCP 80 and 443, and UDP 3478.
  • A domain such as netbird.example.com pointing to the host.
  • A wildcard record such as *.netbird.example.com pointing to the same host if you use NetBird Proxy.
  • Docker with Compose v2.
  • curl, jq, and openssl.

Set these shell variables before running the commands in this guide:

export NETBIRD_INSTALL_DIR="/path/to/netbird/install"
export NETBIRD_DOMAIN="netbird.example.com"

NETBIRD_INSTALL_DIR is the directory that contains the generated docker-compose.yml, proxy.env, dashboard.env, and crowdsec/ files.

Deploy NetBird with Traefik and CrowdSec

For a new deployment, follow the Self-hosting Quickstart Guide. When the quickstart asks for deployment options, choose:

Reverse proxy: 0, Traefik
Enable proxy: y
Enable CrowdSec IP reputation blocking: y

Enabling CrowdSec creates a local crowdsec service, registers a netbird-proxy bouncer, and writes NB_PROXY_CROWDSEC_API_URL and NB_PROXY_CROWDSEC_API_KEY to proxy.env.

If you already have a self-hosted NetBird deployment without NetBird Proxy, follow Enable Reverse Proxy Feature first.

Add a dashboard bouncer key

Create a dedicated bouncer key for the Traefik plugin and store it in the Compose project .env file:

cd "$NETBIRD_INSTALL_DIR"
umask 077
if ! grep -q '^CROWDSEC_DASHBOARD_BOUNCER_KEY=' .env 2>/dev/null; then
  printf 'CROWDSEC_DASHBOARD_BOUNCER_KEY=%s\n' "$(openssl rand -hex 32)" >> .env
fi

Rerunning this command preserves the existing key.

Add CrowdSec AppSec acquisition

Create the AppSec acquisition file:

mkdir -p "$NETBIRD_INSTALL_DIR/crowdsec/acquis.d"
cat >"$NETBIRD_INSTALL_DIR/crowdsec/acquis.d/appsec.yaml" <<'EOF'
appsec_configs:
  - crowdsecurity/appsec-default
labels:
  type: appsec
listen_addr: 0.0.0.0:7422
source: appsec
EOF

Update docker-compose.yml

Start from the docker-compose.yml generated by the quickstart with proxy CrowdSec enabled. The file already includes a crowdsec service, and proxy.env already points NetBird Proxy to http://crowdsec:8080.

Under services.traefik.command, add the Traefik CrowdSec bouncer plugin static arguments:

services:
  traefik:
    command:
      - '--experimental.plugins.bouncer.modulename=github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin'
      - '--experimental.plugins.bouncer.version=v1.6.0'

If your generated Compose project network is named netbird_netbird, make sure Traefik uses that Docker network:

services:
  traefik:
    command:
      - '--providers.docker.network=netbird_netbird'

Under services.traefik.depends_on, make Traefik wait until CrowdSec is healthy:

services:
  traefik:
    depends_on:
      crowdsec:
        condition: service_healthy

Under services.dashboard.labels, add the CrowdSec middleware to the existing dashboard router:

services:
  dashboard:
    labels:
      - traefik.http.routers.netbird-dashboard.middlewares=netbird-dashboard-crowdsec@docker

In the same services.dashboard.labels list, define the middleware:

services:
  dashboard:
    labels:
      - traefik.http.middlewares.netbird-dashboard-crowdsec.plugin.bouncer.enabled=true
      - traefik.http.middlewares.netbird-dashboard-crowdsec.plugin.bouncer.crowdsecMode=stream
      - traefik.http.middlewares.netbird-dashboard-crowdsec.plugin.bouncer.crowdsecLapiScheme=http
      - traefik.http.middlewares.netbird-dashboard-crowdsec.plugin.bouncer.crowdsecLapiHost=crowdsec:8080
      - traefik.http.middlewares.netbird-dashboard-crowdsec.plugin.bouncer.crowdsecLapiKey=${CROWDSEC_DASHBOARD_BOUNCER_KEY}
      - traefik.http.middlewares.netbird-dashboard-crowdsec.plugin.bouncer.crowdsecAppsecEnabled=true
      - traefik.http.middlewares.netbird-dashboard-crowdsec.plugin.bouncer.crowdsecAppsecHost=crowdsec:7422
      - traefik.http.middlewares.netbird-dashboard-crowdsec.plugin.bouncer.crowdsecAppsecFailureBlock=true
      - traefik.http.middlewares.netbird-dashboard-crowdsec.plugin.bouncer.crowdsecAppsecUnreachableBlock=true

Under services.netbird-server.labels, add a higher-priority router for dashboard API and embedded IdP paths:

services:
  netbird-server:
    labels:
      - traefik.http.routers.netbird-dashboard-api.rule=Host(`netbird.example.com`) && (PathPrefix(`/api`) || PathPrefix(`/oauth2`))
      - traefik.http.routers.netbird-dashboard-api.entrypoints=websecure
      - traefik.http.routers.netbird-dashboard-api.tls=true
      - traefik.http.routers.netbird-dashboard-api.tls.certresolver=letsencrypt
      - traefik.http.routers.netbird-dashboard-api.service=netbird-server
      - traefik.http.routers.netbird-dashboard-api.middlewares=netbird-dashboard-crowdsec@docker
      - traefik.http.routers.netbird-dashboard-api.priority=110

Replace netbird.example.com with your NetBird domain. Traefik labels do not read the NETBIRD_DOMAIN shell variable used in the command examples.

Merge these fields into services.crowdsec. This adds the AppSec collections, registers the dashboard bouncer key, and updates the health check so CrowdSec reports healthy only after both LAPI and AppSec are available. Keep the generated fields that already exist in your Compose file, such as image, container_name, restart, networks, labels, logging, and volumes.

The installer-created health check only verifies the CrowdSec Local API:

healthcheck:
  test: ['CMD', 'cscli', 'lapi', 'status']
  interval: 10s
  timeout: 5s
  retries: 15

Dashboard protection also needs the AppSec listener on port 7422, so replace the installer-created health check with the one below.

FieldPurpose
crowdsecurity/linuxKeeps the base Linux collection created by the installer.
crowdsecurity/appsec-virtual-patchingAdds virtual patching rules for common web application attack patterns.
crowdsecurity/appsec-generic-rulesAdds generic AppSec detection rules used by the AppSec engine.
BOUNCER_KEY_netbird_dashboardRegisters a separate Traefik dashboard bouncer key without reusing the NetBird Proxy bouncer key.
healthcheck.testReplaces the installer LAPI-only check with a check for both LAPI and the AppSec listener on 7422.
services:
  crowdsec:
    ...
    environment:
      COLLECTIONS: "crowdsecurity/linux crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules"
      BOUNCER_KEY_netbird_dashboard: "${CROWDSEC_DASHBOARD_BOUNCER_KEY}"
    healthcheck:
      test: ["CMD-SHELL", "cscli lapi status >/dev/null 2>&1 && bash -c '</dev/tcp/127.0.0.1/7422'"]
      interval: 10s
      timeout: 5s
      retries: 15
      start_period: 30s

Keep the installer-created CrowdSec volume in the top-level volumes section:

volumes:
  crowdsec_db:

Apply the change

Validate the Compose file without printing the resolved configuration, which contains the dashboard bouncer key:

cd "$NETBIRD_INSTALL_DIR"
docker compose config --quiet

Start the updated stack:

docker compose up -d
docker compose ps

Verify protection

Open https://$NETBIRD_DOMAIN in a browser and confirm the dashboard loads. Then verify the status codes from the NetBird host or another machine that can reach the dashboard.

Normal dashboard and OIDC requests should return 200:

curl -ks -A 'Mozilla/5.0 NetBirdDashboardCheck' -o /dev/null -w '%{http_code}\n' "https://$NETBIRD_DOMAIN"
curl -ks -A 'Mozilla/5.0 NetBirdDashboardCheck' -o /dev/null -w '%{http_code}\n' "https://$NETBIRD_DOMAIN/oauth2/.well-known/openid-configuration"

A known malicious probe should return 403:

curl -ks -A 'Mozilla/5.0 NetBirdDashboardCheck' -o /dev/null -w '%{http_code}\n' "https://$NETBIRD_DOMAIN/.env"

Check AppSec metrics and bouncer registration:

docker compose exec -T crowdsec cscli metrics show appsec
docker compose exec -T crowdsec cscli bouncers list

A healthy deployment shows AppSec requests being processed, the netbird-proxy bouncer from the quickstart, and the netbird_dashboard bouncer used by Traefik.

Test manual IP blocks

You can test that the Traefik bouncer blocks dashboard access by adding a temporary CrowdSec decision for your own public IP.

First, get your public IPv4 address from the same client you will use to test dashboard access:

curl -4s ifconfig.me

Run this command through the same network path as the dashboard test. NAT or proxies can change the source IP that Traefik sees.

On the NetBird host, add a short test ban:

cd "$NETBIRD_INSTALL_DIR"
TEST_IP="<your-public-ipv4>"
docker compose exec -T crowdsec cscli decisions add --ip "$TEST_IP" --duration 3m --reason "dashboard bouncer test"

Wait up to 60 seconds because the Traefik bouncer runs in stream mode and refreshes decisions periodically. Then open your NetBird dashboard in a browser from the blocked IP. The dashboard request should be blocked.

Alternatively, verify the block with curl:

curl -ks -A 'Mozilla/5.0 NetBirdDashboardCheck' -o /dev/null -w '%{http_code}\n' "https://$NETBIRD_DOMAIN"

The response should be 403.

Confirm the decision exists:

docker compose exec -T crowdsec cscli decisions list

Remove the decision when done:

docker compose exec -T crowdsec cscli decisions delete --ip "$TEST_IP"

Verify access returns:

curl -ks -A 'Mozilla/5.0 NetBirdDashboardCheck' -o /dev/null -w '%{http_code}\n' "https://$NETBIRD_DOMAIN"

The response should be 200.

Troubleshooting

If all dashboard requests return 403 immediately after startup, Traefik may have started before CrowdSec LAPI and AppSec were ready. Confirm that the CrowdSec health check is present and that Traefik uses depends_on.condition: service_healthy.

If Traefik logs warnings about a missing Docker network, check the actual network name:

docker network ls | grep netbird

Then set --providers.docker.network to the actual network name, commonly netbird_netbird.

If AppSec metrics do not increment, confirm that the middleware is attached to the dashboard router and that Traefik can reach crowdsec:8080 and crowdsec:7422.