Start here
Secrets with tvault
Inject only the credentials each process needs, without copying them into the repository, temporary files, or logs.
Reviewed 2026-07-24
Chalupa separates configuration from secrets. chalupa.yml, .env.example,
and the runbooks describe names and contracts; tvault stores encrypted
values. A process receives a secret only while running the command that needs
it.
The least-privilege examples on this page require TinyVault v0.19.0 or
newer. Verify the installed binary with tvault --version before using
--only, --prefix, or identity-backed decryption.
--only and --prefix select and decrypt only matching values across direct,
inherited-group, recipient-identity, and local agent reads. An explicit
tvault://... template reference resolves only the referenced key. Selectors
narrow bulk injection; they do not make unrelated values available to the
child process.
Secret map
| Key | Consumer | Scope |
|---|---|---|
DIGITALOCEAN_API_TOKEN |
Local Pulumi tasks | Operator-requested provisioning and destruction. It never reaches the droplet or control plane. |
CHALUPA_INGEST_KEY |
Agent, task report, and Chalupa |
Signs heartbeats, events, and suite runs. |
DATABASE_URL |
Chalupa | Pooled Neon/Postgres connection. |
MIGRATIONS_DATABASE_URL |
Release migration task | Direct, non-pooled Neon/Postgres connection. It is not a web-runtime credential. |
CLOUD_ADMIN_TOKEN |
Chalupa | Creates the private administrator session. |
CRON_SECRET |
Scheduler and Chalupa | Authorizes reconciliation and retention jobs. |
DIGITALOCEAN_READ_TOKEN |
Cost reconciliation | Read-only inventory access. It cannot create or destroy resources. |
RESEND_SEND_API_KEY |
Trusted outbound mail sender | Domain-scoped sending only; it is not a web-runtime requirement. |
RESEND_RECEIVE_API_KEY |
Signed inbound mail webhook | Full Resend access required by forwarding; Production only. |
RESEND_WEBHOOK_SECRET |
Signed inbound mail webhook | Verifies the exact raw provider request for one endpoint. |
RESEND_FORWARD_TO |
Signed inbound mail webhook | Private forwarding destination; never product copy or a client value. |
The current release uses one fleet-wide ingestion key. Treat it as a shared credential: rotating it requires updating the control plane and recreating or reconfiguring every agent. Root compromise of one enrolled droplet therefore grants write authority across the fleet, including forged deployment identities. This boundary is suitable only for a single-operator private beta with trusted workloads. A multi-operator or untrusted-workload service must first use server-bound deployment or environment key IDs with rotation and revocation.
Diagnose without reading values
Secret names are metadata. Verify availability without unlocking or printing values:
tvault doctor --project chalupa --json
tvault list --project chalupa --json --names-only
tvault status --json also reports whether the vault and local agent are
available. None of these commands returns secret values.
Local operator interface
Starting the local CLI or operator UI, running doctor, or
building an inspect view does not read tvault or contact a provider. The
interface resolves an existing Taskfile credential profile only after the
operator selects an action that needs it. It does not cache config history,
store secret values, or create another credential store.
Inspection is redacted but can still reveal paths, service names, topology, ports, images, and other private metadata. Keep its output and local Pulumi state on the trusted operator workstation.
Create generated credentials
Send random credentials directly to tvault over stdin. Redirecting the command result keeps operational output separate from secret-management output:
openssl rand -hex 32 |
tvault set --project chalupa CLOUD_ADMIN_TOKEN --stdin >/dev/null
openssl rand -hex 32 |
tvault set --project chalupa CHALUPA_INGEST_KEY --stdin >/dev/null
openssl rand -hex 32 |
tvault set --project chalupa CRON_SECRET --stdin >/dev/null
DATABASE_URL, MIGRATIONS_DATABASE_URL, and
DIGITALOCEAN_READ_TOKEN come from their providers. Send them through stdin
or import them from a controlled file. Do not place them in command arguments
when your shell records history.
Resend keys and the webhook signing secret also come from the provider. Create separate sending and receiving keys, then pipe each one-time value directly into tvault. Treat the private forwarding address as secret configuration even though it is not an authentication credential.
Run with least privilege
Repository tasks already pass through fixed profiles in
src/cli/with-tvault.ts:
| Workflow | Profile boundary |
|---|---|
task data-up, Pulumi phase of task up, task data-nuke |
Provisioning token only. |
Agent bootstrap phase of task up, task agent-bootstrap |
Ingest key only; delivered through SSH stdin after Pulumi completes. |
Provider phase of task down |
Provisioning token only; writes a private, short-lived receipt after successful destruction. |
Sunk-report phase of task down |
Ingest key only; reads the receipt after the provider subprocess has exited. |
task report |
Ingest key only. |
task cloud:db:migrate |
Direct migration database URL only. |
task cloud:staging-check |
The five control-plane runtime credentials, never the provisioning token. |
Do not invoke the internal underscore-prefixed lifecycle tasks directly; the public tasks establish the credential boundary.
The wrapper builds each child environment from a fixed operational allowlist
instead of copying the parent shell. It retains executable discovery,
user-scoped Bun/Pulumi/tvault state, temporary directories, locale and
terminal behavior, explicit proxy/CA settings, the empty local Pulumi
passphrase and Taskfile inputs. The local SSH agent socket is admitted only
for the ingest-only bootstrap or the exact provider-bound task _down
command. The latter passes it to credential-free log and Monitor flush
subprocesses; every SSH call retains ForwardAgent=no, so the socket is never
forwarded to the droplet. The allowlist also retains the public control-plane
mode, origin, and
reviewed storage-rate setting when those commands need them. Resolved profile
credentials are then added explicitly.
The empty Pulumi passphrase is a required local-backend convention in this
repository; it does not add meaningful entropy. Local filesystem permissions
and workstation access are therefore the state confidentiality boundary.
Post-provision SSH delivery removes CHALUPA_INGEST_KEY from DigitalOcean
user data and Pulumi state, but the state can still contain generated Compose
configuration and must remain private.
This boundary applies to newly generated user data. It does not erase old checkpoint backups or user data already delivered to an existing droplet. Before declaring an older environment remediated, replace its disposable compute through the reviewed lifecycle, rotate the fleet key using the documented whole-fleet sequence, and retain or remove historical state only under the operator's normal backup policy. Never inspect a checkpoint by printing its secret values.
The bootstrap stores the key as /etc/chalupa/ingest.key through a
same-directory temporary file and atomic rename. Without private log
collection it is owned by root with mode 0600. A logs-enabled deployment
uses owner/group root:chalupa-logs, mode 0640, and a 0750
/etc/chalupa directory so only root and the isolated collector can traverse
and read the required files. The value travels on stdin, never in the SSH
command or routine output. Initial host-key acceptance uses the same OpenSSH
accept-new trust-on-first-use boundary as the tunnel; it is not independent
host identity verification. Subsequent host-key changes fail closed. To
verify the first connection, pin the droplet's reported host key fingerprint
first — see Verified first-connection
identity.
Unrelated cloud credentials, application secrets, NODE_OPTIONS, dynamic
loader hooks, and arbitrary parent variables are not forwarded. If a trusted
tool needs another non-secret operational variable, review and add its exact
name to CHILD_ENVIRONMENT_ALLOWLIST; do not restore broad pass-through.
Supported TVAULT_* unlock, identity, agent, and vault-location variables are
visible only to the short-lived tvault credential reader. They are never
forwarded to Pulumi, Task, Bun application commands, or the control plane.
Provisioning handles application-level Compose interpolation separately. The
wrapper scans only selected service values and their declarative command/env
overrides, captures only the referenced host variables, and passes a bounded,
canonical snapshot under one Chalupa-owned runtime name. Pulumi decodes that
snapshot explicitly; the original variable names do not become subprocess
environment capabilities. CONFIG, CHALUPA_*, PULUMI_*, TVAULT_*, and
known DigitalOcean credentials are forbidden interpolation names. Every
non-empty application value taken from the operator shell is redacted by
output path in the preview, including short values under generic names.
Resolved tunnel ports remain visible structural output, and volume mappings
cannot use interpolation. Literal values under common secret-bearing keys and
URL userinfo are also redacted, but the preview is not a general DLP scanner.
Do not place literal secrets under unusual field names in Compose.
For backward compatibility, only the provisioning profile can read
DIGITALOCEAN_API_TOKEN from ~/.config/secrets/env after tvault is missing
or cannot return that key. The parser treats the file as data and never
evaluates shell syntax. Database, migration, admin, ingest, cron, and read-only
provider credentials never use this fallback. Move the provisioning key into
tvault and treat the file as a temporary migration path.
Prefer tvault run --only to exporting an entire project. Values exist only in
the subprocess environment:
tvault run \
--project chalupa \
--only DATABASE_URL,CLOUD_ADMIN_TOKEN,CHALUPA_INGEST_KEY,CRON_SECRET,DIGITALOCEAN_READ_TOKEN \
--strict \
-- bun run dev
Run that command from cloud/. For a local tour without a database, set
CHALUPA_DATA_MODE=demo and inject only CLOUD_ADMIN_TOKEN.
Do not use eval $(tvault env) as the normal workflow. It expands both the
lifetime and scope of credentials in your shell. Do not persist tvault env
output in .env.local.
tvault:// templates
When a command needs to rename a key, a committed template may contain a reference, never the value:
DIGITALOCEAN_TOKEN=${tvault://chalupa/DIGITALOCEAN_API_TOKEN}
tvault run --env-file <template> -- <command> resolves the reference at
runtime. Its dotenv parser does not evaluate shell substitutions or commands.
Deliver secrets to the platform
A deployment receives the five runtime keys through the platform's encrypted
secret store. tvault can remain the local source of truth, but the platform
keeps its own encrypted runtime copies. The local release task reads
MIGRATIONS_DATABASE_URL from tvault. The serialized GitHub production release
workflow may instead read the same direct URL from an encrypted Actions secret.
On GitHub plans that support private-repository environment secrets, place it
in the protected Production environment; otherwise use a repository Actions
secret and keep the workflow push-only on main. Neither option puts it in the
Vercel web runtime. Vercel's Git Integration performs the build and deployment,
so the workflow needs no Vercel CI token or project identifiers. Configure
Vercel to require the GitHub Actions checks named
Production verification and Production migration gate before assigning
Production aliases. Use a stdin-based workflow and confirm key names, not
values.
After configuring staging:
- confirm that all five control-plane runtime credentials are present;
- use
task cloud:db:migratefrom a trusted local release environment, or approve the serialized GitHub production release workflow, before accepting traffic; - check readiness without returning configuration details;
- sign and submit one synthetic ingestion request;
- record who performed the synchronization and when.
Never reuse DIGITALOCEAN_API_TOKEN as the control plane's read-only token.
The control plane must receive the narrowest provider scope available.
Rotation and revocation
Rotate one credential at a time and keep a recovery path:
- create the new version in tvault;
- update the remote consumer;
- verify readiness and the affected workflow;
- restart or replace agents when the HMAC key changes;
- revoke the previous value at the provider;
- inspect metadata with
tvault history --project chalupa <KEY>.
Rotating CLOUD_ADMIN_TOKEN invalidates existing sessions. Rotating
CRON_SECRET requires updating the scheduler. Rotating the read-only token
must not affect the local provisioning token.
Rules for agents
- Use
tvault list --names-onlyfor discovery; do not loop overget. - Inject values into a trusted subprocess with an explicit allowlist.
- Treat proxy URLs as sensitive when they contain credentials, even though proxy variables are operationally allowlisted.
- Never surface stdout from a command that might echo its environment.
- Do not write secrets to
/tmp, artifacts, screenshots, or messages. - Do not unlock and relock the vault around every operation.
- Stop when creating, rotating, or synchronizing a value needs new authority.