Skip to content

kaveo · Get started

Quick start

Bring up the five-container kaveo stack with Docker Compose, sign in as the bootstrap owner, load the demo dataset and run your first scan.

On this page

This guide takes you from a kaveo distribution to a scan full of findings. You set a few values in .env, start the Docker Compose stack, sign in as the owner and load a synthetic demo account. You do not need an AWS account for any of it.

Requirements#

  • Docker with the Compose plugin (docker compose)
  • openssl, to generate secrets
  • Ports 80 and 443 free on the host. Caddy publishes both. To change the host side, set KAVEO_HTTP_PORT and KAVEO_HTTPS_PORT in .env.
  • make is optional. It works only with the source tree, and the note in step 2 explains how to use it.

Run every command from the root of the distribution, where .env.example lives.

1. Configure the environment#

Copy the example environment file:

bash
cp .env.example .env

Generate three random values. Run this once per secret:

bash
openssl rand -hex 24

Set these values in .env, and use your own address for the owner account. KAVEO_SUPERADMIN_EMAILS is not in the example file, so add that line yourself.

text
POSTGRES_PASSWORD=<generated value 1>
POSTGRES_APP_PASSWORD=<generated value 2>
KAVEO_BOOTSTRAP_PASSWORD=<generated value 3>
KAVEO_BOOTSTRAP_EMAIL=you@example.com
KAVEO_SUPERADMIN_EMAILS=you@example.com
KAVEO_SESSION_COOKIE_SECURE=false
VariableWhat it sets
POSTGRES_PASSWORDPassword of the kaveo Postgres superuser. Only migrations and backups connect with it.
POSTGRES_APP_PASSWORDPassword of the non-superuser application role. The api and worker connect as this non-superuser role, so Postgres row-level security applies to them.
KAVEO_BOOTSTRAP_PASSWORDPassword of the owner account kaveo creates on first boot
KAVEO_BOOTSTRAP_EMAILEmail of that owner account. .env.example ships a placeholder, so replace it.
KAVEO_SUPERADMIN_EMAILSComma-separated emails that kaveo marks as platform superadmins every time the api starts. Step 4 needs this to open the demo workspace.
KAVEO_SESSION_COOKIE_SECUREWhether the session cookie is marked Secure. Compose always passes this variable to the api and worker, and they accept only true or false. They do not start while it is empty, which is how .env.example ships it. false suits this plain-HTTP local run.

Compose will not start until all three passwords are set. Each one uses the ${VAR:?} form, which has no fallback value.

Set them before the first start. Postgres applies the two database passwords only when it initializes an empty data volume. kaveo creates the owner only while the database has no users. If you change these values in .env later, the existing passwords stay the same.

2. Start the stack#

bash
docker compose --env-file .env -f infra/docker-compose.yml up -d --build

The first run builds the api, worker and web images. Five containers start:

