Self-hosted service
TheDuffman85/crowdsec-web-ui avatar
TheDuffman85/crowdsec-web-ui

crowdsec-web-ui: a dashboard that can write bans, and leaves old installs open

A self-hosted dashboard for CrowdSec: investigate alerts, manage decisions, monitor runtime metrics, and send notifications from one responsive UI.

739 stars37 forksTypeScriptAGPL-3.0

At a glance

What is it?
TheDuffman85/crowdsec-web-ui is a React and Hono dashboard for CrowdSec that connects over a watcher password or agent mTLS, can create manual bans, and states in its own documentation that installations migrated from before authentication was added stay unauthenticated until an operator explicitly enables it.
Who is it for?
crowdsec-web-ui is a serious piece of work for anyone running CrowdSec on their own hardware, and the details that matter are the unglamorous ones. Two authentication routes, one of which is certificate-based.
Can I use it commercially?
Yes, with strict conditions. AGPL-3.0 is a network copyleft licence: if people use a modified version over a network, for example as a hosted service, you must offer them its source code under the same licence.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 2, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The watcher password route puts a credential on the command line

The quick start assumes you already have a CrowdSec LAPI running, and there are two ways to attach to it. The first registers the interface as a machine with a watcher password:

bash
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/null

The important instruction is the last flag. The documentation marks it IMPORTANT and says to keep `-f /dev/null`, because that registers the machine without overwriting the CrowdSec container's existing credentials file. Omit it and you can invalidate the credentials your running instance depends on. The second route is agent mTLS: configure LAPI TLS authentication, create a client certificate and key pair, and point the interface at them with `CONFIG_INSTANCE_LAPI_AUTH_CERT_FILE` and `CONFIG_INSTANCE_LAPI_AUTH_KEY_FILE`.

Upgraded installations stay unauthenticated until you enable auth

One sentence in the documentation deserves to be read twice. Alongside a CAUTION marker about using HTTPS and a hardened reverse proxy for public deployments, it states that migrated installations which predate authentication remain unauthenticated until explicitly enabled. So an operator who had a version of this dashboard before the security layer existed, and then upgraded, has a working admin interface with no authentication on it, and nothing in the interface forces the issue. The same note covers the other half of the problem: the built-in authentication protects the UI and the API, but TLS terminates outside the application, so the application is not defending the connection. For single sign-on, three providers are named as supported integrations, Authentik, Authelia, and Keycloak, alongside password and TOTP login, passkeys, group roles, and an instance-wide read-only mode.

The dashboard can create bans, which is why read-only mode exists

The feature table splits into nine areas, and one of them writes rather than reads. Decisions covers active and expired decisions, persistent count-aware quick filters, duplicate hiding, manual bans with custom durations and reasons, and cleanup actions. Notifications can then fire rules for alerts, decisions, CVEs, availability, and updates through Email, Gotify, MQTT, ntfy, or webhooks. So the same interface that shows you which IPs are being banned can also create the ban, with a duration you choose and a reason string. That is the point of the tool for some people and the risk for others, which is why read-only mode is a first-class setting rather than a hidden flag: it appears in the security row as instance-wide, and in the configuration example as a `readOnly` field inside the UI section. Decide which one you want before you hand out the URL.

Configuration arrives as YAML inside environment variables

The configuration story is unusual enough to be worth reading before you copy the compose file. Values can be supplied two ways. A whole section can be given as inline YAML, for example a server section with a port and a base path, a storage section with a data directory and a GeoNames directory and write-ahead logging enabled, or a UI section carrying the time zone, time format, date format, and the read-only flag. Alternatively individual fields can be overridden one at a time, including array entries written with zero-based indices such as `CONFIG_AUTH_OIDC_ADMIN_GROUPS_0`. Everything is parsed as YAML and applied in memory over the config file, which lives at `/app/data/config.yaml` in Docker and `./data/config.yaml` locally. One extra switch decides whether validated overrides are written back to disk: `CONFIG_PERSIST_OVERRIDES`. Set it false and your environment stays the source of truth.

Reverse geocoding works offline because the dataset ships in the image

The Dockerfile caches a compact GeoNames dataset during the build, and the comment on that line states the reason plainly: so reverse geocoding never depends on network access at runtime. That matters more than it sounds, because the alerts and decisions views include IP, autonomous system, and location details. The rest of the storage story is deliberately small. SQLite through better-sqlite3 under `/app/data`, which is the directory the compose file mounts from `./data`, and write-ahead logging on by default in the example configuration. The native module has its own guard: an `ensure:native-deps` script runs before `start`, before `dev`, and before the server test suite, so the compiled binding is rebuilt whenever the platform needs it rather than failing at the first query.

