Skip to main content
Version: v5.6.1

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​

KindBackendGated by
openbaoOpenBao, over its HTTP APIbase platform
hashicorp-vaultHashiCorp Vault, over the same HTTP APIbase platform
aws-secrets-managerAWS Secrets Managerpacks:secrets-aws
azure-key-vaultAzure Key Vaultpacks: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​

AttributeMeaning
codeThe unique estate identifier the provider is addressed by (lowercase letters, digits, -, _, starting alphanumeric). Immutable after create.
display_nameThe human name the GUI renders.
kindThe backend kind (table above). Immutable after create.
kv_versionThe KV secrets-engine version, 1 or 2 (vault kinds).
addressThe 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_idThe cloud kinds' location facts — refused on the other kinds (see the pack pages).
auth_methodtoken | userpass | approle (vault kinds), access-key (AWS), client-secret (Azure). The method's secret half rides the write-only credential seam.
usernameThe non-secret identity half: the userpass username, the IAM access key id, or the Entra ID application (client) id.
role_idThe AppRole role id (not a secret; the secret id is the credential).
credential_setWhether a credential is stored; the value itself is write-only.
credential_refA 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_pathThe KV engine mount path (empty: the engine default); for the cloud kinds, an optional secret-name prefix.
ssl_verify / ssl_ca_certThe connection's TLS posture: verification on by default, with an explicit opt-out and a trusted CA file path.
cipher_stackingWhether 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_lengthLength of server-generated secret values (0: the endpoint default).
enabledWhether the provider resolves. A disabled provider resolves nothing and says so.
last_test_at / last_test_success / last_test_errorPer-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_ref re-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 the warn:server:secrets_refresh_deferred event 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 carrying credential_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.

  • 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).