Secrets — OpenBao + Vault Agent
Nothing secret is ever committed. Secrets live in OpenBao (vault.stump.rocks);
a Vault Agent under launchd renders them to local files on a schedule, and the
shell sources the result.
Use the
vaultCLI (HashiCorp, API-compatible with OpenBao). The Homebrewbaobinary is an unrelated BLAKE3 hashing tool — not OpenBao.
The flow
- Env secrets — the static template dynamically exports every field of
every
secret/users/<you>/*KV secret. Add a new secret → it shows up automatically (next render orvault-agent restart).sshis skipped (it's files). This shell-formatsecrets-static.envis what most consumers read:~/.oh-my-zshsources it, and on macOS therocks.stump.harnessLaunchAgent sources it before exec'ing the harness daemon, so every supervised agent session inherits the full set (harness layers each agent'senv_fileon top). - systemd
EnvironmentFilecopy — the agent also renders the same secrets tosecrets-static.systemd.envin systemdEnvironmentFilesyntax (noexport, double-quoted), because systemd'sEnvironmentFile=parser can't read the shell format.harness.serviceconsumes it (EnvironmentFile=-…), which is how supervised agents get their secrets on Linux without a login shell in the loop. - SSH keys — rendered to
~/.ssh/id_rsa(0600) andid_rsa.pub(0644) fromsecret/users/<you>/ssh.
Add a secret
vault kv put secret/users/<you>/myservice MY_API_KEY=sk-…
# wait ≤5 min (or: vault-agent restart), then:
exec zsh
echo $MY_API_KEY # there it is
That's it — no template edits. The agent discovers it.
Day-to-day
| Task | Command |
|---|---|
| Is the agent up? | vault-agent status |
| See what it rendered | vault-agent env |
| Tail its log | vault-agent log |
| Force a re-render | vault-agent restart |
| Re-auth (agent down) | czapprole --local (re-provisions AppRole) or vault login -method=oidc (fallback) |
Blast radius (per-host AppRoles)
Prefer a per-host role over the legacy shared personal-vault-agent so one
compromised or retired box can be cut off without re-provisioning the fleet:
# once per host, as admin — creates vault-agent-<host> and merges its login
# alias into your identity entity (required for the templated policy):
~/src/dotfiles/scripts/openbao-approle-setup.sh ie01
# optionally pin its creds to the box's network:
~/src/dotfiles/scripts/openbao-approle-setup.sh --cidr 10.0.0.5/32 ie01
czapprole joestump@ie01.stump.rocks # auto-selects vault-agent-ie01
# revoke JUST that box later:
vault delete auth/approle/role/vault-agent-ie01
czapprole falls back to the shared role (with a warning) for hosts provisioned
before per-host roles existed, so nothing breaks mid-migration.
SSH keys: an untrusted/throwaway box shouldn't hold the shared private key.
Opt it out in that box's ~/.config/chezmoi/chezmoi.toml — the Vault Agent there
will render env secrets but never ~/.ssh/id_rsa:
[data]
vaultSshKeys = false
Over SSH (utility nodes)
OIDC login opens a localhost:8250 callback that needs your laptop's browser. The
tooling detects you're remote and prints the tunnel. Fastest path, from your laptop:
vault-login <host> # opens the tunnel AND logs in on that host
The agent itself talks to OpenBao directly, so once you're logged in, everything renders without a tunnel.
The rules
- Secrets never in the repo,
.envrc, or~/.zprofile. - A
gitleakspre-commit hook + CI scan block accidental commits. - Non-secret config (hosts, ports, regions) goes in a project
.envrc(direnv).