Crowdsec WEB UI
Stack
A modern, responsive web interface for managing CrowdSec alerts and decisions. Built with React, Vite, Bun, and Tailwind CSS.
Image details
Source details
Configuration
TypeComposelinuxghcr.io/theduffman85/crowdsec-web-ui:latest${PORT:-3000}:3000/app/data : /portainer/Files/AppData/Config/crowdsec-web-ui/dataCROWDSEC_URL=${CROWDSEC_URL:-http://crowdsec:8080}CROWDSEC_USER=${CROWDSEC_USER:-crowdsec-web-ui}CROWDSEC_PASSWORD=${CROWDSEC_PASSWORD}CROWDSEC_LOOKBACK_PERIOD=${CROWDSEC_LOOKBACK_PERIOD:-168h}CROWDSEC_REFRESH_INTERVAL=${CROWDSEC_REFRESH_INTERVAL:-30s}CROWDSEC_IDLE_REFRESH_INTERVAL=${CROWDSEC_IDLE_REFRESH_INTERVAL:-5m}CROWDSEC_IDLE_THRESHOLD=${CROWDSEC_IDLE_THRESHOLD:-2m}CROWDSEC_FULL_REFRESH_INTERVAL=${CROWDSEC_FULL_REFRESH_INTERVAL:-5m}BASE_PATH=${BASE_PATH:-}unless-stoppedStandalone Install
Select an install method, to see config/commands for deploying Crowdsec WEB UI
Install on Portainer
Import all app templates into your Portainer instance, for easy 1-click deploys
- Ensure both Docker and Portainer are installed, and up-to-date
- Log into your Portainer web UI
- Under Settings → App Templates, paste the below URL
- Head to Home → App Templates, and the list of apps will show up
- Select Crowdsec WEB UI, fill in any config options, and hit Deploy
Template Import URL
https://raw.githubusercontent.com/Lissy93/portainer-templates/main/templates.json
Show Me
Original stackfile
The compose file this template deploys, straight from its repo:
version: '3.8'
services:
crowdsec-web-ui:
image: ghcr.io/theduffman85/crowdsec-web-ui:latest
container_name: crowdsec-web-ui
restart: unless-stopped
ports:
- ${PORT:-3000}:3000
environment:
- CROWDSEC_URL=${CROWDSEC_URL:-http://crowdsec:8080}
- CROWDSEC_USER=${CROWDSEC_USER:-crowdsec-web-ui}
- CROWDSEC_PASSWORD=${CROWDSEC_PASSWORD}
- CROWDSEC_LOOKBACK_PERIOD=${CROWDSEC_LOOKBACK_PERIOD:-168h}
- CROWDSEC_REFRESH_INTERVAL=${CROWDSEC_REFRESH_INTERVAL:-30s}
- CROWDSEC_IDLE_REFRESH_INTERVAL=${CROWDSEC_IDLE_REFRESH_INTERVAL:-5m}
- CROWDSEC_IDLE_THRESHOLD=${CROWDSEC_IDLE_THRESHOLD:-2m}
- CROWDSEC_FULL_REFRESH_INTERVAL=${CROWDSEC_FULL_REFRESH_INTERVAL:-5m}
- BASE_PATH=${BASE_PATH:-}
volumes:
- /portainer/Files/AppData/Config/crowdsec-web-ui/data:/app/data
networks:
- crowdsec-web-ui-network
networks:
crowdsec-web-ui-network:
driver: bridge
Or deploy it directly from the source:
git clone https://github.com/xneo1/portainer_templates
cd portainer_templates
docker compose -f Template/Stack/crowdsec-web-ui.yml up -dMore install options in our documentation, or see theduffman85/crowdsec-web-ui for app-specific guidance.
CrowdSec Web UI
A self-hosted dashboard for CrowdSec: investigate alerts, manage decisions, monitor runtime metrics, and send notifications from one responsive UI.Features
| Area | Highlights |
|---|---|
| Dashboard | Alert and active-decision totals, attack map, drilldowns, top lists, shared quick filters, and simulation counts |
| Alerts | Searchable alert history, persistent count-aware quick filters, CrowdSec alert contexts, IP/AS/location details, event metadata, simulation labels, and configurable columns |
| Decisions | Active and expired decisions, persistent count-aware quick filters, duplicate hiding, manual bans, custom durations, reasons, and cleanup actions |
| Multi-instance | Several CrowdSec LAPIs, per-instance views, and a Combined scope for Dashboard, Alerts, and Decisions |
| Metrics | Optional Prometheus views for LAPI activity, bouncers, AppSec, parsers, latency, parsing time, and whitelists |
| Notifications | Alert, decision, CVE, availability, and update rules delivered through Email, Gotify, MQTT, ntfy, or Webhooks |
| Security | Initial administrator setup, password and TOTP login, passkeys, OIDC SSO, group roles, and instance-wide read-only mode |
| Localization | Arabic, Chinese, English, French, German, Hindi, Japanese, Portuguese, Russian, and Spanish |
| Experience | Unified search, dark/light themes, and responsive layouts |
Screenshots
Quick Start
You need a running CrowdSec LAPI. Connect the Web UI using either watcher password authentication or agent mTLS.- Register the Web UI
Watcher password
openssl rand -hex 32
docker exec crowdsec cscli machines add crowdsec-web-ui --password 'replace-with-generated-password' -f /dev/null
# For local installations
sudo cscli machines add crowdsec-web-ui --password 'replace-with-generated-password' -f /dev/nullReplace
replace-with-generated-password with the value printed by openssl.!IMPORTANT
Keep -f /dev/null. It registers the machine without overwriting the CrowdSec container's existing credentials file.Agent mTLS
Configure LAPI TLS authentication and create a client certificate/key pair using the CrowdSec TLS authentication guide.- Start with Docker Compose
services:
crowdsec-web-ui:
image: ghcr.io/theduffman85/crowdsec-web-ui:latest
container_name: crowdsec_web_ui
ports:
- "3000:3000"
# For local CrowdSec instances
# extra_hosts:
# - "host.docker.internal:host-gateway"
environment:
CONFIG_INSTANCE_LAPI_URL: http://crowdsec:8080
# For local CrowdSec instances
# CONFIG_INSTANCE_LAPI_URL: http://host.docker.internal:8080
CONFIG_INSTANCE_LAPI_AUTH_USERNAME: crowdsec-web-ui
CONFIG_INSTANCE_LAPI_AUTH_PASSWORD: your-crowdsec-password
volumes:
- ./data:/app/data
restart: unless-stoppedA ready-to-use
docker-compose.yml is included. Add the generated password, make sure the Web UI can reach CrowdSec on the same Docker network, then start it.docker compose up -dOpen
http://localhost:3000 and create the initial administrator account.Docker Run Alternative
docker pull ghcr.io/theduffman85/crowdsec-web-ui:latest
mkdir -p data
docker run -d \
--name crowdsec_web_ui \
-p 3000:3000 \
-v $(pwd)/data:/app/data \
-e CONFIG_INSTANCE_LAPI_URL=http://crowdsec:8080 \
-e CONFIG_INSTANCE_LAPI_AUTH_USERNAME=crowdsec-web-ui \
-e CONFIG_INSTANCE_LAPI_AUTH_PASSWORD=your-crowdsec-password \
--network your_crowdsec_network \
ghcr.io/theduffman85/crowdsec-web-ui:latestCurrent images use Node.js and do not have the former Bun/AVX-specific x64 limitation.
mTLS Compose Alternative
services:
crowdsec-web-ui:
image: ghcr.io/theduffman85/crowdsec-web-ui:latest
container_name: crowdsec_web_ui
ports:
- "3000:3000"
environment:
CONFIG_INSTANCE_LAPI_URL: https://crowdsec:8080
CONFIG_INSTANCE_LAPI_AUTH_TYPE: mtls
CONFIG_INSTANCE_LAPI_AUTH_CERT_FILE: /certs/agent.pem
CONFIG_INSTANCE_LAPI_AUTH_KEY_FILE: /certs/agent-key.pem
# CONFIG_INSTANCE_LAPI_TLS_CA_FILE: /certs/ca.pem
volumes:
- ./data:/app/data
- /path/on/host/agent.pem:/certs/agent.pem:ro
- /path/on/host/agent-key.pem:/certs/agent-key.pem:ro
# - /path/on/host/ca.pem:/certs/ca.pem:ro
restart: unless-stoppedAdjust the URL and certificate paths. Enable
CONFIG_INSTANCE_LAPI_TLS_CA_FILE and its volume when LAPI uses a private CA.!CAUTION Use HTTPS and a hardened reverse proxy for public deployments. Built-in authentication protects the UI and API, but TLS terminates outside the application. OIDC integrations include Authentik, Authelia, and Keycloak. Migrated installations that predate authentication remain unauthenticated until explicitly enabled.
Architecture
| Component | Implementation |
|---|---|
| Client | React, Vite, and Tailwind CSS; builds to dist/client |
| Server | Node.js and Hono; builds to dist/server |
| Storage | SQLite via better-sqlite3 under /app/data |
| CrowdSec | Watcher password or agent mTLS; delta refreshes and chunked historical synchronization |
| Container | Runs as the non-root node user |
Configuration
Configuration files
| Environment | Default path |
|---|---|
| Docker | /app/data/config.yaml |
| Local | ./data/config.yaml |
Use
CONFIG_FILE only to select another existing file. config.example.yaml contains the complete commented YAML reference.Configuration lifecycle
| Stage | Behavior |
|---|---|
| First start | Creates the missing default file. Supplied values become active YAML; defaults and optional examples remain comments. Generated mappings use block rows and the documented order. |
| Later starts | Treats the file as user-managed. CONFIG values override it in memory without rewriting it; generated explanations and defaults are not refreshed. |
| Persistent overrides | CONFIGPERSISTOVERRIDES: "true" writes validated merged values while preserving comments where possible. Removing a persisted non-secret override leaves its last value in YAML. |
| Precedence | Applies section variables, then field variables, then indexed array variables. Removing a non-persisted override reveals the file value. |
| Logging | Records applied paths and before/after values. Credentials are redacted; secret references show only their environment name or file path. |
| Reloading | Requires a restart after configuration changes or secret rotation. |
Environment overrides
- Values are parsed as YAML and validated.
- Arrays use zero-based contiguous indexes:
CONFIG_AUTH_OIDC_ADMIN_GROUPS_0,CONFIG_INSTANCES_0_ID,CONFIG_INSTANCES_0_METRICS_0_URL. - Whole sections accept YAML through
CONFIG_SERVER,CONFIG_STORAGE,CONFIG_UI,CONFIG_AUTH,CONFIG_NOTIFICATIONS,CONFIG_UPDATES,CONFIG_CROWDSEC, orCONFIG_INSTANCES. CONFIG_INSTANCE_*addresses instance0:CONFIG_INSTANCE_NAMEequalsCONFIG_INSTANCES_0_NAME. Metrics index0may also be omitted:CONFIG_INSTANCES_0_METRICS_URLequalsCONFIG_INSTANCES_0_METRICS_0_URL, andCONFIG_INSTANCE_METRICS_URLapplies both shorthands. Do not set equivalent forms together.- Secrets accept a direct string or exactly one
env: NAME/file: PATHreference. Secret overrides also accept_FILE. - Initial direct secret overrides are stored as environment references, never plaintext. Persisted secret references still require their environment variable.
Server, storage, UI, and updates
| YAML field | Default | Purpose | Environment override |
|---|---|---|---|
server.port | 3000 | HTTP listen port. | CONFIGSERVERPORT |
server.basePath | "" | Optional URL prefix such as /crowdsec; no trailing slash. | CONFIGSERVERBASEPATH |
storage.dataDir | /app/data | SQLite database and persistent application state. | CONFIGSTORAGEDATADIR |
storage.geonamesDir | /app/geonames in Docker; ./geonames locally | Local GeoNames snapshot used for location labels. | CONFIGSTORAGEGEONAMESDIR |
storage.walEnabled | true | Enables SQLite write-ahead logging. Set to false for filesystems that do not support WAL. | CONFIGSTORAGEWALENABLED |
ui.timeZone | browser | Browser timezone or an IANA zone such as Europe/Berlin or UTC. | CONFIGUITIMEZONE |
ui.timeFormat | browser | Clock format: browser, 12h, or 24h. | CONFIGUITIMEFORMAT |
ui.readOnly | false | Hides management actions and rejects mutating API operations. | CONFIGUIREADONLY |
updates.enabled | true in packaged images | Enables the built-in update check. | CONFIGUPDATESENABLED |
Authentication
Notifications
| YAML field | Default | Purpose | Environment override |
|---|---|---|---|
notifications.secretKey | Generated and stored | Encrypts saved notification credentials. | CONFIGNOTIFICATIONSSECRETKEY or CONFIGNOTIFICATIONSSECRETKEYFILE |
notifications.allowPrivateAddresses | true | Allows private, loopback, and link-local notification destinations. | CONFIGNOTIFICATIONSALLOWPRIVATEADDRESSES |
notifications.debugPayloads | false | Logs truncated rendered payloads after failed notification delivery. | CONFIGNOTIFICATIONSDEBUGPAYLOADS |
Alert handling
Omittingcrowdsec.alertFilters uses the standard non-CAPI feed. Setting any explicit filter field enables explicit filtering.Global synchronization
Synchronization durations acceptms, s, m, h, or d, for example 500ms, 30s, 5m, or 7d. The lookback fields accept only m, h, or d.| YAML field | Default | Purpose | Environment override |
|---|---|---|---|
crowdsec.sync.lookback | 168h | Imported history and retention window. | CONFIGCROWDSECSYNCLOOKBACK |
crowdsec.sync.refreshInterval | 1m | Active refresh cadence; 0 or manual disables scheduling. | CONFIGCROWDSECSYNCREFRESHINTERVAL |
crowdsec.sync.manualRefreshEnabled | false | Enables manual refresh controls. | CONFIGCROWDSECSYNCMANUALREFRESHENABLED |
crowdsec.sync.idleRefreshInterval | 10m | Refresh cadence while the application is idle; 0 disables it. | CONFIGCROWDSECSYNCIDLEREFRESHINTERVAL |
crowdsec.sync.idleThreshold | 2m | Inactivity before idle refresh behavior begins. | CONFIGCROWDSECSYNCIDLETHRESHOLD |
crowdsec.sync.requestTimeout | 30s | Timeout for individual LAPI requests. | CONFIGCROWDSECSYNCREQUESTTIMEOUT |
crowdsec.sync.bouncerPropagationDelay | 15s | Grace period before deleting alerts owned by expired decisions. | CONFIGCROWDSECSYNCBOUNCERPROPAGATIONDELAY |
crowdsec.sync.deletionQueueMaxAge | 24h | Stops retrying failed queued deletions after this age; 0 disables the limit. Tombstones remain until the retention window passes. | CONFIGCROWDSECSYNCDELETIONQUEUEMAXAGE |
crowdsec.sync.metricsRequestTimeout | 5s | Default timeout for metrics endpoints. | CONFIGCROWDSECSYNCMETRICSREQUESTTIMEOUT |
crowdsec.sync.heartbeatInterval | 30s | CrowdSec machine heartbeat cadence; 0 disables it. | CONFIGCROWDSECSYNCHEARTBEATINTERVAL |
crowdsec.sync.alertSyncChunk | 12h | Historical import window size. | CONFIGCROWDSECSYNCALERTSYNCCHUNK |
crowdsec.sync.alertSyncMinChunk | 15m | Minimum retry window after a timed-out import. | CONFIGCROWDSECSYNCALERTSYNCMINCHUNK |
crowdsec.sync.reconcileWindow | 1h | Fixed alert-history reconciliation window size. | CONFIGCROWDSECSYNCRECONCILEWINDOW |
crowdsec.sync.reconcileRecentAge | 24h | Boundary between recent and older windows. | CONFIGCROWDSECSYNCRECONCILERECENTAGE |
crowdsec.sync.reconcileRecentInterval | 15m | Reconciliation cadence for recent windows. | CONFIGCROWDSECSYNCRECONCILERECENTINTERVAL |
crowdsec.sync.reconcileActiveInterval | 5m | Reconciliation cadence for windows with active decisions. | CONFIGCROWDSECSYNCRECONCILEACTIVEINTERVAL |
crowdsec.sync.reconcileOldInterval | 3h | Reconciliation cadence for older windows. | CONFIGCROWDSECSYNCRECONCILEOLDINTERVAL |
crowdsec.sync.reconcileWindowsPerRefresh | 2 | Maximum due windows processed per refresh. | CONFIGCROWDSECSYNCRECONCILEWINDOWSPERREFRESH |
crowdsec.sync.bootstrapRetryDelay | 30s | Delay between failed initial-sync retries; 0 retries immediately. | CONFIGCROWDSECSYNCBOOTSTRAPRETRYDELAY |
crowdsec.sync.bootstrapRetryEnabled | true | Enables background retry after initial synchronization failure. | CONFIGCROWDSECSYNCBOOTSTRAPRETRYENABLED |
Instances and LAPI
<INDEX>is zero-based.- ID defaults to the index; name defaults to
Instance <INDEX>; authentication type is inferred from credentials. - Inferred values appear as comments in initial YAML unless compatibility requires an explicit identity.
- Explicit IDs use lowercase letters, digits,
_, and-. Keep them stable after importing data.
!IMPORTANT Configure exactly one credential shape:
- Password auth: set
usernameandpassword- mTLS auth: set
certFileandkeyFiletypeis optional and inferred from these fields. Set it explicitly tonone,password, ormtlswhen desired. Do not mix password and mTLS credentials.
Plaintext secrets are supported, but mounted secret files are recommended so credentials do not end up in source control, backups, or configuration-management logs.
Metrics endpoints
<INDEX>selects the instance; zero-based<METRIC_INDEX>selects its endpoint.- Endpoint ID defaults to
<METRIC_INDEX>and name toMetrics <METRIC_INDEX>. - Inferred values appear as comments in initial YAML.
Per-instance synchronization overrides
Every field inherits its corresponding global value when omitted.Multiple CrowdSec instances
Use zero-basedCONFIG_INSTANCES_<INDEX>_* overrides to define each instance. Indexes must be contiguous, starting at 0.services:
crowdsec-web-ui:
image: ghcr.io/theduffman85/crowdsec-web-ui:latest
environment:
CONFIG_INSTANCES_0_ID: eu-prod
CONFIG_INSTANCES_0_NAME: EU Production
CONFIG_INSTANCES_0_LAPI_URL: http://crowdsec-eu:8080
CONFIG_INSTANCES_0_LAPI_AUTH_USERNAME: crowdsec-web-ui
CONFIG_INSTANCES_0_LAPI_AUTH_PASSWORD_FILE: /run/secrets/eu-lapi-password
CONFIG_INSTANCES_0_METRICS_0_ID: lapi
CONFIG_INSTANCES_0_METRICS_0_NAME: EU LAPI
CONFIG_INSTANCES_0_METRICS_0_URL: http://crowdsec-eu:6060/metrics
CONFIG_INSTANCES_1_ID: us-prod
CONFIG_INSTANCES_1_NAME: US Production
CONFIG_INSTANCES_1_LAPI_URL: http://crowdsec-us:8080
CONFIG_INSTANCES_1_LAPI_AUTH_USERNAME: crowdsec-web-ui
CONFIG_INSTANCES_1_LAPI_AUTH_PASSWORD_FILE: /run/secrets/us-lapi-password
# For mTLS, replace instance 1's URL and password credentials above with:
# CONFIG_INSTANCES_1_LAPI_URL: https://crowdsec-us:8080
# CONFIG_INSTANCES_1_LAPI_AUTH_TYPE: mtls
# CONFIG_INSTANCES_1_LAPI_AUTH_CERT_FILE: /certs/us-client-cert.pem
# CONFIG_INSTANCES_1_LAPI_AUTH_KEY_FILE: /run/secrets/us-client-key.pem
# CONFIG_INSTANCES_1_LAPI_TLS_CA_FILE: /certs/us-ca.pem
volumes:
- ./secrets/eu-lapi-password:/run/secrets/eu-lapi-password:ro
- ./secrets/us-lapi-password:/run/secrets/us-lapi-password:ro
# Mount these files when using the commented mTLS configuration:
# - ./certs/us-client-cert.pem:/certs/us-client-cert.pem:ro
# - ./secrets/us-client-key.pem:/run/secrets/us-client-key.pem:ro
# - ./certs/us-ca.pem:/certs/us-ca.pem:ro- Use indexed variables for every instance in a multi-instance setup; reserve the
CONFIG_INSTANCE_*shorthand for single-instance deployments. - Each instance needs a stable ID, unique display name, LAPI URL, and one authentication method.
- Add metrics endpoints with
CONFIG_INSTANCES_<INDEX>_METRICS_<METRIC_INDEX>_*. - Mount password files read-only. When using the optional mTLS configuration, mount its private keys and certificates read-only as well, then restart the container.
YAML alternative
Add entries to the top-levelinstances array. Each entry defines one LAPI connection and zero or more metrics endpoints.instances:
- id: eu-prod
name: EU Production
icon: 🇪🇺
lapi:
url: http://crowdsec-eu:8080
auth:
type: password
username: crowdsec-web-ui
password:
file: /run/secrets/eu-lapi-password
metrics:
- id: lapi
name: EU LAPI
url: http://crowdsec-eu:6060/metrics
auth:
type: bearer
token:
file: /run/secrets/eu-metrics-token
- id: edge-engine
name: EU Edge Engine
url: http://crowdsec-edge:6060/metrics
sync:
requestTimeout: 45s
alertSyncChunk: 6h
- id: us-prod
name: US Production
icon: 🇺🇸
lapi:
url: http://crowdsec-us:8080
auth:
type: password
username: crowdsec-web-ui
password:
file: /run/secrets/us-lapi-password
# For mTLS, replace url and auth above with:
# url: https://crowdsec-us:8080
# auth:
# type: mtls
# certFile: /run/secrets/us-client-cert
# keyFile: /run/secrets/us-client-key
# tls:
# caFile: /run/secrets/us-caConfiguration rules
- Instance and endpoint IDs are unique, URL-safe, and immutable database identities. They are 1–63 characters long, start with a lowercase letter or digit, use only lowercase letters, digits,
_, or-, and must never be reused for another LAPI. - Display names are unique but editable.
iconaccepts up to eight Unicode code points of text or emoji without control characters; omitted icons use colored squares and Combined uses a grid. - Password secrets accept a direct value or exactly one
env/filesource. mTLS requires bothcertFileandkeyFile;tls.caFilecontrols server trust. - Metrics authentication supports
none,basic, andbearer; metrics TLS supportscaFileplus an optional complete client certificate/key pair. - Embedded URL credentials, URL fragments, ambiguous secret sources, partial certificate pairs, unreadable files, and TLS verification bypasses fail validation. LAPI base URLs also reject paths.
- Prefer mounted secret files. Restart after configuration, certificate, or secret changes.
Multi-instance behavior
| Area | Behavior |
|---|---|
| Dashboard, Alerts, Decisions | Support one instance or Combined scope |
| Metrics | Always uses one instance and endpoint; process-local counters are not summed |
| Add decision / clean IP | Runs against every LAPI in Combined scope and reports partial failures |
| Row deletion | Uses the row's owning instance; numeric upstream IDs are never broadcast |
Authentication
Authentication covers the browser UI and protected APIs;/api/health remains public.auth.enabled | Behavior |
|---|---|
auto | Enables authentication for new databases; preserves the state of migrated databases |
true | Requires authentication and initial administrator setup |
false | Disables authentication; this deployment setting is not available in the UI |
Upgraded installations
Enable authentication explicitly on installations migrated from older releases.auth:
enabled: trueLocal accounts
- Password changes and passkey registration/removal from Settings.
- Optional TOTP enrollment through a QR code, mobile setup link, or manual key.
- An enrolled TOTP seed overrides the optional base32
auth.totpSeedfallback. - Administrators can disable password login.
OIDC
Configure OIDC in Settings or YAML.auth:
enabled: true
oidc:
issuerUrl: https://idp.example.com/application/o/crowdsec-web-ui/
clientId: crowdsec-web-ui
clientSecret:
file: /run/secrets/oidc_client_secret
scope: openid profile email
groupsClaim: groups
adminGroups: [crowdsec-admins, secops]
readOnlyGroups: [crowdsec-viewers]
unmatchedRole: denyCallback URL
Register this callback URI with the identity provider.https://<crowdsec-web-ui-host>/api/auth/oidc/callbackRequirements and roles
- The callback must exactly match the public scheme, host, port, and base path. For
basePath: /crowdsec, usehttps://<host>/crowdsec/api/auth/oidc/callback. - Reverse proxies must forward
HostorX-Forwarded-HostandX-Forwarded-Proto. - Saved Settings override YAML. Scopes must include
openid; add provider-specific scopes such asgroupsonly when required. - Admin-group matches have full access; read-only-group matches can view data and keep permitted preferences; unmatched users follow
auth.oidc.unmatchedRole(denyby default). - Set an unmatched fallback role only when every user who can sign in should receive it.
ui.readOnly: trueoverrides all roles for the deployment. It blocks CrowdSec writes, refresh changes, notification destination/rule management, test sends, and notification deletion. Language changes and marking notifications read remain available. This is not per-user RBAC.- Identities use stable issuer and subject claims. Username collisions with local accounts remain separate.
- Sessions have a 24-hour absolute lifetime. OIDC-only users cannot add local passkeys; password-backed local accounts retain passkey support.
- Existing OIDC rows migrate on their next successful SSO login.
Deployment and Security
Trusted IPs for Alert Deletion
!IMPORTANT
CrowdSec only permits alert deletion when the request comes from loopback or an address listed in api.server.trusted_ips. Registering the Web UI as a CrowdSec machine authenticates it, but does not grant this IP-based permission.When the Web UI runs in Docker, add the Web UI container's source IP or, preferably, its Docker network CIDR to CrowdSec's
/etc/crowdsec/config.yaml. This is not the browser's IP or the Docker host's public IP. Without this entry, decision operations can still work while alert deletion fails with 403 Forbidden.api:
server:
trusted_ips:
- 127.0.0.1
- ::1
- 172.16.0.0/12 # Docker default bridge networkUse the narrowest CIDR that contains the Web UI container and LAPI network. Container IPs can change when containers are recreated, so the Docker network CIDR is usually more reliable than one container IP. Restart CrowdSec after updating the file. The current CrowdSec container does not provide a
TRUSTED_IPS environment override. See the CrowdSec configuration reference.Local or Custom LAPI Certificate
A self-signed certificate or internal CA may produce the following error.Login failed: unable to get local issuer certificateMount the CA certificate and configure it for the LAPI instance.
services:
crowdsec-web-ui:
image: ghcr.io/theduffman85/crowdsec-web-ui:latest
container_name: crowdsec_web_ui
ports:
- "3000:3000"
environment:
CONFIG_INSTANCE_LAPI_URL: https://crowdsec:8080
CONFIG_INSTANCE_LAPI_AUTH_USERNAME: crowdsec-web-ui
CONFIG_INSTANCE_LAPI_AUTH_PASSWORD_FILE: /run/secrets/crowdsec_password
CONFIG_INSTANCE_LAPI_TLS_CA_FILE: /certs/root_ca.crt
secrets:
- crowdsec_password
volumes:
- ./data:/app/data
- /path/on/host/root_ca.crt:/certs/root_ca.crt:ro
restart: unless-stopped
secrets:
crowdsec_password:
file: ./secrets/crowdsec_password.txtKeep the CA mount read-only.
CONFIG_INSTANCE_LAPI_TLS_CA_FILE maps to instances[0].lapi.tls.caFile; no image rebuild is needed.HTTPS Reverse Proxy
CrowdSec Web UI listens for HTTP on port3000 and does not obtain or terminate TLS certificates itself. Put a reverse proxy in front of it for HTTPS deployments and keep port 3000 private to the proxy.HTTPS is required for passkeys because browsers expose WebAuthn only in a secure context.
http://localhost is suitable for local testing, but a remote deployment needs a stable hostname and a certificate trusted by the browser. Passkeys registered for one hostname cannot be used from a different hostname.The proxy must preserve
Host (or set X-Forwarded-Host) and set X-Forwarded-Proto to the original scheme. The application uses these headers for WebAuthn origins, secure session cookies, OIDC callback URLs, and mutation-origin checks.Traefik example
This minimal example assumes Traefik already has awebsecure entrypoint, a Let's Encrypt resolver named letsencrypt, and an external Docker network named proxy. Add the labels and proxy network to the existing Web UI service, replacing the hostname and CrowdSec settings.services:
crowdsec-web-ui:
image: ghcr.io/theduffman85/crowdsec-web-ui:latest
expose:
- "3000"
environment:
CONFIG_INSTANCE_LAPI_URL: http://crowdsec:8080
CONFIG_INSTANCE_LAPI_AUTH_USERNAME: crowdsec-web-ui
CONFIG_INSTANCE_LAPI_AUTH_PASSWORD: your-crowdsec-password
# For https://crowdsec.example.com/crowdsec/:
# CONFIG_SERVER_BASE_PATH: /crowdsec
volumes:
- ./data:/app/data
labels:
- traefik.enable=true
- traefik.docker.network=proxy
# For /crowdsec/, append: && PathPrefix(`/crowdsec`)
- 'traefik.http.routers.crowdsec-web-ui.rule=Host(`crowdsec.example.com`)'
- traefik.http.routers.crowdsec-web-ui.entrypoints=websecure
- traefik.http.routers.crowdsec-web-ui.tls.certresolver=letsencrypt
- traefik.http.services.crowdsec-web-ui.loadbalancer.server.port=3000
networks:
- proxy
- crowdsec
restart: unless-stopped
networks:
proxy:
external: true
crowdsec:
external: true
name: your_crowdsec_networkTraefik supplies the forwarded headers and WebSocket upgrade handling automatically. See the Traefik ACME documentation if the
letsencrypt resolver is not configured yet.Nginx example
This equivalent example assumes Nginx already terminates HTTPS and the Web UI port is published only on loopback, for example127.0.0.1:3000:3000.# For https://crowdsec.example.com/crowdsec/, set
# CONFIG_SERVER_BASE_PATH=/crowdsec and replace both `/` paths below
# with `/crowdsec/`.
location / {
proxy_pass http://localhost:3000/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}Proxy requirements
- The base path starts with
/and has no trailing slash. /redirects to it; APIs, assets, and navigation follow it automatically.- With Traefik on a shared hostname, add
CONFIG_SERVER_BASE_PATH: /crowdsecand use a router rule such as `Host(example.com) && PathPrefix(/crowdsec). Do not configureStripPrefix`; the application expects to receive the base path. - The backend checks browser mutation origins, applies a Content Security Policy, limits API bodies to 1 MiB, and marks API responses
private, no-store. - Command-line and service clients without browser
OriginandSec-Fetch-Siteheaders remain compatible. - Configure HSTS at the TLS-terminating proxy; the application does not emit it.
Health Check
The public endpoint isGET /api/health. Startup does not wait for LAPI: bootstrap retries in the background, so the container can become healthy before synchronization completes.curl http://localhost:3000/api/health
# {"status":"ok"}The built-in check runs every 30 seconds with a five-second timeout, a 10-second start period, and three retries.
docker inspect --format='{{.State.Health.Status}}' crowdsec_web_uiserver.basePath does not affect the internal check at localhost:3000/api/health. If server.port changes, update the health-check command and port mapping.Runtime Behavior
Prometheus Metrics Page
- Reads a configured raw CrowdSec Prometheus endpoint. The Web UI does not configure one by default; CrowdSec normally exposes its local scrape at
http://127.0.0.1:6060/metrics. - Shows remediation-component and log-processor LAPI activity, AppSec, parsers and datasources, scenarios, LAPI latency, parsing time, and whitelist hits. Log-processor activity uses
POST /v1/alerts; CrowdSec builds that expose an exact last-heartbeat timestamp also provide the processor health badge. - Alert and decision analytics remain on the main dashboard.
Enable full metrics in CrowdSec's
/etc/crowdsec/config.yaml.prometheus:
enabled: true
level: full
listen_addr: 127.0.0.1
listen_port: 6060For separate containers, bind
listen_addr: 0.0.0.0 on a trusted network, then configure the matching Web UI instance.environment:
CONFIG_INSTANCE_METRICS_URL: http://crowdsec:6060/metrics| CrowdSec level | Result |
|---|---|
full | All supported details, including per-machine, per-bouncer, and per-node metrics |
aggregated | Less detail; omits those per-entity metrics |
none | Disables metrics registration |
AppSec and latency sections appear only when CrowdSec emits those metrics. Time-window-only
rate()/increase() metrics are intentionally omitted. See the CrowdSec Prometheus documentation.Display Preferences
crowdsec.simulationsEnabled: truefetches non-remediating simulation alerts/decisions and shows badges, filters, and dashboard counts. Default:false.- Alerts and Decisions column layouts persist per browser profile in local storage.
ID,Machine, andOriginare hidden by default.Machineprefersmachine_alias, thenmachine_id; multiple alert decision origins display asMixed.- Hidden columns remain searchable through fields such as
id:,machine:, andorigin:.
Quick Filters
Dashboard, Alerts, and Decisions share one count-aware Quick Filters drawer. Its trigger shows the number of active selections, and the clear control in the drawer header resets all stored quick filters.Instance and machine options use stable IDs for filtering while displaying their configured names or aliases. Country, instance, and machine searches match the displayed label as well as the stored value. Alerts with several origins or targets contribute to each distinct facet option instead of exposing a combined bucket.
- Filter selections, date range, and simulation mode persist in local storage for the current browser profile and are restored when navigating between the three pages.
- Changing a structured search field updates the matching quick-filter selection, and changing a quick filter updates the structured search. A field supplied explicitly in a page URL takes precedence over its stored value; stored values fill fields that are absent.
- Dashboard top countries, scenarios, AS numbers, targets, the world map, and the Activity History range update the same shared state. Changes made in the drawer update those widgets in return.
- Filtered Dashboard alert and active-decision counts use the same indexed predicates as the Alerts and Decisions lists. The alert card links with the alert-compatible query, while the decision card links with the decision-compatible query.
- The drawer follows each table's configured column order. Filters for hidden columns are grouped under Hidden columns.
- Page-only filters remain visible under Unavailable when another page cannot apply them. Alerts lists
Action,Status, andAlertthere; Decisions listsDecision; Dashboard lists all four. Their stored selections are preserved for the page that supports them and can be cleared from any drawer. - Facet selections use exact equality. Selecting the target
tausend.megeneratestarget=tausend.meand does not includebw.tausend.me. Manually enteringtarget:tausend.meremains a broader contains search. - Each facet reports counts after the other active filters have been applied. Use the facet search control to find values beyond the initially loaded list.
Dashboard applies the shared fields
Country, Scenario, AS, IP / Range, Target, ID, Instance, Region, City, Machine, and Origin. Filters that depend on decision-only or alert-list-only data are retained in Unavailable instead of being silently discarded.Active decisions are deduplicated by instance, value, and simulation mode. When filters exclude the globally preferred decision, the best matching decision is promoted so enabling Hide duplicates cannot make an otherwise matching duplicate group disappear.
Search Syntax
| Syntax | Example |
|---|---|
| Free text / quoted phrase | ssh hetzner, "nginx bf" |
| Field contains / exact value | country:germany, country=DE |
| Date comparison | date>=2026-03-24, date<2026-03-25T12:00:00Z |
| Negative / empty | -sim:simulated, sim<>simulated, origin:"", origin<>"" |
| Boolean / grouping | country:(germany OR france) AND -sim:simulated |
| Decision filters | status:active AND action:ban, alert:123 OR ip:"192.168.5.0/24" |
The
: operator performs a case-insensitive contains match, while = matches the complete field value. Quick Filter selections use =. Date fields support <, >, <=, >=, and =>. A bare field name is free text unless followed by :. Quote literal AND, OR, or NOT. The search Info button lists page-specific fields and examples.Examples
| Page | Query |
|---|---|
| Alerts | country:germany ssh |
| Alerts | date>=2026-03-24 AND date<2026-03-25 |
| Alerts | country:(germany OR france) AND -sim:simulated |
| Alerts | origin:"" |
| Decisions | status:active AND action:ban |
| Decisions | date>=2026-03-24 AND action:ban |
| Decisions | alert:123 OR ip:"192.168.5.0/24" |
Alert Source Filtering
Limit the local cache by origin when CrowdSec ingests automation, blocklists, or community feeds.crowdsec:
alertFilters:
includeOrigins: [crowdsec, cscli-import]
excludeOrigins: [cscli]
includeCapi: true
includeOriginEmpty: true
excludeOriginEmpty: false| Origin | Source |
|---|---|
crowdsec | Security-engine decisions |
cscli | Manual cscli decisions add |
cscli-import | cscli decisions import |
lists | Imported list feeds |
CAPI | Central API / community blocklist |
Behavior
- No explicit filters fetches the normal non-CAPI/non-lists feed.
- Includes are pushed upstream where possible. Generic excludes and empty-origin handling run locally because LAPI lacks those filters.
includeCapi: trueadds CAPI to the default feed;includeOrigins: [CAPI]selects only CAPI.- If any origin is excluded, the whole alert is dropped.
- Origins prefer associated decisions, then blocklist/list source scopes for alerts without decisions.
includeOriginEmptyretains origin-less alerts alongside includes;excludeOriginEmptyremoves them.- Because Decisions is built from synchronized alerts, filters also change which imported decisions appear.
Examples
| Setting | Result |
|---|---|
includeOrigins: crowdsec | Keeps security-engine alerts only |
includeOrigins: lists | Keeps list-based alerts only |
includeCapi: true | Adds CAPI to the default feed |
includeOrigins: CAPI | Keeps CAPI alerts only |
includeOriginEmpty: true | Keeps origin-less alerts alongside explicit includes |
excludeOriginEmpty: true | Removes origin-less alerts |
excludeOrigins: cscli, lists | Removes manual and imported-list alerts |
Notifications
Rules run against locally cached CrowdSec data, create in-app notifications, record delivery status, and optionally deliver outbound messages.Rules
Every rule has a name, severity (info, warning, critical), incident deduplication, and destination channels. Alert rules filter scenario, target, and simulation state; IP Ban and New Alert/Decision also accept exact IP/CIDR filters.| Rule type | Behavior |
|---|---|
Alert Spike | Compares the current window with the previous window and triggers when percentage increase and minimum alert count are exceeded. |
Alert Threshold | Triggers when matching alerts in the configured time window reach the threshold. |
New Alert/Decision | Creates one notification for every matching alert, decision, or both within the lookback window. Includes record ID, timestamps, scenario, target, source/value, and related alert/decision details. Stable per-record deduplication prevents repeats. |
IP Ban | Triggers once for each active ban decision in the configured window, supports exact IP/CIDR filters, and deduplicates duplicate active decision rows for the same ban. |
Recent CVE | Extracts CVE IDs from matching alerts and checks publication age before notifying. |
LAPI Availability | Triggers when CrowdSec LAPI stays unavailable past the outage threshold, with optional recovery notifications. |
Application Update | Uses the built-in update check and triggers when a newer CrowdSec Web UI version is available. |
Multi-instance behavior
| Scope | Rules |
|---|---|
| Aggregate matching alerts across instances | Alert Spike, Alert Threshold, Recent CVE |
| Evaluate each matching record | New Alert/Decision, IP Ban |
| Evaluate each instance | LAPI Availability |
| Application-wide | Application Update |
Instance-backed titles and metadata identify the contributing instance or instances.
!NOTE TheRecent CVErule queries the NVD API to determine when a CVE was published. If outbound access toservices.nvd.nist.govis blocked, recent-CVE notifications may be skipped.
Destinations
Destinations are independently enabled and reusable across rules. Send Test validates saved settings immediately; results are stored asdelivered or failed.| Destination | Settings |
|---|---|
SMTP host/port/security (Plain SMTP, STARTTLS, SMTPS / Implicit TLS), optional user/password, from address, comma-separated recipients, importance (auto, normal, important), and optional insecure TLS for trusted self-signed SMTP endpoints. Auto importance maps info to normal and warning/critical to important. | |
| Gotify | Gotify URL, app token, and priority (auto or explicit integer). Auto priority maps info to 5, warning to 7, and critical to 10. |
| ntfy | Server URL, topic, optional access token, and priority (auto, min, low, default, high, urgent). Auto priority maps info to default, warning to high, and critical to urgent. |
| MQTT | Generic publish-only output with broker URL, optional username/password/client ID, QoS 0 or 1, keepalive, connect timeout, topic, and retain flag. It does not include Home Assistant discovery, entity sync, or command handling. |
| Webhook | Custom HTTP delivery with method (POST, PUT, PATCH), URL, optional query parameters/headers, auth (none, bearer token, or basic auth), body mode (JSON, Text, Form), timeout, retries, retry delay, and optional insecure TLS for trusted self-signed HTTPS endpoints. |
Payloads and security
- MQTT JSON contains
title,message,severity,metadata,sent_at,channel_id,channel_name,channel_type,rule_id,rule_name, andrule_type. Tests userule_id=test,rule_name=Test notification, andrule_type=test. - Webhook templates expose dotted
event.*fields fortitle,message,severity,metadata,sent_at,channel_name,rule_id,rule_name, andrule_type. Each has a*Jsonvariant; nullable rule fields also haveOrUnknownandOrUnknownJsonaliases. - Failed webhooks store HTTP status and a truncated response.
notifications.debugPayloads: truealso logs a truncated rendered body with sensitive form fields redacted; enable it only while troubleshooting. - Destination secrets are masked and encrypted by
notifications.secretKey, or an auto-generated key stored in application metadata. notifications.allowPrivateAddresses: falseblocks private, loopback, and link-local destinations; default:true.- Telegram, Home Assistant discovery/state, and inbound MQTT commands are not supported.
Kubernetes
A Helm chart for CrowdSec Web UI is maintained by zekker6.Persistence and Alert History
SQLite data lives under/app/data. Mount the directory—not only crowdsec.db—because WAL mode also uses crowdsec.db-wal and crowdsec.db-shm.volumes:
- ./data:/app/data- History survives restarts, merges with new LAPI data, and expires after
crowdsec.sync.lookback(default: seven days). - Initial imports and reconciliation retry in smaller windows after timeouts.
- During LAPI outages, the application serves its available cache and retries in the background; partial imports are marked.
Use
POST /api/cache/clear for a full cache reset. Synchronization internals are documented in DEVELOPMENT.md.Documentation
| Guide | Contents |
|---|---|
| Configuration example | Complete commented YAML configuration |
| API reference | Authentication, routes, parameters, and request/response shapes |
| Development guide | Local setup, builds, tests, metadata, translations, and synchronization internals |
| Load testing guide | Synthetic profiles, overrides, benchmarks, and container workflow |
Star History
Related Projects
Check the logs first
Nine times out of ten the logs tell you exactly what went wrong.
- In Portainer, go to Containers, click the container, then Logs. Or run
docker logs <container> - Exit codes help too:
137means killed, usually out of memory.126or127means the command inside the image is broken.
Permission denied on volumes
If the logs show "permission denied", the app can't write to its data folder on the host.
- Fix the ownership:
sudo chown -R 1000:1000 /portainer/Files/AppData/Config/crowdsec-web-ui/data
Image won't pull
Test the pull directly on the host: docker pull ghcr.io/theduffman85/crowdsec-web-ui:latest
- "manifest unknown" means the tag no longer exists. This template uses
latest, so try pinning a specific version instead. - "toomanyrequests" is the Docker Hub rate limit. Log in with
docker loginto raise it. - "no space left on device" means a full disk. Reclaim space with
docker system prune
"exec format error"
This means the image was built for a different CPU architecture than your server.
- This image supports:
amd64, arm64 - Check yours with
uname -m: x86_64 is amd64, aarch64 is arm64. Raspberry Pi and other ARM boards are the usual culprits.
Container keeps restarting
The unless-stopped restart policy relaunches the app after every crash, so the real error can scroll past.
- Check the logs right after a restart, the last few lines before it died are the useful ones.
- Get the exit code with
docker inspect <container> --format '{{.State.ExitCode}}' - Still stuck? Redeploy once with the restart policy set to
noso the failure stays visible.
Stack won't deploy
Compose stacks fail fast on small mistakes, and Portainer shows the reason just above the editor.
- YAML only accepts spaces for indentation, a single tab breaks the whole file.
Raise an issue
Found something which isn't working as it should? Here's how to report it.
- Bug within the app: Open an issue on theduffman85/crowdsec-web-ui
- Template not working: Open an issue on xneo1/portainer_templates
- This website not working: Open an issue on lissy93/portainer-templates
A Compose stack
Crowdsec WEB UI is a Compose stack, a set of containers defined in one file and brought up together by Portainer, then started and stopped as a single app.
The app image
An image is the app packed up ready to go, everything Crowdsec WEB UI needs bundled into one download. This template pulls ghcr.io/theduffman85/crowdsec-web-ui:latest, which Docker fetches once (about 131 MB) and then starts your own copy from.
Where the image comes from
Docker pulls its images from registries, public libraries of ready-built apps. Crowdsec WEB UI's comes from the GitHub Container Registry, published by theduffman85.
Version tags
The bit after the colon in the image name is the version tag. Here it's latest, which always points at the newest build, so a redeploy can bump you to a newer release without you asking. Pin a specific tag if you would rather stay on one version.
Which machines it runs on
Every image is built for particular CPU types. This one ships for amd64, arm64, so it runs on both regular x86 servers and ARM boards like a Raspberry Pi.
Volumes
A volume is where Crowdsec WEB UI keeps its files so they survive an update or a restart. Without one, anything it saves would sit inside the container and vanish the moment it's recreated. This template mounts:
/app/datafrom/portainer/Files/AppData/Config/crowdsec-web-ui/dataon the host
Environment variables
Environment variables are the settings you hand over when you deploy, things like a password or a timezone. Crowdsec WEB UI takes 9 of them, all with defaults you can leave alone or tweak:
CROWDSEC_URL, defaults tohttp://crowdsec:8080CROWDSEC_USER, defaults tocrowdsec-web-uiCROWDSEC_PASSWORD, pulled from your own environmentCROWDSEC_LOOKBACK_PERIOD, defaults to168hCROWDSEC_REFRESH_INTERVAL, defaults to30sCROWDSEC_IDLE_REFRESH_INTERVAL, defaults to5mCROWDSEC_IDLE_THRESHOLD, defaults to2mCROWDSEC_FULL_REFRESH_INTERVAL, defaults to5mBASE_PATH, pulled from your own environment
Restart policy
The restart policy here is unless-stopped, so Docker restarts Crowdsec WEB UI after a crash or reboot, but leaves it off when you stop it on purpose. You can change this on the deploy screen. The choices are no (never restart), on-failure (only after a crash), unless-stopped (restart unless you stop it), and always (bring it back no matter what).
Networking
Nothing custom is set, so Crowdsec WEB UI sits on Docker's default bridge network: its own private space that reaches the outside world only through the ports it publishes.
Container name
Once it's deployed, Portainer names the container crowdsec-web-ui. That's what you'll spot in the containers list and use in commands like docker logs crowdsec-web-ui.
Platform
The platform is linux, the kind of system the container is built to run on. Docker and Portainer handle this on a normal Linux server.
Open source license
Crowdsec WEB UI is open source, released under the AGPL-3.0 license. In plain terms the code is out in the open, so you're free to run it and change it to fit what you need.
Portainer app templates
Zooming out, this whole page comes from a Portainer app template: a short recipe telling Portainer how to set Crowdsec WEB UI up. Add the template list to Portainer once, then deploying Crowdsec WEB UI is a click rather than a wall of config.











