vault
vault
¶
Vault passphrase CLI verbs — session unlock / lock plus passphrase management.
The unlock/lock pair drives the session-tier slot of the SQLCipher
passphrase resolution chain: unlock lands a passphrase on the
session-unlock tmpfs file; lock removes it. Everything else lives
under vault passphrase:
vault passphrase sealpromotes the current passphrase into a machine-boundsystemd-credscredential.vault passphrase to-keyringmoves it from whichever tier holds it now into the OS keyring (the recommended upgrade path off the session-file tier).vault passphrase revealresolves and prints the current passphrase (to/dev/ttyby default, or stdout with--allow-redirect) and offers to mark the recovery key as saved.vault passphrase acknowledgemarks the current passphrase as saved without displaying it — the silent ack a TUI / CI captures.vault passphrase changere-encrypts the DB under a new passphrase and rewrites every tier that stores the old one —change_passphraseis the prompt-free core the TUI shares.vault lockclears every stored copy of the passphrase — session file, keyring, and the sealed systemd-creds credential — so the vault becomes irrecoverable without an off-host copy. The machine-bound tiers are an automatic-unlock convenience on top of a passphrase the operator is expected to have saved; locking peels them away.purge_passphrase_tiersis the prompt-free corelockand panic share.
Each container mounts its own short-lived
VaultProxy
that resolves the passphrase on demand. vault unlock / vault
lock therefore only manage the passphrase tier; a supervisor that's
already running keeps the passphrase it resolved at spawn, so picking
up a changed tier means starting a fresh task (delete the matching
one — per the no-state-preservation rule).
VAULT_COMMANDS = (CommandDef(name='vault', help='Vault passphrase management', children=(CommandDef(name='status', help='Show lock state, the passphrase resolution chain, and stored secrets', handler=(LazyHandler('terok_sandbox.commands.vault:_handle_vault_status')), args=(ArgDef(name='--json', dest='as_json', action='store_true', help='Machine-readable JSON output'),)), CommandDef(name='unlock', help='Provision the credentials-DB passphrase for this session (tmpfs file)', handler=(LazyHandler('terok_sandbox.commands.vault:_handle_vault_unlock')), args=(ArgDef(name='--force', action='store_true', help='Write the session file even if a durable tier already unlocks the vault'),)), CommandDef(name='lock', help="Clear every stored copy of the passphrase — you'll need it to unlock again", handler=(LazyHandler('terok_sandbox.commands.vault:_handle_vault_lock')), args=(ArgDef(name='--force', action='store_true', help="Skip the 'have you saved the passphrase?' confirmation"),)), CommandDef(name='list', help='Inventory stored credentials (and optionally proxy tokens)', handler=(LazyHandler('terok_sandbox.commands.vault:_handle_vault_list')), args=(ArgDef(name='--include-tokens', action='store_true', help='Also show proxy-token rows (token values are masked)'), ArgDef(name='--json', dest='as_json', action='store_true', help='Machine-readable JSON output'))), _PASSPHRASE_GROUP)),)
module-attribute
¶
VAULT = VAULT_COMMANDS[0]
module-attribute
¶
__all__ = ['VAULT', 'VAULT_COMMANDS']
module-attribute
¶
SessionProvisionResult(written, validated=False, shadowed_durable=None)
dataclass
¶
Outcome of provision_session_passphrase.
written is the load-bearing bit: False means the write was
refused because a durable tier already resolves the vault, so a
session file would only shadow it (shadowed_durable names that
tier). Refusal is a normal outcome, not an error — callers report
it ("already unlocked via X") rather than raising. validated
says whether the written value was test-opened against an existing
DB (vs. an empty install where it becomes the key on first use).
TierRewrite(tier, ok, detail)
dataclass
¶
What happened to one passphrase-holding tier during a change.
ok means the tier now holds the new passphrase. A failed
rewrite (ok=False) is reported, never raised — by the time the
fan-out runs the DB is already rekeyed, so aborting would only
hide which tiers still need the operator's attention.
PassphraseChangeResult(passphrase, generated, rekeyed, rewrites)
dataclass
¶
Outcome of change_passphrase.
generated carries the same follow-up duty as
TierProvisionResult:
a minted value must be revealed to the operator. The recovery
marker has been dropped either way — whoever renders this result
owns re-running the acknowledgement flow.
passphrase
instance-attribute
¶
The new passphrase — mint or caller-supplied.
generated
instance-attribute
¶
True iff this call minted the value (caller passed None).
rekeyed
instance-attribute
¶
True iff an existing DB was re-encrypted (False on a
fresh install where the new value simply becomes the key on first
use).
rewrites
instance-attribute
¶
Per-tier outcomes, in resolution-chain order.
problems
property
¶
The rewrites that failed — non-empty means the operator has cleanup to do.
provision_session_passphrase(cfg, passphrase, *, force=False)
¶
Validate passphrase against the DB, then land it on the session tier.
The single writer of the session-unlock tmpfs file — the CLI
vault unlock and terok's TUI unlock modal both funnel through
here, so the no-shadow and validation guards apply to every caller
by construction; neither can store a value the DB rejects, nor
silently shadow a working durable tier.
Two guards, in order:
- No-shadow. The session tier is the highest-priority slot, so
writing it masks any durable tier (systemd-creds / keyring)
underneath — a reboot then wipes the session copy and the
operator only thought the sealed key was in use. When a durable
tier already resolves and force is false, nothing is written and
the result reports
written=False+ the shadowed tier. force (re-key / deliberate override) skips this guard. - Validation. When the DB exists (and isn't a legacy plaintext
file) the value is test-opened first; a mismatch raises
WrongPassphraseErrorand nothing is written. A missing DB skips validation (opening it just to check would create it as a side effect) — the value becomes its key on first use.
Source code in src/terok_sandbox/commands/vault.py
purge_passphrase_tiers(cfg)
¶
Remove every stored copy of the credentials-DB passphrase.
Clears the session-unlock tmpfs file, the OS keyring entry, the
sealed systemd-creds credential, and the
credentials.passphrase_command wiring in config.yml — then
drops the recovery-acknowledged marker, since it's meaningless once
no tier remains. After this the
vault can only be reopened by re-supplying the passphrase
(vault unlock); it is unrecoverable without an off-host copy.
No prompts and no acknowledgement check: this is the raw destructive
action. The lock verb gates it behind a typed-SAVED
confirmation when recovery is unacknowledged; panic calls it
directly — no questions asked.
Source code in src/terok_sandbox/commands/vault.py
handle_vault_seal(*, cfg=None, key='auto')
¶
Seal the credentials-DB passphrase into a systemd-creds credential.
Adds the systemd-creds tier to the resolution chain: machine-bound
(TPM2 + host key, or either alone), survives reboot, no OS
keyring required. After sealing, every new supervisor resolves the
passphrase via systemd-creds decrypt on start — no operator
interaction needed at boot, no plaintext-on-disk.
Requires an already-resolvable passphrase — typically from a fresh
vault unlock in the current session.
Source code in src/terok_sandbox/commands/vault.py
handle_vault_to_keyring(*, cfg=None)
¶
Move the current passphrase from its current tier into the OS keyring.
Resolves the passphrase via the chain (or prompts as a last resort),
writes it to the keyring, flips credentials.use_keyring to true
in config.yml, clears any plaintext credentials.passphrase /
credentials.passphrase_command wiring, and removes the
session-file and sealed systemd-creds copies.
The validate-before-destroy ordering is deliberate: if the keyring write fails, the source tier is still intact.
Source code in src/terok_sandbox/commands/vault.py
change_passphrase(cfg=None, *, old=None, new=None)
¶
Re-encrypt the vault under a new passphrase and rewrite every tier holding the old one.
The prompt-free core shared by the vault passphrase change CLI
verb and the TUI — same contract shape as
provision_passphrase_tier:
no /dev/tty, no ack prompt; the caller owns the conversation.
old is only consulted when the resolution chain can't produce the
current passphrase (locked vault) — when a caller supplies it
explicitly it wins over the chain, so a stale tier value can't
override an operator who knows better. new None mints a
fresh generate_passphrase
value (generated=True in the result — reveal it!).
The ordering is the safety argument:
- Escrow the new value first
(
vault_pending_passphrase_file, owner-only, RAM-backed): from this point a crash can never leave the DB encrypted under a key that exists nowhere on the host. - Verify + rekey the DB (
rekey_in_place). Every failure here — wrong old passphrase, a live supervisor holding the WAL ("database is locked") — aborts with nothing changed anywhere (the escrow is removed on the way out). - Only then fan the new value out to every tier that currently holds material, in resolution order, collecting per-tier outcomes instead of raising: a tier that can't take the new value (keyring denied, systemd-creds host regressed) is purged where possible so no tier keeps resolving the old passphrase, and reported either way. Once at least one tier holds the new value the escrow is deleted; if every rewrite failed it stays, so the key remains recoverable.
- Drop the recovery-acknowledged marker — the saved copy the operator confirmed is now the wrong passphrase.
Refuses up front (RuntimeError) while passphrase_command is
configured: that tier's secret lives in a store the operator owns,
so the sandbox rewriting everything else would leave the helper
resolving a stale value that fails closed on the next boot. Update
the external store first, or remove the wiring.
Raises NoPassphraseError when
neither the chain nor old yields the current passphrase,
WrongPassphraseError when
that value doesn't open the DB, and ValueError for
an empty or unchanged new.
Source code in src/terok_sandbox/commands/vault.py
631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 | |