Secrets providers
Included from Basic tiersecrets:multi-vaultpacks:secrets-awspacks:secrets-azure
A secrets provider is a registered vault connection — the backend the
server resolves secret-vault:// references through. Secret values never
live in properties, action files or run documents; everything carries a
reference, and the provider is where the referenced value actually lives.
The provider is a first-class connection object with the
identity-provider treatment end to end: addressed by an estate code,
an encrypted, write-only credential (requests carry it, the store
encrypts it at rest, and no response, audit row or error ever echoes it —
reads serve credential_set), audited writes, per-connection health and
a test verb. The hands-on walkthroughs are opschain docs getting-started/secrets-providers (operating the backends) and
opschain learn 3.5 (writing and using secrets); this page is the
component reference.
The kind set — open and additive
| Kind | Backend | Gated by |
|---|---|---|
openbao | OpenBao, over its HTTP API | base platform |
hashicorp-vault | HashiCorp Vault, over the same HTTP API | base platform |
aws-secrets-manager | AWS Secrets Manager | packs:secrets-aws |
azure-key-vault | Azure Key Vault | packs:secrets-azure |
The vault kinds share the OpenBao/Vault wire protocol today but stay
distinct kinds, so posture, defaults and future divergence are per-kind —
the set is open by construction, and a new backend arrives as a new kind
on the same connection object, CRUD verbs, test verb, health recording
and audit events. The cloud kinds are pack-gated: creating one, or
enabling a disabled one, answers the typed
ERROR_REASON_FEATURE_ENTITLEMENT_REQUIRED refusal naming the kind's
pack (see entitlements). Their kind-specific
configuration — field meanings, secret mapping, IAM/Entra ID
requirements — lives on their own pages:
AWS Secrets Manager and
Azure Key Vault.
The record, attribute by attribute
| Attribute | Meaning |
|---|---|
code | The unique estate identifier the provider is addressed by (lowercase letters, digits, -, _, starting alphanumeric). Immutable after create. |
display_name | The human name the GUI renders. |
kind | The backend kind (table above). Immutable after create. |
kv_version | The KV secrets-engine version, 1 or 2 (vault kinds). |
address | The vault endpoint URL (azure-key-vault: the vault URL, required; aws-secrets-manager: an optional endpoint override — empty derives it from the region). |
region / tenant_id | The cloud kinds' location facts — refused on the other kinds (see the pack pages). |
auth_method | token | userpass | approle (vault kinds), access-key (AWS), client-secret (Azure). The method's secret half rides the write-only credential seam. |
username | The non-secret identity half: the userpass username, the IAM access key id, or the Entra ID application (client) id. |
role_id | The AppRole role id (not a secret; the secret id is the credential). |
credential_set | Whether a credential is stored; the value itself is write-only. |
credential_ref | A provider-qualified secret-vault://<code>@<path>/<key> reference the auth credential lives at — an address in ANOTHER enabled provider's vault, never a value. Empty on a record whose credential is stored inline (encrypted at rest). A vault cannot custody its own key. |
mount_path | The KV engine mount path (empty: the engine default); for the cloud kinds, an optional secret-name prefix. |
ssl_verify / ssl_ca_cert | The connection's TLS posture: verification on by default, with an explicit opt-out and a trusted CA file path. |
cipher_stacking | Whether values written through /api/secrets/store are encrypted again by the server's codec on top of the backend's at-rest encryption (default true). |
password_length | Length of server-generated secret values (0: the endpoint default). |
enabled | Whether the provider resolves. A disabled provider resolves nothing and says so. |
last_test_at / last_test_success / last_test_error | Per-connection health, recorded by the test verb — the failure reason never carries credential material. |
Listings order by code ascending — there is deliberately no display order, because providers never render as an end-user picker.
Lifecycle
- Create registers a connection: code and kind immutable, the credential supplied through an environment variable or a no-echo prompt (never argv), encrypted at rest. An approle provider may defer its credential and seeds disabled until one is rotated in.
- Update partially updates — absent fields preserve stored values; a
supplied credential rotates the stored one (audited, never
echoed), and a supplied
credential_refre-pins the reference and clears any inline credential — custody moves to the named provider's vault. - Enable/disable ride the same update verb. The composition is
rebuilt whole on every provider or binding write and swapped
atomically, so enabling, rotating or disabling takes effect without a
restart. Every server also re-reads the composition periodically (the
5-minute backstop behind the cross-server invalidation). A re-read
whose database read fails — a database outage spanning the tick —
is not a composition: the last good composition stays in force,
secret resolution keeps working through it, and the re-read retries
every 2 s once the database answers; a working composition is never
replaced by a refusing one. Only a server that started inside the
outage, with no composition read yet, refuses secret resolution by
name (
the enabled secrets providers cannot be used: …) and heals the same way. The deferral is recorded as thewarn:server:secrets_refresh_deferredevent once the database answers. - Test probes with the stored credential exactly as resolution
would use it: token auth proves the token via a self-lookup,
userpass/approle perform the login, the cloud kinds perform their
list-shaped capability probes. The typed outcome rides the response
and is recorded as the connection's health — an unreachable vault is a
successful call with
success: false, the honest replacement for finding out at step time. - Delete removes a provider — refused by name while scope bindings still reference it, so a delete never silently re-routes bound scopes to the default. Deleting the enabled default means re-creating one or losing vault resolution: only an install that never had a provider falls back to the local AGE dev store.
The fallback ladder is deliberate: no provider registered → the local AGE dev store resolves references best-effort (an unresolvable reference passes through unresolved — it is a pointer, not a value, so nothing leaks); providers exist but all are disabled → resolution refuses loudly by name, never a silent fall-back.
Scope bindings and multi-vault routing
The first enabled provider is base platform and acts as the estate's
default resolution source. Enabling a second provider — whatever the
kinds — requires the secrets:multi-vault entitlement, as does
declaring a scope binding (the typed refusal names the feature).
A binding is one declared scope-path → provider mapping — declared and
auditable, never inferred: / binds the estate default,
/projects/<code> and /projects/<code>/environments/<code> override
it down the node tree, and the deepest covering scope wins (segment
boundaries, exactly like the properties overlay). An unprefixed
secret-vault:// reference resolves through the binding of the step's
node position; the provider-qualified form
secret-vault://<code>@<path>/<key> is the explicit cross-provider
escape hatch. Each binding read carries the bound provider's identity
and recorded health, so the posture view answers which parts of the
estate resolve against an unhealthy provider, and a resolve verb answers
provenance for any node path: which provider governs it and which scope
decided.
Two refusals keep routing honest: with several providers enabled and no root binding, default resolution refuses by name rather than guessing which vault governs unbound scopes; and a binding naming a disabled provider refuses resolution for its scope loudly rather than silently falling back to the default.
Licence lapse is polite: existing providers and bindings keep resolving — a lapse refuses new enabling writes and binding declarations but retains configuration untouched, and binding deletion stays ungated so a lapsed estate can always unwind. The runner step lease (below) ships ungated: security posture, never metered. See entitlements.
How steps consume it
Just before each step executes, the server resolves every whole-value
secret-vault://<path>/<key> reference in the step's converged
properties through the provider the bindings route to, and registers the
resolved values with the log masker — logs, events and stored properties
only ever show the reference. The reference grammar, secret-typed run
inputs and the masking walk-through are opschain learn 3.5.
For the vault kinds the server additionally projects the provider's
connection facts into the action step's runner as the OPSCHAIN_VAULT_*
environment — the variable set opschain-core reads during action setup
to resolve secret-vault:// references inside the step itself. That
in-runner resolution lane is the Ruby runner lane, so it is only
applicable to existing MintPress customers and those using the MintPress
SDK or the packs:mintpress feature; every other step still gets its
properties resolved server-side, before dispatch, masked in logs. The
cloud kinds resolve server-side only and project no runner environment —
each pack page records that gap precisely.
On that projected lane, the estate credential never rides the job spec:
per action step the server mints a response-wrapped child token from
the step's bound provider (TTL bounded to the step's timeout plus slack,
non-renewable, metadata carrying run/step identity — never values) and
ships it as the spec's additive vault lease; the runner bridges the
unwrapped lease into OPSCHAIN_VAULT_TOKEN just-in-time. The wrapping
token is single-use with a short TTL, so one that expired or was already
unwrapped refuses loudly at the runner — the interception-evidence
property. Path scoping rides the standard token-role mechanism (a
mount-scoped ACL policy and token role, provisioned idempotently); an
estate whose server credential may not write policies degrades one tier
(the child carries the parent's own policies), and one refusing child
minting entirely degrades to the credential-env projection — both
degrades named in the step's log, never silent. Runner sessions
predating the lease protocol, and in-process dispatch, keep the
credential env projection: the payload is additive both ways.
Interactive reads are different from platform resolution: the
decrypt/reveal lane authenticates to the vault as the asking user,
with each user's own stored vault credential and the vault's own ACLs
deciding what they may read — no stored personal credential, no read.
The verbs, the step-up floor on decrypt-for-display and the audit trail
are in opschain learn 3.5.
Writes are explicitly addressed, never binding-routed. A store
(vault store / vault store-file, or the store API verbs) with an
unqualified secret-vault://<path>/<key> reference lands in the
platform's default provider, as always. A provider-qualified reference
(secret-vault://<code>@<path>/<key>) writes through the named
provider instead, authenticated as the asking user with their own stored
credential — the same identity rule as interactive reads. An unknown or
disabled provider code refuses by name, a caller without a personal
credential for the named provider gets the typed credential refusal, and
a qualified write is never silently redirected to a different vault.
Scope bindings route step reads; they never redirect a write.
Settings and configuration
No config.yaml key tunes this component at runtime. The retired
vault.* provider keys (and the server-process OPSCHAIN_VAULT_*
environment) survive only as the upgrade seed's input: they are consumed
once at boot to write one enabled provider (recommended code vault),
after which a config.yaml still carrying provider keys refuses boot by
name — see opschain docs config. The seed never connects to the vault;
the operator's first action is the test verb. Per-provider knobs
(cipher_stacking, password_length, TLS posture) live on the record
itself.
Governance and audit
Every verb sits behind the /admin updatable determination
(permission denied on deny), over the v2 user-token lane — the caller's
own opschain login session, with the admin-token fallbacks the CLI
manual records. Compositions without the secrets-provider plane answer
not-found, the nil-domain convention.
Events (all catalogued on opschain docs events):
api:secrets_providers:create/update/destroy— connection lifecycle; connection facts only, never a credential, a rotation carryingcredential_rotated: true.api:secrets_providers:test— a probe ran; the provider's code and the success flag.api:secrets_provider_bindings:create/update/destroy— a scope binding declared, re-pointed or removed; the scope path and the bound provider's code.
Entitlements, in one line each: the component itself carries no gate;
the cloud kinds gate on packs:secrets-aws / packs:secrets-azure
(create/enable only, polite lapse); multi-vault — the second enabled
provider and every binding declaration — gates on secrets:multi-vault
(polite lapse, binding deletes ungated). All three are
entitlements territory.
Related pages
opschain docs cli/admin-secrets-providers— the command family (list,show,create,update,delete,test), verb by verb.opschain docs cli/admin-secrets-bindings— binding semantics (list,set,delete,resolve).opschain docs getting-started/secrets-providers— the operator walkthrough;opschain learn 3.5— writing and using secrets.opschain docs packs-secrets-aws/opschain docs packs-secrets-azure— the cloud kinds' configuration detail.opschain docs entitlements— the gates and the lapse posture.- The API service is the v2
SecretsProviderService(opschain docs api-v2).