Self-host

Runs on your infrastructure

One published container image serves the API and the dashboard on a single port, with the database in a directory you mount and login built in. The dashboard loads no third-party fonts, scripts or analytics.

U0 · Read first

Security notes

  • Login is on from the first start. There's no default password. A new instance prints a one-time setup code to its log, and creating the account needs it, so whoever reaches the instance first can't claim it without also being able to read its logs. Repeated failed sign-ins are slowed down, up to 15 minutes between attempts.
  • One account, no roles. Anyone signed in, and any API token, can scan, sweep subnets and edit remediation records. Tokens can't change the password or manage other tokens.
  • Plain HTTP exposes credentials. CveDeck doesn't serve HTTPS itself. Over HTTP, the password, the session cookie and the SSH credentials you scan with all cross the network readable, so put a TLS reverse proxy in front before exposing it beyond a network you trust.
  • Each host's SSH key is pinned on first contact. A host that later presents a different key is refused before it sees a credential. The first connection is still trust on first use, so on hosts where it matters, compare the fingerprint on the machine's page against ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub run on the host, or set CVEDECK_SSH_HOST_KEY_POLICY=strict to refuse any host that has no pin yet.
  • Use least privilege. The collector only runs read-only commands, so give it its own unprivileged account on each target, with no sudo.
  • Login can be turned off, with CVEDECK_AUTH=disabled, for instances that already sit behind something that authenticates people, such as Authelia, Authentik or oauth2-proxy. Everything is then open to anyone who can reach the port.
U1 · Demo

Try it with demo data

Demo mode seeds a fictional fleet into an empty database and refuses the routes that reach the network: scans, discovery sweeps, connection tests, feed refreshes and forgetting a host key. Leave it off on any instance you actually scan with.

docker run -d --name cvedeck-demo \
  -p 127.0.0.1:3325:8000 \
  -e CVEDECK_DEMO_MODE=true \
  ghcr.io/reprodev/cvedeck:latest

Then open http://localhost:3325. Demo mode needs no login, and it arrives with its own fictional threat intel already loaded, so the ranking is there on the first paint. It won't fetch real feeds: a refresh is refused before anything is downloaded, whether it's asked for over the API or from cvedeck-admin inside the container, and the dashboard paints no button for it.

The fleet includes a partially checked host, a host whose SSH key changed, failed scans and hosts nobody has scanned, because those are the states worth seeing. What each one means.

U2 · Install

Scan your own fleet

Docker

Mount a data directory. The database is written there and survives container replacement.

docker run -d --name cvedeck \
  -p 127.0.0.1:3325:8000 \
  -v /srv/cvedeck:/data \
  -e PUID="$(id -u)" -e PGID="$(id -g)" \
  --restart unless-stopped \
  ghcr.io/reprodev/cvedeck:latest

On first start there's no account. Get the one-time setup code from the log:

docker logs cvedeck

Open http://localhost:3325, enter the code, and choose a username and a password of at least 12 characters. To create the account from configuration instead, set CVEDECK_ADMIN_USERNAME and CVEDECK_ADMIN_PASSWORD_FILE.

Then add hosts by address, such as 192.168.1.50, or sweep a subnet such as 192.0.2.0/24 to find hosts to enrol. To build the image from source instead, run docker build -t cvedeck . in a checkout and use cvedeck as the image name.

Docker Compose

From a checkout, copy the example environment file and set the host port, data directory and user IDs. The port defaults to 3325.

cp .env.example .env
docker compose up -d
docker compose logs

The last command shows the one-time setup code for creating the account.

Native Linux (systemd)

For hosts without Docker. Needs Python 3.11 or later, plus Node and npm to build the dashboard. The service binds to 127.0.0.1:8000 only, ready for a TLS reverse proxy, and the installer adds a daily feed-refresh timer.

git clone https://github.com/reprodev/cvedeck
cd cvedeck
sudo ./deploy/install.sh

Find the setup code in the service log:

journalctl -u cvedeck | grep -A4 'no account yet'
U3 · Intel

Keep threat intel fresh

CveDeck refreshes the CISA KEV and FIRST EPSS feeds itself, every 24 hours, starting at start-up. Both publish daily, so that's enough, and there's nothing to set up.

