Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/components/NavigationDocs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -614,6 +614,10 @@ export const docsNavigation = [
title: 'CrowdSec IP Reputation',
href: '/selfhosted/maintenance/crowdsec',
},
{
title: 'CrowdSec Dashboard Protection',
href: '/selfhosted/maintenance/crowdsec-dashboard',
},
],
},
{
Expand Down
314 changes: 314 additions & 0 deletions src/pages/selfhosted/maintenance/crowdsec-dashboard.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,314 @@
import { Warning } from '@/components/mdx'

export const description =
'Protect the self-hosted NetBird dashboard with CrowdSec AppSec and the Traefik CrowdSec bouncer plugin.'

# Protect the dashboard with CrowdSec AppSec

This guide shows how to protect a self-hosted NetBird dashboard with [CrowdSec AppSec](https://docs.crowdsec.net/docs/appsec/intro) 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](/selfhosted/maintenance/crowdsec).

## 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:

```bash
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](/selfhosted/selfhosted-quickstart). When the quickstart asks for deployment options, choose:

```text
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](/selfhosted/migration/enable-reverse-proxy) first.

<Warning>
This CrowdSec dashboard protection setup requires the [combined container setup](/selfhosted/migration/combined-container). If your deployment uses the separate-container architecture (`management`, `signal`, `relay`, and `coturn`), migrate to the combined container setup before enabling NetBird Proxy.
</Warning>

## Add a dashboard bouncer key

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

```bash
cd "$NETBIRD_INSTALL_DIR"
umask 077
printf 'CROWDSEC_DASHBOARD_BOUNCER_KEY=%s\n' "$(openssl rand -hex 32)" >> .env
```

## Add CrowdSec AppSec acquisition

Create the AppSec acquisition file:

```bash
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:

```yaml
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:

```yaml
services:
traefik:
command:
- "--providers.docker.network=netbird_netbird"
```

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

```yaml
services:
traefik:
depends_on:
crowdsec:
condition: service_healthy
```

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

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

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

```yaml
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:

```yaml
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.

<Warning>
Leave the NetBird Proxy TCP passthrough labels unchanged. Do not attach this middleware to the `proxy-passthrough` TCP router.
</Warning>

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:

```yaml
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.

| Field | Purpose |
|---|---|
| `crowdsecurity/linux` | Keeps the base Linux collection created by the installer. |
| `crowdsecurity/appsec-virtual-patching` | Adds virtual patching rules for common web application attack patterns. |
| `crowdsecurity/appsec-generic-rules` | Adds generic AppSec detection rules used by the AppSec engine. |
| `BOUNCER_KEY_netbird_dashboard` | Registers a separate Traefik dashboard bouncer key without reusing the NetBird Proxy bouncer key. |
| `healthcheck.test` | Replaces the installer LAPI-only check with a check for both LAPI and the AppSec listener on `7422`. |

```yaml
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:

```yaml
volumes:
crowdsec_db:
```

## Apply the change

Validate the Compose file. This command only checks and renders the merged Compose configuration; it does not need to write the output to a file:

```bash
cd "$NETBIRD_INSTALL_DIR"
docker compose config
```

Start the updated stack:

```bash
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`:

```bash
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`:

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

Check AppSec metrics and bouncer registration:

```bash
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 the public IP address Traefik sees:

```bash
curl -4s ifconfig.me
```

On the NetBird host, add a short test ban:

```bash
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`:

```bash
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:

```bash
docker compose exec -T crowdsec cscli decisions list
```

Remove the decision when done:

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

Verify access returns:

```bash
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:

```bash
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`.

## Related documentation

- [Self-hosting Quickstart Guide](/selfhosted/selfhosted-quickstart)
- [Enable Reverse Proxy Feature](/selfhosted/migration/enable-reverse-proxy)
- [Combined Container Setup](/selfhosted/migration/combined-container)
- [CrowdSec IP Reputation](/selfhosted/maintenance/crowdsec)
- [Configuration Files Reference](/selfhosted/maintenance/configuration-files)
- [Reverse Proxy](/manage/reverse-proxy)
- [CrowdSec Traefik AppSec quickstart](https://docs.crowdsec.net/docs/appsec/quickstart/traefik/)
- [Traefik CrowdSec bouncer plugin](https://plugins.traefik.io/plugins/6335346ca4caa9ddeffda116/crowdsec-bouncer-traefik-plugin)
Loading