Secret Backends

July 9, 2026 ยท View on GitHub

Last modified: 2026-07-09

SBproxy resolves secret material through provider-specific reference schemes. The scheme names the provider type, the authority names the configured backend instance, and the path is interpreted by that provider:

<scheme>://<backend-name>/<provider-path>[?version=<n>][&key=<json-field>]

Backend instances are declared once, at proxy scope, under proxy.secrets.backends:. There is no per-tenant or per-origin backend list. A reference resolves against the backend whose name matches the authority segment and whose provider type matches the scheme; to keep tenants on separate stores, declare one named backend per store and reference the right name from each origin.

Scheme Table

SchemeProvider typeExample
vault://HashiCorp Vault KVvault://primary/secret/data/openai-prod?key=api_key
awssm://AWS Secrets Managerawssm://primary/openai-prod?version=3&key=api_key
gcpsm://GCP Secret Managergcpsm://primary/openai-api-key?version=latest
k8ssecret://Kubernetes Secretk8ssecret://primary/sbproxy-secrets/openai-key
secretfile://Local YAML or JSON secret filesecretfile://local/openai-prod?key=api_key
secret://Local static secret mapsecret://local/openai-prod

Environment variables keep the existing ${ENV_NAME} form. Do not use an env URI. The legacy non-vault forms also remain valid:

${ENV_NAME}
file:/path/to/secret

The Go-era secret:<name> colon form is removed: it now fails config load with a pointer at the secret://<backend>/<name> replacement, and the proxy.secrets.map key that served it parses but has no effect. The old umbrella form, vault://<alias>/..., is still accepted with a warning as of SBproxy 1.5.0; a removal release has not been announced. To rewrite known aliases, run:

sbproxy config migrate sb.yml --out sb.migrated.yml

HashiCorp Vault

The HashiCorp client speaks KV v1 or KV v2 against Vault OSS or Vault Enterprise. The operator picks one of three auth methods at backend construction.

Configuration

proxy:
  secrets:
    backends:
      - type: hashicorp
        name: primary
        addr: https://vault.shared.example/v1
        mount: secret/tenants/acme-corp
        engine: v2
        cache_ttl_secs: 300
        auth:
          type: token
          token: ${VAULT_TOKEN_ACME}
FieldTypeDescription
addrstringVault server URL. Trailing slash is normalised.
mountstringKV mount path. Tenant-isolated deployments scope this to a per-tenant directory.
engineenumv1 or v2. KV v2 is the default for new Vault deployments.
cache_ttl_secsintegerTTL in seconds on cached reads. Default is 300.
authobjectOne of token, approle, or kubernetes.
namespacestringOptional X-Vault-Namespace header for Vault Enterprise.

Auth Methods

Token auth uses an operator-supplied static token:

auth:
  type: token
  token: ${VAULT_TOKEN_ACME}

AppRole exchanges role_id and secret_id at backend construction. The backend refreshes the token on a 403 and retries the read once.

auth:
  type: approle
  role_id: acme-prod
  secret_id: ${VAULT_SECRET_ID_ACME}
  mount: approle

Kubernetes auth exchanges the pod's service-account JWT for a Vault token. Use it for in-cluster deployments where the pod has a Vault role bound to its service account.

auth:
  type: kubernetes
  role: sbproxy-acme
  jwt_path: /var/run/secrets/kubernetes.io/serviceaccount/token
  mount: kubernetes

Reference Shape

vault://primary/<sub-path>[?version=<n>][&key=<json-field>]

Sub-paths are interpreted under the configured mount. A relative reference such as secret/data/openai-prod is rewritten to the canonical KV v2 URL. References that already encode <mount>/data/... are taken verbatim. The backend rejects paths that escape the configured mount prefix.

AWS Secrets Manager

The AWS client speaks the official Secrets Manager API. The default credential chain works in EC2, ECS, EKS, Lambda, SSO, and web identity contexts. The operator can also supply static keys or an assumed IAM role for cross-account access.

Configuration

proxy:
  secrets:
    backends:
      - type: aws
        name: primary
        region: us-east-1
        mount_prefix: prod/sbproxy/tenants/acme-corp
        cache_ttl_secs: 300
        auth:
          type: default_chain
FieldTypeDescription
regionstringAWS region. Required.
mount_prefixstringPath prefix every read must stay inside. Tenant deployments scope this to a per-tenant directory.
cache_ttl_secsintegerTTL in seconds on cached reads. Default is 300.
authobjectOne of static_keys, default_chain, or assumed_role.

Auth Methods

Static keys are useful for development and CI. Production deployments should prefer the default chain or assumed role.

auth:
  type: static_keys
  access_key_id: ${AWS_ACCESS_KEY_ID}
  secret_access_key: ${AWS_SECRET_ACCESS_KEY}
  session_token: ${AWS_SESSION_TOKEN}

Default chain picks up env vars, EC2 instance profile, ECS task role, SSO, web identity, and other AWS-standard sources.

auth:
  type: default_chain

Assumed role exchanges the proxy's identity for a session in a different account.

auth:
  type: assumed_role
  role_arn: arn:aws:iam::222222222222:role/sbproxy-acme
  external_id: opt-in-string-from-trust-policy
  session_name: sbproxy

Reference Shape

awssm://primary/<secret-id>[?version=<n>][&key=<json-field>]

