Secret Manager

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:

  1. The service account’s private key is locked with its token (the lzit_sa_… credential shown once when you create it).
  2. 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.
  3. The lazyit-fetch tool 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

  1. 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.
  2. 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

  1. Open the vault in the Secret Manager and use Add service account in the members area.
  2. 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.