TILLSECRETS · SEALED

A secret nobody reads again

Beta

Most secrets never need to be read by a person. Your app needs the database password; you don't. Seal it and nobody can reveal it, owners included, while everything that uses it keeps working. If someone ever does need to see it, two owners have to agree.

01Scope

What a seal changes

PathOnce sealed
Reveal (console, tilldev secrets get, API)Refused for everyone. A one-time reveal takes two owners.
tilldev secrets exec, tilldev mcp run, the local bridgeRefused, with the reason sealed. They would put the value on a machine.
The broker: grants, presigned calls, catalog actions, SSH signingWork as before. The value is used inside TillDev and never returned.
Pull with a service tokenOnly a token pinned to its environment gets it, as for a restricted secret. Others see it withheld.
Sync targetsKeep syncing.
CIProtected branches only, and only an owner can bind it to a repository.
Broker destinationsOnly an owner can add one.
Set, generate, rotate, roll back, deleteWork as before.
TierCannot go below restricted until it is unsealed.
02Seal

Seal a secret

In the console, open the secret's row under Secrets → Projects and choose Seal, or use the CLI. Only owners can seal, and sealing waits for your approval (secret.seal). Dynamic secrets are not sealed; they have no stored value to reveal.

terminal
# Seal it. Owners only; it waits for your approval.
tilldev secrets seal DATABASE_URL --env prod

# Give it a value nobody has ever seen: made in the vault, never shown.
tilldev secrets gen DATABASE_PASSWORD --env prod --type password --length 40

# From now on:
tilldev secrets get DATABASE_URL --env prod
# x DATABASE_URL is sealed, so nobody can reveal it, owners included. ...
#   Ask: tilldev secrets releases ask DATABASE_URL --env prod --reveal --reason "…"
Seal, then rotate
A seal stops future reveals. It cannot take back a value someone already saw. Seal first, then set a new value with tilldev secrets gen so the vault makes it and no person or chat ever holds it.
03Release

Two owners to release it

  1. An owner asks, with a reason, to reveal it once or to unseal it. Asking waits for their approval (release.request).
  2. The other owners get an email. The request waits under Secrets → Releases for 24 hours.
  3. A different owner approves (release.approve, which waits for their approval too) or denies it. The asker can withdraw it.
  4. An approved reveal lets the asker reveal the current version once, within an hour, and the secret stays sealed. An approved unseal lifts the seal at once.
release
# Owner A asks, with a reason the other owners will see.
tilldev secrets releases ask DATABASE_URL --env prod --reveal --reason "restore drill"

# Owner B sees it waiting, and approves or denies.
tilldev secrets releases
tilldev secrets releases approve <id>        # or: deny <id>

# Owner A reveals it, once, within the hour.
tilldev secrets get DATABASE_URL --env prod

# Lifting the seal for good works the same way.
tilldev secrets unseal DATABASE_URL --env prod --reason "moving to a dynamic backend"

An owner can never approve their own request, and one secret has at most one waiting request of each kind. If the asker stops being an owner, their request can no longer be approved. A workspace with one owner can seal a secret but cannot release it until it adds a second owner. Admins can see requests but cannot ask or decide.

04Limits

What a seal does not stop

A seal closes the direct ways to read a value: the reveal button, secrets get, and the commands that would put it on a machine. Those are what a phished session or an agent with your login reaches for first.

Other ways out
An owner or admin can still send the value somewhere new: an environment-pinned service token, a sync target, a broker destination or a CI binding. Each of those waits for an approval and is written to the audit log, and a destination or CI binding for a sealed secret needs an owner. Seal what nobody should read, and watch the audit log for new tokens, targets and destinations.

A program that pulls the value at boot holds it in memory, as it would without a seal. Use the broker where you can, so the program never holds it at all.

05API

API

endpoints
POST /api/secrets/secrets/{id}/seal
GET  /api/secrets/releases?open=1[&secret_id=…]
POST /api/secrets/releases              {"secret_id": "…", "action": "reveal" | "unseal", "reason": "…"}
GET  /api/secrets/releases/{id}
POST /api/secrets/releases/{id}/approve
POST /api/secrets/releases/{id}/deny
POST /api/secrets/releases/{id}/cancel

A reveal of a sealed secret answers 403 with reason: "sealed". Asking answers 409 with not_sealed, needs_second_owner or pending (with the waiting request). Lowering a sealed secret's tier answers 409 with sealed. Listing an environment's secrets shows each one's sealed_at. See the API reference.

Audit
secret.seal, secret.unseal, release.request, release.approve, release.deny and release.cancel. A reveal through a release is a secret.read that names the release and the owner who approved it.