v0.3.0
Release theme: Vault production posture. Closes the largest
"Known limitations" entry in single-host-prod.md — Vault now
runs with a real file storage backend instead of -dev mode,
and an automated init+unseal layer keeps the operator UX identical
to v0.2.x (edit .env.prod, make prod-up, done). State persists
across container restarts, so the bootstrap-host-installed CA
pubkey on a target host stays valid through a Vault container
restart — the exact bug class that motivated this work.
Added
-
Production-mode Vault with auto-init + auto-unseal (PR #54).
The prod overlay swapsvault server -dev(in-memory, fixed
root token) forvault server -config=/vault/config/vault.hcl
with file storage at/vault/fileon the
wg_manager_vault_datanamed volume.docker/vault/vault.hcl— file storage, HTTP listener on
0.0.0.0:8200, ui on,disable_mlock = truefor portability.
No listener TLS — Vault is the PKI source for every other
cert in the stack, so the listener can't depend on a Vault-
minted cert at boot. A dedicated listener-TLS cycle is the
honest next step.scripts/vault_init_unseal.sh— idempotent state machine
that runs fromprod_bootstrap_substrate.shBEFORE the engine
bootstraps. Probes/v1/sys/init+/v1/sys/seal-status. On
uninit: callsvault operator initvia the HTTP API with 5
shares / 3 threshold, captures the JSON output to
${VAULT_INIT_FILE}(default/app/vault-init.json) at mode
0600 owned by UID 1001 (the wg-manager runtime UID), then
unseals. On sealed: reads the same file and unseals. On
unsealed: no-op.docker/entrypoint-wg-manager.sh— tiny shim baked into
the wg-manager image. Readsvault-init.jsonwhen present +
non-empty, exports theroot_tokenfield asVAULT_TOKEN
beforeexec-ing the CMD. First-boot tolerant (empty file
→ no-op).docker-compose.prod.ymlvault service dropsVAULT_DEV_*VAULT_TOKENenv (blanked to""so Compose's environment-
map merge doesn't ride the dev compose's values through).
entrypoint: ["vault"]bypasses the official image's
docker-entrypoint.shwhich would otherwise APPEND
-dev-listen-addressand race vault.hcl's listener.
Healthcheck override accepts 501 (uninit) / 503 (sealed) as
healthy via?standbyok=true&sealedcode=204&uninitcode=204
so bootstrap-substrate'sdepends_on: { vault: service_healthy }trips on the listener being up
rather than the substrate being bootstrapped.
- api / worker / bootstrap-app lose their
VAULT_TOKEN
env — the entrypoint shim sources it fromvault-init.json
instead. All four containers bind-mountvault-init.json
(substrate writable, the rest read-only). .env.prod.exampledropsVAULT_ROOT_TOKEN(auto-
generated now), adds optionalVAULT_KEY_SHARES/
VAULT_KEY_THRESHOLD(defaults 5 / 3), documents the backup
story (back upvault-init.jsonalongside.env.prod).single-host-prod.mdKnown Limitations table flips the
"Vault in dev mode" row to "Vault unseal keys live on disk
invault-init.json" — the honest residual gap. Real
cloud-KMS auto-unseal (transit / awskms / gcpckms) is the
next step but out of scope without cloud creds. The "Where
state lives" table grows the three-file backup story
(.env.prod+vault-init.json+tls/).- End-to-end verified on rv.vpn: clean
make prod-upfrom
zero state brings all 7 services healthy. Smoke on
/healthz,/readyz,/tenants(default-tenant
subnet_poolreflects the operator'sDEFAULT_SUBNET),
/crypto/status(Vault Transit backend) all 200. Critical
restart test:docker restart wg_manager_vault→ sealed →
re-runbootstrap-substrate→ auto-unseals from existing
vault-init.json→ all engine bootstraps log "already
present" → Phase 1 complete. Final status:
storage_type: file, initialized: true, sealed: false.
Five bugs surfaced + fixed during verification (the full diary
is in PR #54's body): vault image'sdocker-entrypoint.sh
mangling theservercommand, missingurllib.errorimport,
unquoted heredoc backtick command-substitution,
vault-init.jsonUID mismatch from the Makefiletouch, and a
duplicate stale wait loop inprod_bootstrap_substrate.sh.Tests: 47 new cases across three new files plus extensions
to four existing files. Backend pytest 1027/1027 in local
mode (was 989 on v0.2.x's merge). -
docs/deploy/single-host-prod.mdgained an "Onboarding a
target host (SSH CA install)" section (PR #53). Recipe for
runningwg-manager bootstrap-hostfrom inside the prod stack
viadocker compose run --rm, mounting~/.sshread-only so
the operator's pre-existing key reaches the container without
exposing the rest of$HOME. The full design + cert profiles
still live indocs/operator-guide.md§3; this is a
deployment-side recipe rather than a duplicate. Closes the gap
a real operator hit on rv.vpn: aftermake prod-upreturned
and they curl'd the API successfully, the next step ("how do I
get wg-manager onto my first hub box?") wasn't covered in the
deploy doc.
Published images
ghcr.io/jfudally/wg_manager:v0.3.0— API + workerghcr.io/jfudally/wg_manager-web:v0.3.0— dashboard
Pull either with a granularity-appropriate tag (vX.Y.Z, vX.Y, vX, or latest).
Supply-chain attestation
Both images are signed via cosign keyless OIDC and carry an
in-toto CycloneDX SBOM attestation. Verify with:
cosign verify \
--certificate-identity-regexp \
'https://github.com/jfudally/wg_manager/.github/workflows/release.yml@.*' \
--certificate-oidc-issuer \
'https://token.actions.githubusercontent.com' \
ghcr.io/jfudally/wg_manager:v0.3.0The SBOMs are also attached as release assets:
sbom-api.cdx.json— CycloneDX SBOM of the API + worker image's Python depssbom-web.cdx.json— CycloneDX SBOM of the dashboard image's Node deps