Programmatic access (service accounts)
Programmatic access (service accounts)
Sometimes a script or a deploy pipeline needs a vault’s secrets — a CI job that injects environment
variables, an Ansible run, a container that reads its config on startup. Instead of a person copy-pasting
values, a service account can pull them over the API and a small command-line tool decrypts them on
your machine into a .env file.
The important guarantee is unchanged: lazyit still cannot read your secrets. The server only ever hands back encrypted data; the decryption happens on your machine, with the service account’s token. See Security model for what “the server can’t read them” means.
How it works, in one picture
A service account gets its own encryption key, exactly like a person does. You then add the service account to a vault (the same “add member” action you use for people). From then on, a headless tool can fetch that vault’s encrypted secrets and unlock them locally:
- The service account’s private key is locked with its token (the
lzit_sa_…credential shown once when you create it). - Adding the service account to a vault wraps that vault’s key to the service account — the standard grant. You can only do this for a vault you can already read.
- The
lazyit-fetchtool sends the token to the API, gets back only ciphertext, and unlocks it on the deploy machine.
Because the key is per vault, a token can read only the vault(s) that service account was added to — never everything. If you want tighter isolation, put sensitive secrets in a separate vault and add the service account only there.
Step 1 — Create the service account and its key
- In Settings → Service accounts, create a service account and grant it the Fetch secrets
programmatically permission (
secret:fetch). This is the only secret permission a service account can hold — it can never be given the human “view” or “manage” permissions. - On creation the app generates an encryption keypair for every service account (not only Fetch ones), locked with its token. Copy the token now — it is shown once and is what the fetch tool uses to decrypt. If you lose it, rotate the token (see below), which re-issues the key.
Older service accounts without a key. A service account created before this feature has no encryption key, so it can’t be added to a vault yet (the grant dialog will say so). Rotate its token in Settings → Service accounts — that generates a key for it. Rotating always re-issues the key, so it removes the service account from every vault it belonged to; add it back afterwards (Step 2).
Step 2 — Add the service account to a vault
- Open the vault in the Secret Manager and use Add service account in the members area.
- Pick the service account. Your browser re-encrypts the vault’s key to the service account’s key — so, as always, you can only grant a vault you can read yourself.
The service account now appears as a machine member of that vault. Revoke it any time with the member’s remove action (this stops future reads; rotate the underlying credential if you suspect the token leaked).
Step 3 — Fetch secrets on the deploy machine
The lazyit-fetch command-line tool lives in the monorepo (packages/fetch-cli) and is not
published to a package registry — so there is no npx/bunx install. Instead you run a standalone
binary compiled from it. From a checkout of the monorepo, build the binaries once:
# Compiles dist/lazyit-fetch-x64 and dist/lazyit-fetch-arm64 (self-contained Linux executables).
bun run --filter @lazyit/fetch-cli compile
Copy the binary that matches your server’s architecture (e.g. lazyit-fetch-x64) to the deploy machine.
It needs no Bun or Node runtime there. Then give it the token, the API URL and the vault id:
# The token is read from an env var so it never lands in your shell history or `ps` output.
export LAZYIT_SA_TOKEN="lzit_sa_…"
# Write a .env file in the current directory:
./lazyit-fetch-x64 --api https://lazyit.example.com/api --vault <vaultId> --out .env
# …or print to stdout to compose with other tools:
./lazyit-fetch-x64 --api https://lazyit.example.com/api --vault <vaultId> > .env
# List the vaults this service account may fetch:
./lazyit-fetch-x64 --api https://lazyit.example.com/api --list
# Verify the tool's crypto without touching the server (no API, no token needed):
./lazyit-fetch-x64 --self-check
Running from a checkout (for testing). If you already have the monorepo, you can run the tool straight from source with Bun instead of compiling — handy while trying things out:
bun packages/fetch-cli/src/index.ts --api <url> --vault <vaultId>.
Each secret becomes one line, HANDLE=value. The handle is upper-cased and non-alphanumeric
characters become _, so prod-db-password becomes PROD_DB_PASSWORD. Choose handles that make good
environment-variable names.
Staying compatible
lazyit-fetch is stamped with its own version. On each run it quietly checks your server’s version,
and if the tool is a major version behind it prints a one-line warning to stderr (never to the
.env output, so piping stays clean) telling you to update the binary. It’s a hint only — the fetch
still runs. The check is best-effort: if the server can’t be reached or is older, it stays silent, and
a tool run from source reports as dev and never warns.
What the server sees, and what it doesn’t
- The server returns ciphertext only — the encrypted values plus the encrypted keys the tool needs. It never returns a plaintext value, and it never decrypts one.
- Every programmatic fetch is recorded — which service account read which vault, and when.
- The token is the key. Anyone who holds it can decrypt that vault’s secrets, so treat it like the secrets themselves: keep it in a secure environment variable, scope each service account to only the vaults it needs, and rotate it if it may have leaked.