The path is a Secrets Manager secret id under the configured mount_prefix. A relative reference such as openai-prod lands at <mount_prefix>/openai-prod. References that already encode the prefix are taken verbatim. The backend rejects paths that escape it.

Binary secrets are returned base64-encoded so the resolved value is text across all backends.

GCP Secret Manager

The GCP backend reads Secret Manager through the AccessSecretVersion API. It supports Application Default Credentials, service-account key files or inline JSON, and external-account Workload Identity Federation files.

Configuration

proxy:
  secrets:
    backends:
      - type: gcp
        name: primary
        project_id: acme-prod
        cache_ttl_secs: 300
        auth: application_default
FieldTypeDescription
project_idstringDefault project for short references such as gcpsm://primary/openai-api-key. If omitted, the backend uses GOOGLE_CLOUD_PROJECT, GCLOUD_PROJECT, or the ADC project id.
endpointstringSecret Manager API endpoint. Defaults to https://secretmanager.googleapis.com.
cache_ttl_secsintegerTTL on cached reads. Default is 300 seconds.
authenum or objectapplication_default, service_account_key_file, service_account_key_json, or external_account_file.

Reference Shape

gcpsm://primary/<secret>[?version=<n>][&key=<json-field>]
gcpsm://primary/projects/<project>/secrets/<secret>[?version=<n>][&key=<json-field>]
gcpsm://primary/projects/<project>/secrets/<secret>/versions/<version>[&key=<json-field>]

The default version is latest. Secret payload bytes must decode as UTF-8. Use key=<json-field> when the payload is a JSON object and the config field needs one member.

Kubernetes Secrets

The Kubernetes backend reads Secret objects through the standard Kubernetes API. Each backend is bound to one namespace; cross-namespace reads are rejected at URL composition.

Configuration

proxy:
  secrets:
    backends:
      - type: k8s
        name: primary
        namespace: tenant-acme
        cache_ttl_secs: 300
        auth:
          type: in_cluster
FieldTypeDescription
namespacestringNamespace the backend reads from. Cross-namespace references are rejected.
cache_ttl_secsintegerTTL in seconds on cached reads. Default is 300.
authobjectOne of in_cluster or kubeconfig.

Auth Methods

In-cluster auth reads the pod's service-account token and Kubernetes API server address from the standard in-cluster files and env vars.

auth:
  type: in_cluster

Kubeconfig auth selects an explicit kubeconfig file for out-of-cluster operators.

auth:
  type: kubeconfig
  path: /home/operator/.kube/config
  context: acme-prod

Reference Shape

k8ssecret://primary/<secret>[/<key>]
k8ssecret://primary/<namespace>/<secret>[/<key>]

Valid shapes:

Reference pathBehaviour
<secret>Returns the whole secret as a JSON map of key to decoded value.
<secret>/<key>Returns a single field from the configured namespace.
<namespace>/<secret>[/<key>]Uses an explicit namespace. It must match the backend's configured namespace.

Both data and stringData fields are honoured. data keys are base64-decoded automatically. UTF-8 is required; binary fields surface as decode errors.

File And Static Map Backends

Use secretfile:// for a backend-configured YAML or JSON secret file. Use secret:// for a backend-configured static secret map. Keep file:/path/to/secret and secret:<name> for legacy configs that already use those forms.

Configure these backends under proxy.secrets.backends. Each has a name used in the reference. A local backend's entries values may be ${ENV} so real secrets stay in the environment rather than the config file. A reference in an AI provider api_key resolves against these at startup, and an unresolved reference stops the proxy from starting rather than being sent verbatim as a bearer token.

proxy:
  secrets:
    backends:
      - type: file
        name: local
        path: /etc/sbproxy/secrets.yaml
        format: yaml
      - type: local
        name: app
        entries:
          openai_key: "${OPENAI_KEY}"
secretfile://local/openai-prod?key=api_key
secret://app/openai_key

Scope

Backends are declared at proxy scope under proxy.secrets.backends, and every origin resolves references against that one set. A reference names the backend it wants, so you can point different origins at different physical stores by giving each store its own backend name:

proxy:
  secrets:
    backends:
      - type: hashicorp
        name: shared
        addr: https://vault.shared.example/v1
        mount: secret/tenants/shared
        auth:
          type: token
          token: ${VAULT_TOKEN}
      - type: hashicorp
        name: acme
        addr: https://vault.acme.example/v1
        mount: secret/tenants/acme-corp
        auth:
          type: token
          token: ${VAULT_TOKEN_ACME}

An origin that reads vault://acme/secret/data/openai-prod?key=api_key resolves through the acme backend; one that reads vault://shared/... uses the shared store.

Per-tenant and per-origin backend scopes (where the same reference name resolves to a different physical store depending on the request's tenant) are not wired yet. Give each store a distinct backend name at proxy scope for now.

Cache Semantics

Every backend caches successful reads for the configured TTL. A set on the same key invalidates the cache so a follow-up get sees the new value. There is no proactive watch-based invalidation today. A future watch hook can invalidate Kubernetes entries when Secret objects change.

  • docs/configuration.md for the proxy.secrets block and reference URI grammar.
  • docs/multi-tenant.md for the inheritance model and isolation guarantees.
  • docs/migration-credentials.md for the virtual_keys: to credentials: migration and the vault reference migration note.