Resolve secrets from Vault, SOPS, or 1Password
OrchStep doesn't reinvent secret storage — it shells out to the tool you already run. One cmd resolver, fetch-once-pull-many with field extraction, and the value stays masked end to end.
blog/resolve-from-vaultYou already chose a secret manager. Maybe it is HashiCorp Vault, maybe SOPS-encrypted files in git, maybe 1Password or AWS Secrets Manager. The last thing you want is a workflow tool with its own competing notion of where secrets live and its own plugin you have to keep updated for each backend.
OrchStep sidesteps that entirely. Its secret resolver is just a command. Every one of those tools ships a CLI, so OrchStep runs the CLI, takes the output, and treats it as a masked secret. No plugin matrix, no new place to store credentials — your existing tool stays the source of truth.
One resolver, any backend
The same cmd: shape covers every manager — you just change the command:
secrets:
DB_PASSWORD: { cmd: "vault kv get -field=password secret/db" } # HashiCorp Vault
API_KEY: { cmd: "op read op://prod/api/key" } # 1Password CLI
TLS_KEY: { cmd: "sops -d --extract '[\"tls\"][\"key\"]' secrets.enc.yaml" } # SOPS
RDS_PW: { cmd: "aws secretsmanager get-secret-value --secret-id rds --query SecretString --output text" }cmd: runs the command and uses its trimmed stdout as the secret value. That value is tainted the instant it's captured — masked in logs, scrubbed from output, excluded from run history.
Auth comes from the environment
Most secret CLIs need to authenticate first (VAULT_ADDR + VAULT_TOKEN, OP_SERVICE_ACCOUNT_TOKEN, AWS creds). A cmd: resolver runs with the workflow's full environment — the host env, anything from dotenv:, your env: values, and other already-resolved secrets — so you provide auth the same way you provide any other variable:
dotenv: [.vault.env] # VAULT_ADDR=... VAULT_TOKEN=...
secrets:
DB_PASSWORD: { cmd: "vault kv get -field=password secret/db" }The auth token can itself be a secret, resolved first and handed to the next command:
secrets:
VAULT_TOKEN: { env: CI_VAULT_TOKEN }
DB_PASSWORD: { cmd: "VAULT_TOKEN={{ secrets.VAULT_TOKEN }} vault kv get -field=pw secret/db" }Fetch once, pull many fields
Most secret stores return a bundle — a JSON blob with several fields. You don't want to run the (slow, rate-limited) CLI once per field. Give several secrets the same command and add a field: to extract each value; identical commands run only once per run and the result is cached:
name: resolver
# Fetch once, pull each field. In production the cmd is
# vault kv get -format=json secret/app (or `op item get ... --output json`).
# Here a printf stand-in returns the same JSON shape so the demo runs offline.
# Identical cmds run only once per run — the result is cached.
secrets:
DB_USER:
cmd: "printf '{\"data\":{\"user\":\"app\",\"pass\":\"s3cr3t\"}}'"
field: '{{ result.data.user }}'
DB_PASS:
cmd: "printf '{\"data\":{\"user\":\"app\",\"pass\":\"s3cr3t\"}}'"
field: '{{ result.data.pass }}'
tasks:
migrate:
# The real values reach psql via the OS env; the run history stays masked.
env:
PGUSER: "{{ secrets.DB_USER }}"
PGPASSWORD: "{{ secrets.DB_PASS }}"
steps:
- name: run
func: shell
do: 'echo "migrating as user={{ secrets.DB_USER }} pass={{ secrets.DB_PASS }}"'field: navigates the parsed result — so vault kv get -format=json becomes field: "{{ result.data.data.user }}", and the printf stand-in above (one level shallower) is field: "{{ result.data.user }}". Both DB_USER and DB_PASS share a command, so the fetch happens exactly once. Run it and both values are masked, even though the migration tool gets the real ones:
$ orchstep run migrate
$ echo "migrating as user=*** pass=***"
migrating as user=*** pass=***When fetching needs real logic
Sometimes a producer needs a login step, a retry, or a reusable module — more than one command can express. Make the producer a task and pull values from its step outputs with field::
tasks:
vault-creds:
steps:
# Keep login and read in ONE step so they share a shell session.
- name: fetch
func: shell
do: |
vault login -method=aws >/dev/null
vault kv get -format=json secret/app
outputs:
user: "{{ result.output | fromJson | dig \"data\" \"data\" \"user\" \"\" }}"
pass: "{{ result.output | fromJson | dig \"data\" \"data\" \"pass\" \"\" }}"
secrets:
DB_USER: { task: vault-creds, field: "{{ steps.fetch.user }}" }
DB_PASS: { task: vault-creds, field: "{{ steps.fetch.pass }}" } # task runs once, not twiceA producer task's output is never displayed — it is secret material by definition. (Each step is its own process, so keep an auth step and the read it enables in the same do:; a vault login in a separate step wouldn't carry over.)
What you get for the wiring
| Without OrchStep | With the cmd: resolver | |
|---|---|---|
| Backend support | a plugin per tool | any CLI you already run |
| Where secrets live | a new place to trust | your existing manager, unchanged |
| Bundle of fields | N calls, or hand-parsing | one fetch, field: per value |
| Leakage into logs/history | your problem | masked + excluded automatically |
Planning (--dry-run) | needs credentials | resolver never runs |
Where to go next
- Secrets — producers (
cmd:,env:,task:),field:, and auth in depth - Secrets without leaks: the conduit model — the masking guarantees
- Environment Variables — handing a resolved secret to a tool via
env:
The fetch-once demo above runs offline in orchstep-demos — replace the printf with vault kv get -format=json, op item get --output json, or sops -d and nothing else changes.
curl -fsSL https://orchstep.dev/install.sh | sh