Vaultwarden on Kubernetes without Docker Compose, with Kapitan
Vaultwarden, a password server for the Bitwarden clients, with its vault in SQLite on a claim, rendered by the kadet-hub kubernetes generator 1.14.0 from the Docker Compose example in the README of Vaultwarden 1.37.5.
- Latest version:
1.0.1, published - Source: Docker Compose file
dani-garcia/vaultwarden@6ed6921cc2b0e03b4d74189574d2a5e71c512e97(README.md),sha256:5c7e29e287f600fe1979d196d53f94cd8f87f9c0de1acf6481fde510fbfdd4fc - Proof level:
deploys, deployed on kind and smoke tested by the kadet-hub/recipes CI renderspassed in the release run of with check 0.18.0, kind v0.33.0- Output capabilities: none
- Inventory backends:
omegaconf - Generators: kubernetes 1.14.0
sha256:787129e1eaed6b9bc868fe4c4aec58d40de6a89fc3cc08424a4cb5822b855080 - Image security: grade F 358 findings, one per advisory and package, in the 1 image of version 1.0.1: CRITICAL 4, HIGH 76, MEDIUM 132, LOW 146, UNKNOWN 0. Scanned 2026-11-07 11:47 UTC. Full report
The grade rates the upstream images the recipe pins, not the recipe:docker.io/vaultwarden/server.
What it deploys
recipes.vaultwarden(init.yml)recipes.vaultwarden.vaultwarden(vaultwarden.yml):vaultwarden
Required parameters
applications.vaultwarden.domain is the URL clients reach Vaultwarden at,
with its scheme, such as https://vault.example.com. Vaultwarden builds
the links in its emails and the addresses it gives clients from it.
applications.vaultwarden.admin_token is the token of the admin page
/admin, set as a ref with the backend of your choice. Vaultwarden
recommends an Argon2id PHC string over the token itself.
docker run --rm -it docker.io/vaultwarden/server:1.37.5 /vaultwarden hash
asks for the token twice and prints the string as ADMIN_TOKEN='<string>'.
Without either parameter the compile stops with Interpolation key and
its name.
Include
A target includes the recipe and its options as classes and can override anything the recipe sets:
classes:
- recipes.vaultwarden
parameters:
namespace: vaultwarden
kapitan:
vars:
target: vaultwarden
applications:
vaultwarden:
domain: https://vault.example.com
admin_token: <ref>
Fetching it
The quickstart goes through these steps on a kind cluster with the uptime-kuma recipe, from installing Kapitan to the running app.
A separate fetch inventory pins the recipe by version and digest. This is fetch/targets/recipes.yml for version 1.0.1:
parameters:
kapitan:
dependencies:
- type: oci
source: ghcr.io/kadet-hub/recipes/vaultwarden:1.0.1@sha256:55a988881f27e76af4607ed4aa08c5057f141c4b10114e9c1ec2dd6aa771586a
output_path: inventory/classes/recipes/vaultwarden
The first command fetches the recipe into inventory/classes/recipes/vaultwarden, the second the generators and compiles:
kapitan compile --inventory-path fetch --fetch --output-path .
kapitan compile --fetch
The first command exits with status 1 because the fetch target compiles nothing. Kapitan's --fetch checks neither the digest nor the attestation. This verifies the attestation of the recipe:
gh attestation verify \
oci://ghcr.io/kadet-hub/recipes/vaultwarden@sha256:55a988881f27e76af4607ed4aa08c5057f141c4b10114e9c1ec2dd6aa771586a \
--owner kadet-hub \
--cert-identity https://github.com/kadet-hub/registry/.github/workflows/release.yml@refs/heads/main
The attested index must also list each digest under its recipe with the pinned version, and not as yanked. Verifying by hand shows how to read it.
Settings
| Setting | Inventory |
|---|---|
| Vaultwarden version | components.vaultwarden.image, tag and digest |
| signups, off | components.vaultwarden.env.SIGNUPS_ALLOWED |
other settings of .env.template, such as SMTP_HOST |
components.vaultwarden.env.<variable> |
| exposure | components.vaultwarden.service.type, or a route or Ingress of the target to the Service vaultwarden on port 8000 |
size of the claim vw-data, 1Gi |
components.vaultwarden.volume_claims.vw-data.spec.resources.requests.storage |
| resources | components.vaultwarden.resources |
Vaultwarden keeps its state in /data on the claim: the SQLite database
db.sqlite3, the attachments and Sends, the key that signs its sessions
and config.json. Settings saved on the admin page go to config.json and
take precedence over the environment, so a setting changed there no longer
follows the inventory. SQLite allows one replica.
Signups are off (DEC-55 in the spec). The admin page invites users, and an invited user creates the account in the web vault with the invited address. Without SMTP settings Vaultwarden sends no invitation mail, and nobody but the admin knows of the invitation.
The Service vaultwarden serves plain HTTP on port 8000, the port the
compose example publishes, with the web vault, the API and the WebSocket
notifications. The web vault works only over HTTPS, so expose it through a
route or Ingress that terminates TLS at the address of domain. A path in
domain moves every route below it, the health check’s /alive too.
Mapping
The service vaultwarden becomes the component vaultwarden. Its name
stays, so no reference is rewritten.
| Compose | Recipe |
|---|---|
service vaultwarden |
StatefulSet vaultwarden, Service vaultwarden, Secret vaultwarden with ADMIN_TOKEN |
vaultwarden/server:latest |
docker.io/vaultwarden/server:1.37.5@sha256:4fe17f415f90087377d37c755e0ea3cf189dcc6794e49cdd2a9adbb4937d9e1f, which latest was at migration |
port 127.0.0.1:8000:80 |
Service port http on 8000 to the container’s port 80, ClusterIP |
DOMAIN, https://vw.domain.tld |
env from applications.vaultwarden.domain |
bind mount ./vw-data/ at /data/ |
claim template vw-data, ReadWriteOnce, 1Gi |
the image’s HEALTHCHECK, /healthcheck.sh |
readiness probe every 60 seconds with a timeout of 10 seconds and 3 failures |
restart: unless-stopped, container_name |
not rendered |
| not in the example | ADMIN_TOKEN from applications.vaultwarden.admin_token (DEC-56), SIGNUPS_ALLOWED: "false" (DEC-55) |
The example keeps the vault in SQLite, Vaultwarden’s default, and so does the recipe (DEC-57).
Proof
check vaultwarden --level deploys with tests/target.yml, on kind for
each Kubernetes minor CI tests, with check 0.14.0. A compose file has no
Kubernetes objects to compare with, so the recipe is proven by deploying it
(RCP-38). tests/target.yml sets the compose example’s domain and the
Argon2id PHC string of a test token.
tests/smoke starts a pod of Vaultwarden’s image, which carries curl.
From there, through the Service vaultwarden on port 8000, it
- gets
/alive - requires that
/api/versionnames the image tag the StatefulSet runs, and that/api/confignames the domain - gets 401 from
/adminwith a wrong token, logs in with the test token, lists the users through/admin/usersand readssignups_allowed: falsefrom/admin/diagnostics/config - invites
smoke@example.comthrough/admin/invite, deletes the podvaultwarden-0, and finds the user in/admin/usersonce the new pod is ready, which needs the database on the claim.
Not tested: the web vault in a browser, a client’s login, mail, and a route that terminates TLS.
Versions
1.0.1, :sha256:55a988881f27e76af4607ed4aa08c5057f141c4b10114e9c1ec2dd6aa771586a1.0.0, :sha256:1d0e702c639b83ebfd6b59e9aba690cbe80df00a3a3134806fb77365299a6cb6
The recipe's files are in kadet-hub/recipes. All recipes