Skip to content

Dynamic Configuration & Secrets

How Appcircle services get their configuration and secrets: Appcircle.ConfigService (repo ac-service-config), the Appcircle.Core.Config client library every service uses to talk to it, how a caller authenticates on each deployment target (Docker Compose, Helm), and ac-config-agent for third-party components that cannot call Config Service themselves. Grew out of the OpenBao migration TDD — see TDD-0004 and ADR-0007.

For the envdef.yaml file format a service author writes, see Environment Configuration (envdef). For the AC_ naming rules, see Environment Variables.

Config Service never lets a caller hold a raw OpenBao/Vault token. Every caller presents its own credential; Config Service exchanges it for a scoped store token server-side, uses it, and throws it away — nothing is ever returned to the caller.

Client Library — Appcircle.Core.Config

Two clients, for the two moments a service needs config:

Client When Used for
ConfigServiceClient Once, at startup POST /v1/config/resolve — the whole envdef.yaml at once
RuntimeSecretClient Any time after startup GET/PUT/DELETE /v1/secrets/{path} — one value at a time

Startup — resolve the whole envdef.yaml:

var options = new ConfigServiceOptions
{
    ServiceUrl = BuildResolveUrl(Environment.GetEnvironmentVariable("AC_CONFIG_INTERNAL_URL")),
    Service = EnvDefLoader.LoadService("envdef.yaml"),   // reads the file's own `service:` field
    DefPath = "envdef.yaml",
    CredentialSource = Environment.GetEnvironmentVariable("AC_CONFIG_CREDENTIAL_URI")
};
options.Validate();
builder.Configuration.AddConfigService(opts => { /* copy the fields above */ });

EnvDefLoader.LoadService reads the identity out of the file instead of a second, independently-set Service value — the two drifting apart would silently point the auth exchange at the wrong identity.

Any time after — read or write one secret:

using var client = new RuntimeSecretClient(new RuntimeSecretClientOptions
{
    ServiceUrl = "http://ac-config",
    Service = "task-service",
    CredentialSource = Environment.GetEnvironmentVariable("AC_CONFIG_CREDENTIAL_URI")
});

var value = await client.ReadAsync("task-service/some_key", cancellationToken);   // null if absent
await client.WriteAsync("task-service/some_key", "new-value", cancellationToken); // throws if refused

Both clients take the same CredentialSource — see How a Caller Authenticates below.

Bootstrap variable Meaning
AC_CONFIG_INTERNAL_URL Base URL of Config Service, e.g. http://ac-config
AC_CONFIG_CREDENTIAL_URI Where to read the caller's own credential from (an AppRole secret_id or a ServiceAccount JWT, depending on deployment target) — see How a Caller Authenticates
AC_CONFIG_REPORT_ENABLED Off by default. Prints every resolved key at startup; secret-sourced keys print name only, never the value

Startup ordering — retrying while Config Service isn't up yet

Kubernetes/Compose restart a crashed container until Config Service answers, so production never needs this. A bare dotnet run has no such restart loop — set opts.StartupRetryEnabled = true (plus StartupRetryAttempts/StartupRetryDelay, both defaulted) on ConfigServiceOptions for local dev only. Off by default everywhere; retries any resolve failure since a non-transient one just fails identically on every attempt.

How a Caller Authenticates

Every call — resolve, runtime read, runtime write — starts the same way: the caller presents its own credential, Config Service exchanges it for a scoped store token, uses that token, then drops it. The caller never sees that store token.

The credential itself is one of two concrete things, never anything else — which one depends on the deployment target:

  • Docker Compose → an AppRole secret_id
  • Helm/Kubernetes → the pod's own ServiceAccount JWT

One install-wide setting, AC_CALLER_AUTH_KIND (approle or kubernetes, default kubernetes), tells Config Service which of the two to expect from every caller on this install — it is not something a caller declares about itself. The client library never branches on this either: it just sends whatever CredentialSource resolves to (a secret_id string, or a JWT string) as a Bearer token, without knowing or caring which one it is.

