ADR-0018: Credentials & secrets — a governed reference, a plaintext-blind kernel
ADR-0018: Credentials & secrets — a governed reference, a plaintext-blind kernel
Status
Accepted
Date
2026-07-03
Context
Every driver reaches outward through the egress gateway holding a credentialScope
(ADR-0005; LLD P16–P17). Canon names that field but decides
nothing behind it: where the secret lives, how it is stored and isolated, how a driver
authenticates to a foreign system (OAuth, API key, basic…), how a per-tenant credential is
selected at run time, how rotation and expiry work, and what replaces the legacy pattern.
The legacy pattern is the anti-spec: the SmartInventory study found plaintext credentials in
every Web.config/datashare.inc, base64-as-encryption, a master-password bypass, and
AllowInsecureHttp=true. Separately, the only secret mechanism in use today — dx secret — is
one keeper’s personal store, present on nobody else’s machine, and therefore not a team
mechanism at all. This ADR closes NETW-41.
Reality check. Secrets handling is greenfield in code: only P1/P2/P6/P23 are built; the egress gateway (P16), the driver contract (P17), and the Keys sub-port of P14 are designed, not implemented. This ADR fixes contracts, not running code.
Vocabulary. A Secret is the sensitive value itself. A Credential is a governed,
versioned reference to a Secret plus its auth-strategy metadata, bound to a driver capability
(the thing credentialScope names). AuthStrategy is the typed scheme by which a Credential
authenticates to a foreign system. Binding uses the structural hierarchy —
Tenant (the org / security boundary) ⊃ Universe (a reality container, e.g.
networkaccess-airtel) ⊃ Realm (a domain namespace) — and the physical Cell (the
shard/placement unit).
Decision
-
Secrets management is a kernel concern, split at the seam. The kernel owns the Credential reference and its injection — which driver may hold which credential, resolved per invoking context, audited at every egress crossing — and is blind to the plaintext. The Secret value lives only behind a storage adapter the kernel never reads from directly. This is “shape, not meaning” (the-seam §IV) applied to secrets: the kernel governs who may bind which credential to which capability without seeing the bytes. Pushing secrets fully to deployment infra is rejected — the no-ambient-authority invariant requires the kernel to be the injector.
-
One credential mechanism, both directions. “Inbound source credentials” (a driver reading an external system →
Asserts) and “outbound system credentials” (a driver actuating an external system → aCall) are the same primitive: a Credential bound to a driver’scredentialScope, presented at the P16 egress gateway on every outward crossing. The direction of data (EMIT vs ACTUATE) lives on the DriverManifest, never on the Credential. -
The reference is a write-only Resource; the value lives behind P14-Keys; P16 injects. The Credential’s identity and metadata register in P0 as a special Resource sub-kind (ADR-0006) — versioned, policy-marked, bitemporally audited — but flagged never-projected: it is never admitted as an
Assert, never enters World/Serving, never appears in a Query result or the generated SDK, and an Action’sbasedOncan never cite it (the same rule as delegated traits, LLD §28.2). The Secret value is guarded by the Keys sub-port of P14 (LLD §16). The P16 egress gateway pulls-and-injects at crossing time. One Credential, three ports; no new port invented. -
AuthStrategy is typed on the Credential and executed by P16 — drivers never hand-roll auth. The strategy is a closed, kernel-known set, v1:
oauth_client_credentials · oauth_ropc · api_key · basic · bearer_static. The egress gateway resolves the bound Credential, runs the strategy — including acquiring, caching, and refreshing OAuth tokens before expiry — and injects the header. Acquired tokens are ephemeral (cached by(credential, version), never persisted to World). Extending the set (mTLS client-cert, AWS SigV4) is a governed change to kernel vocabulary, held to the same falsifiable-test discipline as kinds and verbs — never a per-driver hack. Centralizing here is what prevents a re-run of the legacy per-endpoint auth debt. -
Binding is logical (Tenant → Universe); placement is physical (per-Cell). A Credential binds at Tenant by default and may bind narrower at Universe (e.g. prod vs staging), resolved most-specific-first; Realm (domain) is not a binding axis.
credentialScopeis a set typed by purpose (read-db vs actuate-api), selected by(target, purpose). The encrypted value and the per-Tenant KEK physically live in the Cell(s) hosting that Tenant/Universe; an air-gapped or dedicated tenant runs its own Cell and the secret never leaves its Cell. This is LLD §26.4’s isolation tiers expressed as Cell placement. Resolution is deterministic from context, never ambient; an unprovisioned credential fails closed at acquire time. -
Encryption, rotation, expiry. At rest: envelope encryption — value encrypted with a DEK, DEK wrapped by the per-Tenant KEK in the Cell key manager. The registry holds only the wrapped-ciphertext reference; plaintext is decrypted only inside the P16 gateway, in memory, at crossing time — never persisted, never logged. Rotation is a governed Act minting a new Credential version; the current pointer flips and old-version cached tokens are honored until their natural expiry (grace window), then retired. KEK rotation re-wraps DEKs without re-encrypting secrets. Rotation is capability-gated, author-blind, and audited (metadata only). Expiry is proactive and fail-closed:
rotation-due/expiryin P0 metadata fires a WorkItem ahead of time; a hard-expired credential makes the Call fail closed and flips the driver toSTALE. Auto-rotation is opt-in per Credential (only where the target supports programmatic rotation); a manual rotation Act is always available. -
One Keys port, adapters per profile; no personal stores. The Keys sub-port has one contract and per-profile adapters: gitignored
.env/file for local dev (dev Cells only; production secrets never in.envor git), OpenBao for Lean, cloud KMS / HSM for Scale and air-gap. Deployed secrets are injected at deploy time (P20/P21), never baked into images or configs.dx secretis forbidden in platform and product code — it is the keeper’s personal store, reserved for the JIRA/tracking scripts. The named team mechanism is the P14 Keys sub-port: OpenBao (Lean) / cloud KMS or HSM (Scale, air-gap). -
Auth failures surface, never fail silent. Auth failures are cataloged errors (ADR-0014) —
VINXI_CREDENTIAL_MISSING,VINXI_CREDENTIAL_EXPIRED,VINXI_CREDENTIAL_AUTH_REJECTED— recorded as CallLog facts; the driver emits a healthAssertand showsSTALE, and an ops WorkItem fires. Agents check a driver’s settledness before relying on it, the same discipline as reads. -
Legacy auth-debt is quarantined behind the driver boundary and never replicated. The driver adapter translates legacy auth into a proper
AuthStrategy+ Credential in the governed store; base64-as-encryption, master-password bypass, andAllowInsecureHttpnever cross into Vinxi. The SmartInventory Integration API’s ROPC becomes anoauth_ropcCredential; nothing else carries forward.
Consequences
- The credential model reuses three existing ports (P0 reference, P14-Keys value, P16 injection) and adds no new port. It feeds NETW-18 (Integration — the driver/connector boundary) and NETW-19 (Multi-tenancy & Security — the Tenant/Universe/Cell isolation model).
AuthStrategyis public kernel vocabulary; adding a scheme is a deliberate, versioned change.- A per-tenant credential to one foreign system serves every tenant from one driver definition — the legacy 1,626-branch fork becomes per-tenant data, not code.
- Naming dependency (flagged, not fixed here). This ADR uses Tenant KEK, where LLD §16 and
§26.4 currently say “realm KEK” and use “Realm” for the per-city/per-environment container that
the glossary calls a Universe. That Universe↔Realm collapse (it reaches the
EntityRefshape) is a substantive canon fix owed its own ADR; this ADR is written against the glossary’s correct hierarchy and should be reconciled when that fix lands. - Revisit triggers: the first non-OAuth/non-key strategy a real target demands (mTLS, SigV4); the first tenant requiring hardware-rooted keys (HSM) in a shared Cell; the first programmatic rotation integration.
Alternatives Considered
- Secrets as pure deployment infra (opaque
credentialScopestring, ops-managed resolution). Rejected: reintroduces ambient authority and removes the kernel’s ability to audit and isolate credential binding per tenant. - Credential as an ordinary P0 Resource with no special casing. Rejected: a Resource’s data
can flow into reality; a Secret must never be projected, fused, cited by
basedOn, or SDK-exposed. The write-only sub-kind carries that hard invariant. - Per-driver auth code. Rejected: it is precisely how the legacy debt was created; strategy on the Credential + execution at P16 gives one correct implementation per scheme.
- A separate inbound “connection credential store” distinct from outbound creds (the SMP shape). Rejected: an operational artifact, not an architectural distinction — one mechanism covers both crossing directions.