Several CrowdSec instances get a Combined scope

The multi-instance row is more than a connection list. You can register several CrowdSec LAPIs, switch between them with per-instance views, and get a Combined scope that spans the Dashboard, the Alerts view, and the Decisions view at once. That is the configuration shape to look at if you run more than one instance, because the same `CONFIG_INSTANCES` value accepts a YAML list of objects each carrying an identifier, a display name, and a LAPI URL. The testing side matches the ambition: the root carries a load-testing script, a dedicated Docker entrypoint for load tests, and `LOAD_TESTING.md`, and the package scripts include seeding a load-test dataset, running a load-test server, benchmarking the multi-instance path, and building the client for that run. Ten display languages ship with the interface as well.

The build pins Node 24.20.0 and writes the commit into the bundle

The container starts from an exact Node patch on Debian trixie-slim, installs pnpm at an exact version globally, and configures npm fetch retries and timeouts before doing anything else. The builder installs `python3`, `make`, and `g++` for the native module, copies the workspace manifests first so dependency layers cache, runs `pnpm install --frozen-lockfile --prod=false`, then copies the source and the build configuration. Five build arguments are declared before the build step, holding a commit hash, a build date, a repository URL, a branch, and a version, with a comment explaining why the order matters: Vite bakes `VITE_*` variables into the static bundle at build time. The build itself is `vite build`, then `tsup`, then `pnpm prune --prod`. The runtime runs as the non-root `node` user. One oddity: `package.json` carries version `0.0.0` while the releases are dated `2026.9.2`, `2026.9.1`, and `2026.8.3`.

Three Vitest configs and a smoke test against a real CrowdSec

Testing is split three ways. There is a root `vitest.config.ts` plus a separate server config and a separate client config, and the `test` script runs the server suite and then the client suite, each under its own config file. Coverage has the same split with a check step on top, and the server coverage script has the same `ensure:native-deps` pre-hook as the rest. The most interesting script is the mTLS one, which runs `node --experimental-strip-types` against a script named for a CrowdSec smoke test, meaning the certificate authentication path is exercised against something rather than mocked. The runtime floor is pinned exactly as well, with the engine requiring Node `24.20.0` and pnpm `11.17.0`, and the root carries both a `.node-version` and an `.nvmrc` alongside shell and PowerShell run scripts for local work.

Editorial conclusion

crowdsec-web-ui is a serious piece of work for anyone running CrowdSec on their own hardware, and the details that matter are the unglamorous ones. Two authentication routes, one of which is certificate-based. SQLite with write-ahead logging for local state. Ten locales. A GeoNames dataset baked into the image so reverse geocoding never needs the network at runtime. And a build that pins an exact Node patch and writes the commit hash into the client bundle, so the version in the interface tells you which build produced it. Two things to do before you point anything at a real instance. Read the warning about migrated installations remaining unauthenticated, and decide whether you want the interface able to create bans at all, because the Decisions area can act on your CrowdSec instance rather than only report on it. If the answer is that you want reporting only, turn on the instance-wide read-only mode before you hand anyone else the URL.

Frequently asked questions

What is CrowdSec, in the terms this dashboard uses?

CrowdSec is the system this dashboard reads and acts on. The interface talks to its Local API, registers itself as a machine, and works with the objects CrowdSec produces: alerts and their contexts, decisions with durations and reasons, bouncers, AppSec, parsers, whitelists, and simulations.

Can I host a self-hosted CrowdSec dashboard?

Yes. The project is described as a self-hosted dashboard for CrowdSec, and the quick start is a Compose file that runs the image `ghcr.io/theduffman85/crowdsec-web-ui:latest` on port 3000 with a mounted `./data` directory. You need a running CrowdSec LAPI and either a watcher password or agent mTLS to connect to it.

How do I connect the CrowdSec Web UI to a CrowdSec instance?

Either register it as a machine with a watcher password generated by `openssl rand -hex 32`, or configure LAPI TLS authentication and give it a client certificate and key. The watcher route needs `-f /dev/null` kept on the `cscli machines add` command so the container's existing credentials file is not overwritten.

Where does the CrowdSec Web UI keep its data?

In SQLite through better-sqlite3 under `/app/data`, mounted from `./data` in the compose file, with write-ahead logging enabled by default. The GeoNames dataset used for reverse geocoding is downloaded and baked into the container image during the build so it never needs the network at runtime.

Official sources

  1. Issues
  2. License: AGPL-3.0
  3. README
  4. Releases
  5. TheDuffman85/crowdsec-web-ui on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/theduffman85-crowdsec-web-ui.svg)](https://hysenlabs.com/projects/theduffman85-crowdsec-web-ui)