Credentials DB encryption¶
The credentials DB (SSH private keys, AI-provider secrets, proxy tokens) is SQLCipher-encrypted at rest. AES-256, mandatory — there is no plaintext mode.
Where the passphrase comes from¶
Every per-container supervisor and CLI call walks the same chain top-to-bottom and stops at the first hit:
- Session-unlock file —
$XDG_RUNTIME_DIR/terok/sandbox/vault.passphrase, RAM-backed, cleared on reboot. Written byvault unlock. - systemd-creds — sealed credential at
${XDG_DATA_HOME:-~/.local/share}/terok/vault/vault.passphrase.cred(XDG_DATA_HOMEis rarely set — the~/.local/sharefallback is what most hosts hit; the path matchesvault status'sDB:line directory). Decrypted viasystemd-creds(1). Machine-bound (TPM2 or host key), survives reboot, no keyring needed. Written byvault passphrase seal. Requires systemd ≥ 257. - OS keyring —
(service=terok-sandbox, username=credentials-db), used only whencredentials.use_keyring: trueis set inconfig.yml. - passphrase_command — operator-supplied shell command set as
credentials.passphrase_commandinconfig.yml. Same shape asgit config credential.helper, ssh pinentry, orBORG_PASSCOMMAND— one field plugspass,bw,op,vault kv, or any cloud secret-manager CLI into the resolver without per-backend code in the sandbox. See Headless setup below for the canonical recipe. Fails closed when the helper is configured but exits non-zero / times out — silent fall-through to plaintext would be an unannounced security downgrade. - Config fallback —
credentials.passphraseinconfig.yml. Plaintext-on-disk; only as strong as filesystem-layer protection (LUKS / signed image / permissions).vault statusand sickbay permanently surface a WARNING when this tier is configured. Last-resort on hosts with no systemd-creds and no usable helper — preferpassphrase_commandwhenever the operator haspass,bw,op, or a cloud secret-manager CLI available. - Interactive prompt —
*-masked, TTY only. CLI calls; non-interactive supervisors fail loud instead.
Headless setup (data-center terminals)¶
For hosts reached over SSH where systemd-creds isn't available (older
systemd, shared cluster nodes
with no per-user TPM), point passphrase_command at whichever
credential helper the operator already trusts:
The resolver tokenises with shlex.split and runs
subprocess.run(...) with a 30-second timeout, then strips trailing
whitespace from stdout. Anything that prints a passphrase on stdout
works; ready-to-use recipes (🤖 guesses, not manually verified):
| Backend | passphrase_command value |
|---|---|
pass (gpg-agent) |
pass show terok-sandbox/vault-passphrase |
| Bitwarden CLI | bw get password terok-sandbox |
| 1Password CLI | op read op://vault/terok/passphrase |
| HashiCorp Vault | vault kv get -field=passphrase secret/terok |
| AWS Secrets Manager | aws secretsmanager get-secret-value --secret-id terok-vault --query SecretString --output text |
| GCP Secret Manager | gcloud secrets versions access latest --secret=terok-vault |
| Azure Key Vault | az keyvault secret show --vault-name terok --name vault-passphrase --query value -o tsv |
The pass recipe is the canonical headless choice: gpg-agent caches
the GPG key passphrase for the session, so after one prompt each
per-container supervisor resolves it silently without re-prompting.
Provision the entry once on a
trusted workstation, then pass git push to your sync remote and
pass git pull on the data-center host — the encrypted store sits in
the same repo your dotfiles already follow.
Diagnostics from the helper land in the per-container supervisor logs
($XDG_STATE_HOME/terok/logs/<container-id>.log); look for lines
like passphrase_command 'pass' exited 1: <stderr>. Each
container's supervisor walks the passphrase chain when it spawns.
Day-to-day¶
terok-sandbox vault unlock # writes the session-unlock tmpfs file
terok-sandbox vault lock # removes the session-unlock tmpfs file
vault unlock is normally run once per boot. The next supervisor to
start picks the freshly-resolved passphrase up automatically.
Picking a tier at setup¶
terok setup (or terok-sandbox setup) auto-detects systemd-creds
when the host supports it (systemd ≥ 257 with the io.systemd.Credentials
Varlink service) and uses it silently — that's the strongest tier
available, and asking when the answer is unambiguous just slows the
install down. Either host+tpm2 (TPM-equipped hosts) or host only,
chosen by systemd-creds --with-key=auto.
When systemd-creds isn't available, setup asks once:
| Choice | When to pick it |
|---|---|
[k] OS keyring (default) |
desktop with a working Secret Service / Keychain |
[s] session-unlock |
servers with no keyring; one vault unlock per boot |
[c] config file |
last-resort plaintext-on-disk; requires yes confirmation |
Either branch auto-generates the passphrase and prints it once ("write this down") — that's your recovery key for rebuilds and other hosts.
Changing tiers (move the passphrase to a different backend)¶
The passphrase is one secret; the tier is just where it lives.
Upgrade to keyring or systemd-creds (first-class commands)¶
For the two most common upgrade paths — moving off the session-file or plaintext-config tiers onto the OS keyring or a machine-bound sealed credential — one verb does the whole swap:
# Move the passphrase from its current tier into the OS keyring.
terok-sandbox vault passphrase to-keyring
# Move it into a machine-bound systemd-creds credential.
# (Land it as session first if not already auto-resolvable, then seal.)
echo -n "<passphrase>" | terok-sandbox vault unlock
terok-sandbox vault passphrase seal --key=auto
terok-sandbox vault passphrase destroy # clear lower-tier copies
to-keyring resolves the passphrase from whichever tier currently
holds it, validates it, writes to the keyring, flips
credentials.use_keyring: true in config.yml, drops any plaintext
fallbacks, and removes the session/sealed copies. No retrieve-then-reseed
by hand — the next per-container supervisor to spawn resolves the
passphrase fresh from the keyring.
Manual three-step (for anything not on the upgrade path)¶
For other transitions (keyring → systemd-creds, anything →
passphrase_command, downgrades) the swap is still retrieve →
destroy → reseed:
1. Retrieve from the current tier¶
Keep the value somewhere safe for the duration of the swap — a
password manager, or a mktempd file you delete after step 3.
2. Destroy the stored passphrase¶
Clears every persistent tier in one go (session file plus keyring,
sealed systemd-creds, credentials.passphrase, and
credentials.passphrase_command). The underlying secret stays put
in whichever store the helper points at (pass, 1Password, Vault,
…) — only the resolver wiring is removed. Without this step, the
next per-container supervisor to spawn would resolve the passphrase
from a leftover tier, defeating the swap.
3. Provision in the new tier¶
# → session-file (default; ephemeral, cleared on reboot):
echo -n "<passphrase>" | terok-sandbox vault unlock
# → systemd-creds (machine-bound, persistent):
echo -n "<passphrase>" | terok-sandbox vault unlock # land it as session first
terok-sandbox vault passphrase seal --key=auto # then seal from session
terok-sandbox vault lock # remove the session file
# → OS keyring:
terok-sandbox vault passphrase to-keyring # one verb, no chooser
# → passphrase_command (headless; helper points at pass / bw / op / cloud CLI):
pass insert -m terok-sandbox/vault-passphrase # or your helper's
# "store this secret" verb
yq -yi '.credentials.passphrase_command = "pass show terok-sandbox/vault-passphrase"' \
~/.config/terok/config.yml
# → config.yml plaintext (last-resort; requires `yes` confirmation):
terok-sandbox setup # chooser → [c] → type "yes" to accept the trust boundary
Run terok-sandbox vault status afterwards to confirm
Passphrase: resolved via <new-tier> — and to verify no stale
plaintext WARNING is still pointing at config.yml.
Recovering from a lost passphrase¶
There is no recovery key, no backdoor, no master key. The passphrase is the only thing that unlocks the credentials DB; if every tier loses it (you forget it, the keyring resets, the sealed systemd-creds blob is gone with the host), the contents are irrecoverable.
What you lose:
- Every SSH private key registered in the vault (the ones
ssh addimported or generated). - Every AI-provider credential set the agents use.
- The phantom tokens minted for in-flight container scopes.
What to do — accepting that the encrypted data is gone:
# 1. Clear any tier wiring that points at the lost passphrase.
terok-sandbox vault passphrase destroy
# 2. Delete the encrypted DB and any leftover backup tarballs.
rm "${XDG_DATA_HOME:-$HOME/.local/share}/terok/vault/credentials.db"
rm "${XDG_DATA_HOME:-$HOME/.local/share}/terok/vault/credentials.db.plaintext-backup-"*.tar.gz 2>/dev/null
# 3. Re-run setup; it mints a fresh passphrase and creates a clean DB.
terok-sandbox setup
After step 3 you have an empty vault — re-import your SSH keys, re-add provider credentials.
Back the passphrase up before you lose it.
Doctor / sickbay¶
If the DB is encrypted but no tier resolves a passphrase, terok
sickbay reports the vault as locked with the unlock hint. The TUI
surfaces the same state in its status line.