CVEDECK_FEED_REFRESH_HOURS changes the interval. Set it to 0 to switch the built-in refresh off and drive it from your own scheduler — which is what the native installer does, so its systemd timer is the only thing refreshing. Don't run both. If you add a cron entry while the built-in refresh is still on, the feeds are fetched twice on two different clocks. GET /api/health reports the interval actually running.

Driving it yourself, from the host, with the built-in refresh switched off. It runs inside the container, so it needs no API token. If you set a different PUID/PGID, use those in place of 1000:1000 here and below.

17 3 * * * docker exec --user 1000:1000 cvedeck cvedeck-admin refresh-feeds >/dev/null

Check each feed's age, record count and staleness. A refresh that finds the published file unchanged since last time reports unchanged and does no further work, which is a different fact from a feed rewritten with identical numbers. The API needs a token, created under Settings in the dashboard:

curl -fsS -H "Authorization: Bearer $CVEDECK_TOKEN" http://localhost:3325/api/feeds

A refresh reapplies to the findings you already have, so a CVE added to KEV overnight is marked on your fleet in the morning without re-scanning anything. How the feeds are used.

U4 · Operating

Running it day to day

  • Network access. Outbound TCP 22 to your Linux targets, and HTTPS to the data sources: OSV.dev, CISA and FIRST, plus NVD if you enable it. For air-gapped networks, point the feed URLs at an internal mirror, for example https://mirror.example.com/.
  • API tokens. Scripts authenticate with tokens created under Settings, sent as Authorization: Bearer. A token is shown once, stored only as a hash, and stops working as soon as you revoke it.
  • Locked out. Reset the password from the host with docker exec -it --user 1000:1000 cvedeck cvedeck-admin reset-password. It also signs out every session.
  • Reverse proxies. Scans run within the request and respond only when the whole batch finishes, so raise your proxy's read timeout to several minutes. Pass the original Host header through. For a proxy in another container, set FORWARDED_ALLOW_IPS to its address or CVEDECK_COOKIE_SECURE=true.
  • SSH keys. For repeat fleet scans, mount a dedicated key read-only and point CVEDECK_DEFAULT_SSH_KEY_PATH and CVEDECK_DEFAULT_SSH_USER at it. The key file is re-read on every scan, so rotating it needs no restart.
  • Rebuilt hosts. A host you have reinstalled, or whose keys you regenerated, presents a new SSH key and is refused. Clear the old pin with Forget host key on the machine's page, and the next scan pins whatever key it presents. Nothing else ever replaces a pin. Set CVEDECK_SSH_HOST_KEY_POLICY=strict to refuse unpinned hosts outright, instead of the default tofu.
  • Pinned keys, in one place. Settings lists every key CveDeck has pinned, with its type and fingerprint, so you can audit the fleet without opening each host. A host reached on more than one port is pinned per port, and the machine's page shows each one.
  • Scan history. Each host keeps its last 50 scan runs, with what each one found new and cleared. CVEDECK_SCAN_HISTORY_LIMIT changes how many; a host's latest successful run is always kept.
  • Backups. With SQLite, everything lives in one file, cvedeck.db, in the data directory. Stop the container and copy it. PostgreSQL is supported through CVEDECK_DB_URL.
  • Upgrades. Pull the new image and recreate the container. The schema migrates forward automatically on start. Back up first, and check /api/health, which reports the running version. Upgrading from 0.6.0 or earlier prints a setup code on the first start, and your fleet and findings are untouched. Re-scan your hosts after upgrading from 0.7.2 or earlier: what a finding's own release fixes is worked out at scan time. Coming from 0.7.3 or earlier, each host's first scan pins the key it presents and is its history baseline, so it reports nothing as new or resolved. The migrations from 0.8.6 and 0.8.9 run on start like the rest.
  • An old demo database keeps its old gaps. Demo mode seeds only an empty database, so a demo you first ran before 0.8.9 has no fictional intel cache and goes on showing a dash where the exploitation counts should be. Start it from an empty data directory, or just run it without a mounted one.

The full deployment guide, including every setting, is in the repository.