Secrets
--env puts config in the app spec — which is stored in cleartext in the
app database and rides into every admin backup. That's fine for a log level;
it's wrong for a database password. Secret bundles fix that: sensitive values
live in a dedicated store, encrypted at rest, and reach the guest as
environment variables without the value ever appearing in the app spec, the
API, backups, or logs.
A secret is a bundle: a named set of key → value pairs (like a Kubernetes
Secret). An app injects a bundle with envFrom — every key becomes an
environment variable.
Enable it
Secrets are opt-in: the daemon needs a master key that encrypts the store. Point it at a key file (generated on first use) or supply the key in the environment:
crucible daemon … --secrets-key-file /var/lib/crucible/secrets.key
# or, key from a KMS / systemd credential (takes precedence over the file):
CRUCIBLE_SECRETS_KEY="$(cat key.b64)" crucible daemon …With no key configured, secrets are disabled and the /secrets routes answer
501 — there is no silent plaintext fallback. Managing secrets is gated by the
default-deny secret scoped-token op.
⚠️ Back up the master key. Losing it means losing every secret — the store is useless without it. The key is deliberately excluded from
admin backup(so the backup alone is inert), so keep it somewhere safe and separate.
Create a bundle
The most common path is a .env file — the whole file becomes one bundle:
crucible secret set web-env --from-env-file .envOr set one key at a time (the value comes from stdin or --from-file, never
the command line):
printf '%s' "$DB_PASSWORD" | crucible secret set web-env DATABASE_URL
crucible secret set web-env API_KEY --from-file api-key.txtInspect without ever seeing values:
crucible secret ls # bundle names
crucible secret ls web-env # that bundle's KEY names (not values)
crucible secret rm web-envInject into an app (envFrom)
# inject every key of one or more bundles as env vars
crucible app create web --image … --secrets web-env
# one-shot: import a .env into a bundle named <app>-env AND inject it
crucible app create web --image … --secrets-from .envThe app spec stores only the bundle name in secret_env_from — so app get
and backups carry no secret material. At boot the daemon decrypts each bundle and
merges its keys into the environment, with precedence image ENV → secret
bundles → --env (last wins), so a plaintext --env can still override.
A bundle referenced by an app must exist (create/update fails fast otherwise); a bundle deleted out from under a running app is skipped on the next boot with a warning.
What's protected — and what isn't
| Surface | Secret value exposed? |
|---|---|
the app spec / app get |
no — only the bundle name |
admin backup |
no — the store rides it as ciphertext; the key is not in the backup |
the /secrets API and crucible secret ls |
no — names and key names only |
| daemon logs | no — the resolved environment is never logged |
| the on-disk secret store | no — AES-256-GCM sealed |
| the running guest's memory (and its snapshot) | yes — see below |
The honest limit: snapshot residency
Once injected, a secret is in the guest process's environment — in guest RAM.
Because crucible snapshots guest RAM to disk for sleep / wake / fork, that
plaintext is written to the snapshot's memory file under --work-base. So a
running or slept app's secrets are guest-RAM-grade: recoverable by host-root
or by stealing an unencrypted disk — the same posture as any VM platform (a
snapshot is just persisted RAM).
What this release changes is the at-rest config exposure, which was far worse: the store, backups, and API no longer leak. For the runtime residual:
- Put
--work-baseon an encrypted filesystem (LUKS/dm-crypt) so disk theft doesn't yield snapshot memory. - Encrypting the snapshot memory file itself is planned (a later release), which closes disk-at-rest for snapshots too.
Rotation
A running or slept instance holds the old value in its RAM. After changing a
secret, redeploy the app (app update) to re-inject the new value.
Non-sensitive config
Keep using --env for values that aren't sensitive (a port, a log level) — it's
simpler, and plaintext-in-the-spec is fine for those. Move only the secrets to a
bundle.