← All posts

tsafe's trust model: profiles, namespaces, and the strip list

The tsafe exec post covered the surface: wrap your command, secrets get injected, nothing in history. But exec is built on top of a trust model with more structure than that. This post covers the model itself — profiles, namespaces, modes, the strip list, and how they compose.

Profiles are security boundaries

Most developers think of profiles as organizational grouping — dev, staging, prod. In tsafe, a profile is a security boundary. A process can only receive secrets from the profile it is explicitly given. There is no way to access another profile's secrets without explicitly specifying that profile.

Profile as security boundary
PropertyWhat it means in practice
Independently encryptedEach profile has its own vault file, its own Argon2id-derived key. Compromising one profile's password does not expose others.
Explicit grant onlyA process only receives secrets from the profile specified via --profile or TSAFE_PROFILE. No ambient cross-profile access.
CI isolationA CI job with TSAFE_PROFILE=ci can only see the ci profile's secrets, even if the vault file contains dev and prod profiles too.
Compare without decryptingtsafe compare dev prod shows key-name differences across profiles without decrypting any values.

Namespaces scope injection

Within a profile, namespaces are key prefixes — api/KEY, db/KEY, build/KEY. The --ns flag scopes injection to one sub-tree and strips the prefix from the env var names.

PortableText [components.type] is missing "code"

Everything outside the declared namespace is withheld. The child process cannot see db/DATABASE_URL by trying to read DATABASE_URL — it isn't there. This is the same model as CellOS secretRefs: declare what you need, get exactly that.

Scope control flags
FlagBehaviourOn missing key
(none)Inject all secrets from the active profile
--ns <name>Inject only keys under <name>/; prefix strippedSilent — namespace may be empty
--keys k1,k2Inject exactly these named keysFails fast — aborts before exec
--require k1,k2Assert these keys exist before exec; does not restrict injectionFails fast — aborts before exec
--mode hardenedMinimal inherited env, redacted child output, dangerous-name denyRejects keys matching credential name patterns

The strip list

Before injecting vault secrets into the child environment, tsafe exec explicitly removes a set of parent process credentials. The child cannot inherit your pipeline credentials by accident.

  • PortableText [components.type] is missing "span" — the vault master password
  • PortableText [components.type] is missing "span", PortableText [components.type] is missing "span" — service principal credentials
  • PortableText [components.type] is missing "span" — HashiCorp Vault token
  • PortableText [components.type] is missing "span" — GitHub Actions identity token

The strip happens before injection. Your application code cannot access the credentials that tsafe itself used to open the vault, even if they were in the parent environment.

Trust modes

Exec trust modes
ModeWhat changesUse case
standardDefault. Inherits most parent env; no output redaction.Development, general CI use
hardenedMinimal inherited env; child output redacted before logging; dangerous key names denied (keys with names like *_SECRET, *_PASSWORD blocked from injection).Hostile environments, compliance-sensitive pipelines, agent-driven automation
customPersisted per-profile settings, configured via tsafe config.Team-standard posture without per-command flags

Session scope and the agent

Vault access requires a live session — there is no always-on ambient access mode. The agent holds a session token with a configurable idle TTL.

PortableText [components.type] is missing "code"

The TTL is idle-based. Activity resets the timer. When the TTL expires, the session ends and the next vault operation requires re-authentication. The agent holds the vault password in memory for the TTL — it does not persist to disk.

The cryptographic layer

Cryptographic implementation
PropertyImplementationWhy this choice
KDFArgon2id — 64 MiB memory, 3 iterations, 4 parallel lanesOWASP recommended; memory-hardness defeats GPU/ASIC attacks
CipherXChaCha20-Poly1305 (IETF standard)Same as Signal, WireGuard, age; well-audited; 192-bit nonce eliminates nonce-reuse risk
RNGOS-level CSPRNG (BCryptGenRandom / /dev/urandom)No userspace entropy pool; hardware-seeded
Key zeroizationzeroize crate — ZeroizeOnDrop on VaultKey and all secret valuesDeterministic zeroing at drop time, not at GC mercy
Master passwordrpassword for masked input; zeroed after key derivationNever in shell history; never on heap longer than KDF call

Multi-source sync

The local vault is the execution surface. The cloud vault is the source of truth. tsafe bridges them.

Supported secret sources for kv-pull
SourceCommandAuth method
Azure Key Vaulttsafe kv-pull --prefix MYAPP_Managed identity (VMs/ACI), service principal, or interactive az login
HashiCorp Vaulttsafe vault-pull --mount secret --prefix myapp/VAULT_TOKEN in env
1Passwordtsafe op-pull 'Database Credentials' --op-vault Personal1Password CLI (op) authentication

The pull populates the local vault. From there, exec injects. The source of truth (AKV, 1Password, wherever) is never accessed at runtime by your application — only by the explicit kv-pull sync step.

Putting it together

A fully configured tsafe workflow for a team looks like: one profile per environment, namespaces per service or role, kv-pull in the morning to sync from AKV, exec with --ns to run services, hardened mode in CI. The vault file is in the repo (encrypted ciphertext). The only pipeline secret is TSAFE_PASSWORD.

PortableText [components.type] is missing "code"