ServiceRole
caddyIngress. Sends /v1/* and /mcp to the api and everything else to the console.
webThe Next.js console
apiThe FastAPI service
workerBuilt from the same Dockerfile as the api. It runs scans and other background jobs.
postgresPostgres 16, the system of record

When the api boots, it applies any pending migrations while it holds a Postgres advisory lock, so two migration runs never overlap. Compose sets KAVEO_BACKUP_BEFORE_MIGRATE=true, so whenever migrations are pending, the api takes a pg_dump snapshot first. Next it creates the owner account if the database has no users. The worker starts only after the api reports healthy.

Check the services, and wait until api shows as healthy:

bash
docker compose --env-file .env -f infra/docker-compose.yml ps

From the air-gapped bundle#

Check the tarball, unpack it, check every file inside it, then load the images:

bash
shasum -a 256 -c kaveo-airgap-<version>.tar.gz.sha256
mkdir kaveo-airgap && tar -xzf kaveo-airgap-<version>.tar.gz -C kaveo-airgap
cd kaveo-airgap
shasum -a 256 -c SHA256SUMS
docker load -i images.tar

A checksum mismatch means the bundle changed in transit. Stop and ask for a new copy. If everything matches, set up .env as in step 1 and start the stack without building:

bash
docker compose --env-file .env -f infra/docker-compose.yml up -d

3. Sign in#

Open http://localhost. If you changed KAVEO_HTTP_PORT, add that port. Sign in with the KAVEO_BOOTSTRAP_EMAIL and KAVEO_BOOTSTRAP_PASSWORD from your .env.

The owner has the admin role. Because you also listed its address in KAVEO_SUPERADMIN_EMAILS, the sidebar shows Platform (all orgs) under Workspace. There is no data yet. The next step loads some.

4. Load demo data#

bash
make seed

This loads the demo from the api image. It creates a separate workspace named Kaveo Demo Org (slug kaveo-demo) and adds one deliberately insecure synthetic AWS account to it, synthetic-vulnerable-account. It also adds a completed scan with its resources, evidence rows, graph edges and the findings every detector produces over them. If you run it again, it replaces the demo scan with a fresh copy.

To open it:

  1. In the sidebar, open Platform (all orgs).
  2. On the Kaveo Demo Org row, choose Act in this org. The console switches to that workspace and opens the dashboard.
  3. In the top bar, select synthetic-vulnerable-account if it is not already selected. The seeded scan is already there.

You can also press Run scan. No real role exists for the seeded account. With the default KAVEO_SCAN_MODE=auto and no AWS base credentials, the worker falls back to the same synthetic fixture and labels the scan synthetic in its coverage. If kaveo does have AWS credentials, from .env or an instance role, the role assumption fails, and so does the scan.

5. Explore the console#

  • Dashboard. The overview of the selected scan: posture figures, the fix to make first, attack-surface and compliance summaries, and the severity trend.
  • Risk Report. The report view. In the default real_risk mode it collapses hygiene findings into a count, unless the resource is internet-exposed or sits on a toxic path. The API and the compliance report still return every finding.
  • Compliance. Shows control results across 10 frameworks. You can export them as CSV, JSON, an evidence worksheet or PDF.
  • Attack Paths. Shows the paths an attacker could take from the internet to a high-value resource, traced over the IAM and reachability graph, and the choke points that cut the most of them.
  • Remediations. Holds the approval queue for proposed fixes. Only admins can approve or reject. An approved fix runs as an offline dry run unless you set up the AWS executor. See Remediation.

Open any finding to see the evidence behind it.

6. Useful commands#

Each command below starts with docker compose --env-file .env -f infra/docker-compose.yml.

SubcommandMake targetWhat it does
up -dmake upStart the stack, detached
downmake downStop and remove the containers. Volumes stay.
logs -fmake logsTail logs from every service
psmake psShow each service and its health
run --rm api with the migration commandmake migrateApply pending migrations with the advisory-locked runner
run --rm api with the seed commandmake seedLoad or refresh the synthetic demo
down -vmake cleanStop the stack and delete every named volume

To take a backup, run the backup command from the api image using the migration connection (MIGRATE_DATABASE_URL), as documented in the release README and AIRGAP.md.

Row-level security scopes the app role to one workspace at a time, and pg_dump needs a role that can read every row. The make backup target connects as the app role, so use the command above instead. It writes a timestamped archive to the kaveo_backups volume.

The stock Caddyfile doesn't route /health. To check it from inside the Compose network, run:

bash
docker compose --env-file .env -f infra/docker-compose.yml exec api \
  python -c "import urllib.request; print(urllib.request.urlopen('http://localhost:8000/health').read().decode())"

The response includes status, version, ai_provider, database, and the number of detectors and collectors that loaded. The Compose healthcheck probes the same endpoint. It returns 200 even when the database is unreachable, so if something looks wrong, check that database reads up.

Next steps#