The caller also sends its own claimed identity as a service query parameter, alongside that credential — this is not derived from the secret path being requested. That claim is only ever provisional: Config Service passes it straight to OpenBao/Vault as the AppRole role_id (or Kubernetes role) in the exchange step below, and the login only succeeds if the presented secret_id/JWT actually matches what that role is registered for. Whatever identity OpenBao/Vault reports back (never the caller's raw claim) is what every later authorization check uses.

caller                          Config Service                    OpenBao/Vault
  |--- Bearer <secret_id|JWT> ->|                                        |
  |    (+ ?service=<claimed>)   |--- exchange (role_id/role + credential)|
  |                             |--------------------------------------->|
  |                             |<---------- scoped token ---------------|
  |                             |--- read/write with scoped token ------>|
  |<-------- value/result ------|                                        |

Docker Compose (AppRole)

The credential is an AppRole secret_id; service names the AppRole role_id.

Onboarding a service — fully automated: add its name to appcircle-docker-compose/service-config/services.list and run ./stack.sh up. The active store then has, for that service, a read-only policy (service-<name>-read, scoped to secret/data/<name>/*), an AppRole role, and a credential file under ./credentials/<name>/. Idempotent — rerunning up just re-declares the same policy/role/credential.

AC_CONFIG_CREDENTIAL_URI is one of:

  • File — a service running as a container on the compose network: bind-mount ./credentials/<name> and point at /credentials/<name>/secret_id.
  • HTTP — a service that can't rely on a shared bind-mounted path (a developer's host machine, started from an IDE): http://localhost:8090/credentials/<name> against the always-up credential-server.py, which reads ./credentials/<name>/ and returns {"role_id": ..., "secret_id": ...}, unauthenticated (local-dev only — no TTLs, no rotation, no access control beyond the read policy itself).

Helm / Kubernetes (ServiceAccount)

The credential is the pod's own projected ServiceAccount JWT; service names the Vault/OpenBao role it should be bound to.

AC_CONFIG_CREDENTIAL_URI is the mounted token file path (e.g. /var/run/secrets/kubernetes.io/serviceaccount/token) — no HTTP-source case exists here, since every pod already has this file without any extra setup.

Not yet automated

Compose's services.list + ./stack.sh up has no Helm equivalent yet: nothing automatically creates the OpenBao/Vault auth/kubernetes role + read policy for a new service the way provision.sh does for Compose. Until that exists, onboarding a service under Helm means creating that role/policy by hand. Tracked as a gap, not a design choice.

Context Definition

${secret:...} isn't the only thing a service's envdef.yaml can reference — ${context:root_external} pulls from a second, much simpler source: the deployment context, a small set of per-install inputs (a domain, a scheme, a namespace) and a larger set of values derived from them (URLs, realms, image tags), shipped as contextdef.yaml and overridable via AC_CONTEXT_DEFINITION_PATH/AC_CONTEXT__<NAME>/AC_CONTEXT_TEMPLATE__<NAME>. Resolved in the same /resolve call as secrets, just from a different, simpler source. See Deployment Context & Service Naming for the full mechanism (input validation, Scriban templates, precedence, determinism guarantees).

Central Secret Sourcing Policy

A service's envdef.yaml only ever says that it needs ${secret:svc/key}. Where that value comes from, whether it may be generated, and whether a service may write it at runtime — all of that is decided centrally, by one policy document, not per service.

Supplied as JSON, inline (AC_SOURCING_POLICY) or as a file (AC_SOURCING_POLICY_PATH, mutually exclusive with the inline form). Read once at startup, immutable for the process lifetime.

{
  "unknownKey": "deny",
  "mounts": {
    "legacy-mount": { "kvVersion": 1 }
  },
  "rules": [
    {
      "path": "task-service/redis_password",
      "provider": "internal",
      "generatable": true,
      "generator": "password(32)"
    },
    {
      "path": "task-service/certs/root",
      "provider": "internal",
      "writable": true
    },
    {
      "path": "task-service/legacy_key",
      "provider": "internal",
      "writable": true,
      "deletable": true,
      "mount": "legacy-mount"
    },
    {
      "path": "shared/redis_password",
      "provider": "internal",
      "generatable": true,
      "generator": "password(32)",
      "consumers": ["build-service", "publish-service"]
    },
    {
      "path": "task-service/jwt_rsa_private_key",
      "provider": "internal",
      "generatable": true,
      "generator": "rsa(2048)",
      "publicKeyPath": "task-service/jwt_rsa_public_key"
    },
    {
      "path": "task-service/jwt_rsa_public_key",
      "provider": "internal",
      "consumers": ["api-gateway"]
    }
  ]
}

For a worked example using this install's actual services and its two real legacy mounts (local, store), see Deployment Context & Service Naming.

Field Meaning
unknownKey deny (default, fail-closed — every path must be declared) or allow-internal (read-only fallback, never generated).
mounts Every non-default secrets-engine mount this policy's rules reference, keyed by mount name, each declaring its own kvVersion. See KV v1 vs v2 below.
path Exact (store-api/db_connection) or a pattern: trailing /* (mongo/*), leading */ (*/jwt_signing_key). Most specific match wins.
provider internal (this install's own store — may generate/write into) or external (customer-managed — read-only, see External Store).
generatable May Provisioning create this value once, if absent? Must be false when provider is external, and can never resolve to a kvVersion: 1 mount.
writable May Runtime write this value, any time? Must be false when provider is external. Separate from generatable — see why below.
deletable May Runtime delete this value, any time? Must be false when provider is external. Separate from writable, too — see why below.
generator Required when generatable is truepassword(n), hex(n), base64(n), uuid (alias: guid — same generator, accepted since legacy Keycloak client secrets in this codebase were literally .NET Guids), or rsa(n) (n one of 2048/3072/4096 bits).
publicKeyPath Required when, and only meaningful when, generator is rsa(n): the path the generated public key is written to alongside this rule's own (private-key) path. That path needs its own rule too — usually generatable: false, with whichever services need to verify this service's JWTs listed in consumers — since a rule only lets you generate at its own path, not read a different one. See Provisioning.
consumers Other services allowed to read (and, if writable, write) this path on top of its owning namespace — for a shared credential that lives outside any one service's own namespace.
mount The secrets-engine mount this path lives under, e.g. local, store. Absent (default) means this install's own default mount, always kvVersion: 2. A non-empty value must be declared in the policy's own mounts section above — see KV v1 vs v2.

Note there is no kvVersion field on a rule. A rule only says which mount it lives under (mount); that mount's own declaration in the mounts section is the only place kvVersion is ever written down. This is deliberate, not an oversight — see below.

Authorization, everywhere: a service may always touch its own namespace (the path's first segment); anywhere else, it must be named in that rule's consumers, or the call is rejected.

Which credential actually performs each operation

generatable/writable/deletable only say whether an operation is allowed — not which credential carries it out. That mapping is fixed by the system, the same for every rule, and is not (and must not be) a policy field: making it configurable per rule would let a rule opt a generatable path into using the caller's own credential, which is exactly the privilege escalation this design exists to prevent.

Rule field Operation Credential that performs it
generatable Create the value once, if absent — lazily during a resolve, or proactively during a bootstrap sweep Config Service's own privileged AC_PROVISIONING_TOKEN — see Provisioning
writable Set the value, any time, via the runtime proxy The caller's own scoped credential — see Runtime
deletable Delete the value, any time, via the runtime proxy The caller's own scoped credential — see Runtime
(read, always) Read the value, at resolve or via the runtime proxy The caller's own scoped credential

Reading this table the other way: if you see AC_PROVISIONING_TOKEN involved at all, you are looking at generation, never a runtime write/delete — those two never touch a privileged credential, by construction.

KV v1 vs v2 — legacy paths

OpenBao/Vault's KV secrets engine comes in two incompatible on-the-wire shapes — KV v2 nests reads under data.data and wraps writes under a "data" key (plus, on the provisioning-only compare-and-set path, an "options": {"cas": ...} sibling); KV v1 is flat, no nesting, no CAS. (The v1/ segment in every OpenBao/Vault HTTP URL is the HTTP API version — unrelated to this; it's present on both KV v1 and KV v2 paths.)

This matters here because it's not a single install-wide choice: the secrets this system provisions and resolves are all KV v2, but the handful of services still being migrated off the old shared-token ISafeStore<T> pattern (Appcircle.AspNetCore) already have real secrets sitting at KV v1 paths in the store — moving them to the runtime proxy must not also force a store-side migration to v2. A single service can genuinely have both kinds of path at once.

KV version is a property of the mount, not the path. OpenBao/Vault fixes a secrets engine's version when the mount is enabled (vault secrets enable -version=1|2 ...) — one mount cannot serve some paths as v1 and others as v2. Concretely, in this codebase, the legacy ISafeStore<T> services (Appcircle.AspNetCore) talk to two such KV v1 mounts, both entirely distinct from this install's own default mount (AC_OPENBAO_KV_MOUNT/AC_VAULT_KV_MOUNT, which is v2) — see Deployment Context & Service Naming for exactly which real service maps to which mount and legacy path prefix.

Because of that, kvVersion is declared once per mount, in the policy's mounts section — never per rule, and never guessed from the path string (a wildcard match on .v1 or similar would be one typo away from silently reading/writing the wrong shape). A rule only says which mount it lives under; that mount's own entry in mounts is the single place its version is written down. Two rules referencing the same mount can never disagree about its version, because there is no per-rule field left for them to disagree on — the version belongs to the mount, declared exactly once. (An earlier iteration of this design put kvVersion directly on the rule alongside mount; that let two rules for the same mount silently declare different versions, an error that surfaced only against a real store, not while parsing the policy. The mounts registry closes that gap structurally.)

A rule's mount being absent means this install's own default mount — always kvVersion: 2, since provisioning's compare-and-set create needs v2 and this install's own default mount is exactly where provisioning writes. A non-empty mount must appear as a key in mounts, or policy parsing fails outright — a typo'd or forgotten mount name is a parse-time error, not a request that silently goes to the wrong shape and 404s against a real store. generatable paths can never resolve to a kvVersion: 1 mount, enforced the same way: compare-and-set has no v1 equivalent, so a v1 mount is only ever for a secret this system did not create.

Provisioning

Config Service's own action — creating a value nobody asked for right now, because a policy rule declared it must exist. Always uses the privileged AC_PROVISIONING_TOKEN, never a caller's credential.

The mechanics (authoritative read → compare-and-set write → journal) are identical no matter what triggers them — only what triggers them differs, and that's the entire reason AC_PROVISIONING_MODE has two non-off values instead of one:

AC_PROVISIONING_MODE Trigger Recommended?
off (default) Nothing — no privileged credential exists in this process at all.
resolve Lazily, as a side effect of some caller's own /resolve hitting an absent path. No — kept for the current mental model, but means the privileged token is live in a process that also serves HTTP traffic, indefinitely.
bootstrap Proactively: walks the entire sourcing policy once, provisions every missing exact-path generatable rule, logs a summary, and exits without ever starting Kestrel. Yes — run as a separate, short-lived job (a Compose one-shot container, a Kubernetes Job) whenever the policy changes.
path is absent, rule.generatable = true                 (resolve mode: found via some caller's own /resolve)
  |                                                      (bootstrap mode: found by walking the whole policy)
  v
does path really not exist?  --------->  privileged AC_PROVISIONING_TOKEN reads authoritatively
  |
  v (confirmed absent)
create it (compare-and-set) ----------->  same privileged token writes; loses the race gracefully
  |                                        if another instance created it first
  v
journal the creation ------------------>  same privileged token, shared `_state/provision/...` prefix

(resolve mode additionally re-reads with the caller's own credential afterward, to hand the value back in that resolve response — provisioning creates the value, it does not by itself grant anyone sight of it. bootstrap mode has no caller to hand anything back to; it just provisions and exits.)

Variable Meaning
AC_PROVISIONING_MODE off (default), resolve, or bootstrap — see table above.
AC_PROVISIONING_TOKEN Privileged store credential, required whenever mode is not off, used only here — never the caller's own.

Idempotent, once-only: re-running does nothing once the value exists — no forced-regenerate flag, so an accidental rotation can never break a running deployment. This is what makes bootstrap mode safe to run as often as you like (every time the policy changes, or just on a schedule) — each run only ever creates what's genuinely still missing.

A pattern rule can't be swept ahead of time

A pattern rule (*/jwt_signing_key, mongo/*) has no single concrete KV path to provision — the concrete path only becomes known once a specific caller's own resolve references it. The bootstrap sweep skips these (logging how many) and moves on; they still generate lazily, exactly as before, on that caller's own first resolve — but only on an install that also runs a resolve-mode process somewhere, since bootstrap-mode processes never serve /resolve at all.

The serving replica should run with AC_PROVISIONING_MODE=off, always

Keep the long-running, HTTP-serving Config Service replica on off (the default — just don't set the variable). Only a separate, short-lived bootstrap run should ever hold AC_PROVISIONING_TOKEN. This is why bootstrap mode exists: before it, provisioning a newly-added policy rule meant restarting the serving replica with provisioning turned on, then restarting it again to turn it back off — two restarts, in the right order, just to add one secret. A separate job removes that dance entirely from the serving replica's lifecycle.

AC_PROVISIONING_TOKEN needs read and write — never write-only

Compare-and-set provisioning has to authoritatively check absence before creating (the "reads authoritatively" step above), so this credential's OpenBao/Vault ACL needs both read and create/update — "write-only" cannot support that check. The actual least-privilege lever isn't narrowing the capability list, it's narrowing the paths: scope this token's ACL to only the paths the sourcing policy marks generatable (plus the _state/provision/* journal prefix), not the whole store. Separately, Config Service's own code never surfaces an existing value read this way — it only ever inspects whether the read found something, never what it found — so even though the token can technically read a value, nothing in this path ever logs or returns one.

An rsa(n) rule provisions two paths, not one

Every other generator produces one string for one path. rsa(n) produces a PKCS#8 private key (-----BEGIN PRIVATE KEY-----) and an X.509 SubjectPublicKeyInfo public key (-----BEGIN PUBLIC KEY-----) together, and the private key's own rule declares where the public half goes via publicKeyPath. SecretProvisioner writes the private key first, then the public key right after, both with the same compare-and-set safety as every other path. If a prior run crashed between the two writes, the next pass (a resolve call or a bootstrap sweep) finds the private key already present, does not regenerate it, and instead re-derives the public key from that existing private key and writes only the missing half — self-healing without ever producing a public key that doesn't match its private key.

Runtime

The caller's own action, at a moment of its own choosing — not Config Service creating something on its behalf. GET/PUT/DELETE /v1/secrets/{path}?service=<name> (PL-120).

signing-identity-server needs to persist a certificate it just renewed
  |
  v
PUT /v1/secrets/signing-identity-server/certs/root  { "value": "<cert>" }
  |
  v
rule.writable = true?  --- no --->  refused (403)
  | yes
  v
AC_RUNTIME_WRITE_ENABLED?  --- no --->  refused (403)
  | yes
  v
broker signing-identity-server's OWN credential into a scoped token
  |
  v
write the value with THAT token  ----> store's own ACL for this caller decides: allowed, or 403
  |
  v
log the write (origin, path, service, mount, kvVersion) — a log line, not a store write

Read works the same way minus the two gates — it always uses the caller's own credential, exactly like resolve. Delete (DELETE /v1/secrets/{path}) follows the identical shape as write — same credential, same store-ACL backstop, same journal log line — with its own gate pair instead: rule.Deletable and AC_RUNTIME_DELETE_ENABLED. It's a soft delete (KV v2's standard behavior) — the store may still recover a prior version through its own versioning; nothing here destroys history outright.

Why write and delete each need their own field, separate from generatable and from each other: provisioning is Config Service acting with its own elevated authority to satisfy a declared invariant; runtime write/delete are entirely the caller's own action on its own data. Using the caller's own credential means the store's own ACL is a second, independent check — a bug in Config Service's authorization code has no effect if the store itself refuses the operation. A single shared privileged credential (the old design) would have no such backstop: any authorization bug could mutate any service's namespace. And a path being writable never implies it's also deletable — deleting is a materially different risk (a wrong value can be overwritten again; a deleted value may not come back), so an operator can allow updates while keeping delete off entirely.

Variable Meaning
AC_RUNTIME_WRITE_ENABLED Off by default. Install-wide kill switch for write — does not grant capability, only permits attempting it.
AC_RUNTIME_DELETE_ENABLED Off by default. Same shape, independently revocable from write.

Both gates plus the store's own ACL — all three, every time

rule.Writable/rule.Deletable and the matching AC_RUNTIME_*_ENABLED flag being satisfied does not mean the operation succeeds — the caller's own AppRole/ServiceAccount policy still has to grant create/update/delete on its own namespace. This part isn't automated yet: today's Compose policy template (policy.hcl.tmpl) only grants read/list. A service with a writable/deletable path fails with the store's own 403 until its policy is extended — by design, not a bug: turning on runtime write/delete never widens what a caller's credential can already do.

Read, write, and delete all follow the rule's own mount, and that mount's own kvVersion

A writable/deletable rule pointed at a kvVersion: 1 mount (see KV v1 vs v2 above) gets full read/write/delete through this same proxy — it's the intended path for migrating a service off ISafeStore<T> onto the runtime proxy without also having to migrate its existing secret off KV v1 first. Both the value read and the value write/delete target that rule's own mount — never this install's own default mount. The journal (below) is a log line, not a store write, so it has no mount of its own.

The journal is a log line, not a store write

Runtime write/delete log a line (origin, path, caller service, mount, kvVersion) instead of writing an entry into the store, the way an earlier iteration of this design did (at <owner>/_journal/<rest-of-path>, with the caller's own credential). Two reasons: this is a busy, caller-driven path, so an extra store round-trip on every single write/delete is real cost; and for a path whose mount predates this proxy (a legacy v1 mount), that mount's ACL was never written with a _journal sub-path in mind, so the store write would fail on every call for exactly those paths — the ones this feature exists to support. Provisioning's journal is unaffected — it's a real store write, under a purpose-provisioned privileged credential's own ACL, once per secret rather than on every call.

External / Customer-Managed Secret Store

An install has one bundled internal store (may generate/write into) and, optionally, one customer-managed external store (read-only, always — "never write into a store we don't own"). A rule's provider field decides which; never inferred at runtime.

Variable Meaning
AC_SECRET_PROVIDER openbao (default) or vault — backs the internal store
AC_EXTERNAL_SECRET_PROVIDER vault — enables the external store for provider: external rules. Absent by default.
OpenBao variable Vault variable Meaning
AC_OPENBAO_ENDPOINT AC_VAULT_ENDPOINT Base address. Required.
AC_OPENBAO_KV_MOUNT AC_VAULT_KV_MOUNT Default KV v2 mount, used whenever a rule declares no mount of its own. Default secret. A rule with kvVersion: 1 overrides this via its own mount field — see KV v1 vs v2.
AC_OPENBAO_SECRET_FIELD AC_VAULT_SECRET_FIELD Field within the KV entry. Default value.
AC_OPENBAO_KUBERNETES_AUTH_PATH AC_VAULT_KUBERNETES_AUTH_PATH Login path for a ServiceAccount JWT.
AC_OPENBAO_APPROLE_AUTH_PATH AC_VAULT_APPROLE_AUTH_PATH Login path for an AppRole credential.
AC_OPENBAO_TIMEOUT_IN_SECONDS AC_VAULT_TIMEOUT_IN_SECONDS Per-request timeout.

Different from the infra OpenBao Agent

OpenBao Agent is a systemd-level agent for VM/infra secrets (secret/vm/...) — a disjoint namespace, unrelated to any of the above.

Config Agent (ac-config-agent)

Redis, Keycloak, MongoDB, Postgres, and other third-party containers cannot call Config Service themselves. ac-config-agent is a generic init container that resolves on their behalf and writes the result to a .env file on a shared volume — the main container just reads that file at its own startup.

First release scope: init-container mode only, .env output only.

Variable Meaning
AC_CONFIG_INTERNAL_URL / AC_CONFIG_CREDENTIAL_URI Same as the client library above
AC_CONFIG_DEF_PATH The target service's envdef.yaml, mounted read-only alongside the agent
AC_CONFIG_OUTPUT_PATH Where to write the resolved .env file

Exits non-zero on any failure — a blocked init container beats a main container starting with no config. Not the same thing as the infra OpenBao Agent above: this one calls Config Service's /resolve, never OpenBao/Vault directly, and knows nothing about the sourcing policy.

Config Service's Own Environment Variables

Every variable Config Service itself reads, in one place — each is explained in context above; this is the quick-reference version. (This is Config Service's own configuration surface, not a caller's — for what a consuming service/agent configures, see Client Library and Config Agent above.)

Variable Category Default Meaning
AC_ENVIRONMENT Host required Development/Staging/Production/... — selects the NLog config and IWebHostEnvironment.
AC_PORT Host required (serving mode only) Kestrel listen port. Never read at all in bootstrap provisioning mode — see Provisioning.
AC_CALLER_AUTH_KIND Caller auth kubernetes approle or kubernetes — which credential kind every caller on this install presents. See How a Caller Authenticates.
AC_CONTEXT_DEFINITION_PATH Deployment context (image default) Path to the context definition file this install derives its ${context:...} values from. See Context Definition.
AC_CONTEXT__<NAME> Deployment context Sets one context value literally, overriding anything derived. See Context Definition.
AC_CONTEXT_TEMPLATE__<NAME> Deployment context Adds or replaces one derived context template. See Context Definition.
AC_SOURCING_POLICY Sourcing policy unset Inline JSON policy document. Mutually exclusive with the file form below.
AC_SOURCING_POLICY_PATH Sourcing policy unset Path to the policy JSON file — the usual form. See Central Secret Sourcing Policy.
AC_PROVISIONING_MODE Provisioning off off/resolve/bootstrap. See Provisioning.
AC_PROVISIONING_TOKEN Provisioning unset Privileged store credential. Required whenever AC_PROVISIONING_MODE is not off.
AC_PROVISIONING_JOURNAL_PREFIX Provisioning _state/provision KV prefix the provisioning journal is written under.
AC_STARTUP_PROBE_ATTEMPTS Provisioning 10 Store-reachability probe attempts before this process gives up and exits at startup.
AC_STARTUP_PROBE_DELAY_IN_SECONDS Provisioning 3 Delay between those probe attempts.
AC_RUNTIME_WRITE_ENABLED Runtime proxy false Install-wide kill switch for PUT /v1/secrets/{path}. See Runtime.
AC_RUNTIME_DELETE_ENABLED Runtime proxy false Install-wide kill switch for DELETE /v1/secrets/{path}. Independently revocable from write.
AC_SECRET_PROVIDER Store selection openbao openbao or vault — which provider backs the internal store.
AC_EXTERNAL_SECRET_PROVIDER Store selection unset vault — enables the external store for provider: external rules.
AC_OPENBAO_ENDPOINT OpenBao provider required if selected Base address, e.g. http://openbao:8200.
AC_OPENBAO_KV_MOUNT OpenBao provider secret Default KV v2 mount; a rule's own mount field overrides this (required for kvVersion: 1).
AC_OPENBAO_SECRET_FIELD OpenBao provider value Field within the KV entry.
AC_OPENBAO_KUBERNETES_AUTH_PATH OpenBao provider auth/kubernetes/login Login path for a ServiceAccount JWT.
AC_OPENBAO_APPROLE_AUTH_PATH OpenBao provider auth/approle/login Login path for an AppRole credential.
AC_OPENBAO_TIMEOUT_IN_SECONDS OpenBao provider 10 Per-request timeout against OpenBao.
AC_VAULT_ENDPOINT Vault provider required if selected Same shape as the AC_OPENBAO_* row above, for HashiCorp Vault.
AC_VAULT_KV_MOUNT / AC_VAULT_SECRET_FIELD / AC_VAULT_KUBERNETES_AUTH_PATH / AC_VAULT_APPROLE_AUTH_PATH / AC_VAULT_TIMEOUT_IN_SECONDS Vault provider (same defaults as OpenBao) Mirrors every AC_OPENBAO_* variable above — see External / Customer-Managed Secret Store.