Skip to content
Draft
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ Keep a Changelog, and releases use semantic versioning.

### Added

- A protected root-bootstrap credential transport, rejecting plaintext vault
roots in the ordinary config DB, unsafe file objects and dotenv fallback;
42 focused regression cases cover the compatibility boundary.
- A scoped CWL Key Vault migration baseline, preserving the historical snapshot
and separating proposed workload-resolution/release gates from implemented
bootstrap repair.
- An opt-in namespaced Keyvault foundation with encrypted-at-rest values,
atomic secret-change audit records, metadata-only administrator APIs, and a
fail-closed boundary for future workload-scoped reads.
Expand Down Expand Up @@ -59,6 +65,9 @@ Keep a Changelog, and releases use semantic versioning.

### Changed

- Keyvault bootstrap accepts a supervisor-owned file locator instead of a
plaintext passphrase entry. ADR-0014 now remains Proposed until independently
accepted, and operations document controlled migration and residual risks.
- Federation PUT and apply now report `applied_to_keycloak: true` only after a
fresh live Keycloak identity-provider observation matches the desired
observable representation. Keycloak's fixed mask for the known
Expand Down Expand Up @@ -115,6 +124,8 @@ Keep a Changelog, and releases use semantic versioning.

### Fixed

- Excluded client, operator, registration and vault-root credentials from the
typed configuration's repr; arbitrary settings serialization remains unsafe.
- Prevented relying-party inventory from silently accepting a KV key/body
identity mismatch, rejected unsafe live or `Location`-derived client UUIDs,
and aligned exact client discovery with Keycloak's documented
Expand Down
332 changes: 182 additions & 150 deletions docs/adr/0014-keyverse-keyvault-bounded-context.md

Large diffs are not rendered by default.

66 changes: 66 additions & 0 deletions docs/doctoring/keyvault_protected_bootstrap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Keyverse protected bootstrap: source and control evidence

Date: 2026-09-09. Status: focused legacy-adapter repair; no release claim.

## Primary-source interpretation

OWASP's secrets-management guidance supports centralized policy, least
privilege, lifecycle/rotation, auditing and explicit bootstrap choices. Avoiding
`.env` by moving the root credential into plaintext configuration does not meet
those goals. The child separates the root locator from its protected transport;
it does not claim that a file provides hardware key custody.

GitHub's OIDC reference documents workload token claims and reusable-workflow
identity. The future Keyverse verifier must cryptographically validate them and
bind them to approved repository, environment and immutable workflow identity.
This bootstrap child does not implement JWT validation or change Actions tokens.

Keycloak documents vault providers for supported Keycloak credential settings.
That integration is a downstream adapter concern, not evidence of a complete
CWL secret-management service.

## Exact evidence

Parent: `0f10ac556a318c3c3f5ce7eab0802573ecce0c4c` in canonical PR #129.
Implementation: `4caafd0fa56b9ca377c93d78299bfe82dbec8faf`.

Original bootstrap/config/kv_store blobs were reconstructed exactly and verified
with Git blob hashes. Tests first observed absent protected-reader behavior,
plaintext config acceptance and credential-bearing repr. Additional regressions
caught read-time mutation and false-positive atime detection before repair.

Reproduction from the full repository:

```sh
cd services/account_unification
uv sync --locked --extra dev
uv run pytest -q tests/test_keyvault_bootstrap_credentials.py
uv run ruff check app tests tools
uv run interrogate .
uv run coverage run --branch --source=app -m pytest -q
uv run coverage report --show-missing --fail-under=100
```

Only the first focused test command's equivalent was executed in the
reconstructed local environment: 42 passed. Python compilation and diff
whitespace checks passed. Changed executable statements are 61/61 covered;
no missing branch arcs originate on changed executable lines. Full repository
coverage, Ruff, locked installation, independent review and hosted gates must
still execute; historical parent results are not reused for this child.

## Residual risks and nonclaims

The Python adapter cannot guarantee secret zeroization. Host/root access can
read mounted credentials. Other legacy configuration credentials are not yet
migrated. Namespace/key context binding, immutable secret versions, workload
reads, revocation, no-store metadata, managed identity/KMS/HSM, durable rewrap and
organization-wide rollout remain in the gap register. Deleting a SQLite row
cannot prove erasure from WAL, pages or backups.

## References — APA 7th

OWASP Foundation. (n.d.). *Secrets management cheat sheet*. https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html

GitHub. (n.d.). *OpenID Connect reference*. https://docs.github.com/en/actions/reference/security/oidc

Keycloak. (n.d.). *Using a vault*. https://www.keycloak.org/server/vault
114 changes: 114 additions & 0 deletions docs/keyvault_credential_gap_baseline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Keyverse product and technical gap baseline

**Scope:** CWL credential-authority migration, 2026-09-09.
**Status:** active-PR implementation and observed gaps; not release acceptance.
**Canonical owner:** `ContextualWisdomLab/keyverse`.
**Organization migration:** [central #2063](https://github.com/ContextualWisdomLab/.github/issues/2063).

The previous complete 2026-08-21 snapshot is preserved byte-for-byte in
[the historical baseline](product-technical-gap-baseline-2026-08-21.md).
Its PR counts, approvals and Checks are historical evidence, not current gates.
This scoped update does not pretend to have re-audited every identity capability
or every CWL repository.

## Current evidence and decision

Canonical vault PR [#129](https://github.com/ContextualWisdomLab/keyverse/pull/129)
was read at `0f10ac556a318c3c3f5ce7eab0802573ecce0c4c`. It has an encrypted
namespaced store and metadata/write administrator APIs. It does not have a
released, signed-workload credential-resolution API. Its authorization sibling
[#103](https://github.com/ContextualWisdomLab/keyverse/pull/103) remains a separate
owner line; no duplicate authorization implementation is introduced here.

The bounded child implementation at
`4caafd0fa56b9ca377c93d78299bfe82dbec8faf` removes plaintext vault-root loading
from the ordinary config DB, reads a protected supervisor credential, and hides
credential fields from configuration repr. It preserves #129's entire delta.
No consumer is switched to this unreleased branch and no deployed secret is
read, migrated, deleted or rotated by this change.

Keyverse is to be the CWL secret-lifecycle authority, not merely a replacement
name for a plaintext key/value table. Non-secret configuration remains typed
configuration. Identity, service authorization and secret custody remain
separate bounded contexts within the owner product.

## Gap register

| Gap | Evidence / risk | Status and acceptance |
|---|---|---|
| Vault root stored as plaintext configuration | Parent `config.py` reads `keyvault_passphrase` from `idp_config_entries` | Child implementation rejects that entry, accepts only a protected locator; full service/hosted acceptance pending |
| Secret-bearing configuration repr | Parent dataclass includes client/operator/registration/root credentials | Child `field(repr=False)` plus regression tests; arbitrary serialization is still prohibited |
| Safe self-bootstrap | A locked vault cannot obtain its unlock credential through its own API | Explicit root-only supervisor file transport implemented; production KMS/HSM and managed workload identity still missing |
| Cryptographic context separation | Parent fixed installation-wide PBKDF2 salt and value-only ciphertext do not bind tenant/namespace/key/version | Rust envelope-encryption owner work must prove context binding, key separation and cross-context tamper rejection |
| Workload authorization | Administrator access is not consumer read authority | Signature/issuer/audience/subject/tenant/environment/key/version/operation validation and wrong-scope denial required; not implemented here |
| Version and lease lifecycle | Parent overwrites one encrypted value; no immutable secret versions or leases | Add durable version/rotation/revocation/lease records, audit and bounded cache policy |
| Other legacy Keyverse credentials | Client/operator tokens and deployment templates still use historical config/transport | Migrate separately without losing bootstrap/recovery; this PR fixes only vault-root storage and repr |
| Operational confidentiality | Parent review flags metadata caching; backups/WAL may retain retired plaintext root | No-store handling, key rewrap, secure retirement, backup/restore and incident evidence remain mandatory |
| Organization consumers | CO has a CredentialBackend seam; Naruon, LineageWeave and others still expose dotenv entry points in indexed code snapshots | Central #2063 records explicit evidence and owner work; no consumer cutover claimed |
| Release | Feature-head focused tests are not an immutable release | Exact-head full suite, 100% service coverage/docstrings, security checks, independent review, protected merge and release/rollback evidence |

## Verification evidence

The environment reconstructed the exact upstream `bootstrap.py`, `config.py`
and `kv_store.py` blobs and confirmed their Git hashes before changing them.
Initial hostile tests observed 36 failures and one pass. Additional read-race
and atime regressions were observed failing before their corresponding fixes.
The final focused suite has **42 passed** with no failures.

| Published implementation blob | Git blob SHA |
|---|---|
| `services/account_unification/app/bootstrap.py` | `4d368a781f0bfe2fe03f1a9440a1619d9be50d82` |
| `services/account_unification/app/config.py` | `7e45e8e6cc2e127389e912e6d6252c7578b11434` |
| `services/account_unification/tests/test_keyvault_bootstrap_credentials.py` | `c8a51c5423660fd498dca6c2606359d6835ef47c` |

Coverage intersection with changed executable lines is 61/61, with no missing
changed-line branch arcs. This is NOT a claim of 100% coverage of the complete
account-unification service. Python compilation and `git diff --check` pass.
The complete locked dependency suite, Ruff, hosted security Checks, independent
review, deployment acceptance, KMS/HSM and release have not been verified here.

## Product contract and migration sequence

1. Repair and integrate the existing vault foundation through normal reviewed
PRs; never replace a valid predecessor by closing it without delta transfer.
2. Implement the native Rust secret data plane, trusted workload verification,
context-bound storage and versioned lease/rotation/audit contracts in Keyverse.
3. Publish immutable owner artifacts and conformance evidence. Cross-domain
contract metadata may be published through context-graph-contracts; no secret
value or product-owned private schema is copied there.
4. Connect CO through its existing credential port. Other products call CO for
provider operations rather than receiving copies of model-provider keys.
5. For each consumer, prove clean startup without `.env`, access denial,
authority outage, lease expiry/revocation, rotation, recovery and rollback.
Then retire its old dotenv and plaintext credential entry points.
6. Central `.github` uses released AppGuardrail detection and exact-head
inventories. Include unreadable/unscanned repositories in the denominator.
Missing evidence must not be converted into a zero-gap or complete result.

Configuration endpoint/locator values can be transported by environment where
required by an external runtime; application credentials must not depend on
`.env` loading, home-directory discovery or environment fallback. The root-only
exception is documented in [operations](operations/keyvault.md). An authorized
still-valid lease may be used only within its explicit revocation policy;
expired cache, operator-token reuse and fallback secret stores are forbidden.

The required runtime sequence is:

```mermaid
sequenceDiagram
participant W as Consumer workload
participant I as Trusted identity verifier
participant K as Keyverse secret service
participant E as External KMS/HSM
W->>I: Short-lived workload identity
I-->>K: Verified principal and constrained scope
W->>K: Versioned opaque secret reference
K->>K: Tenant/key/operation/lease authorization
K->>E: Authorized envelope-key operation
E-->>K: Protected key operation result
K->>K: Durable audit and version check
K-->>W: Scoped lease or fail-closed error
```

This diagram is the target contract; the implemented child is only the legacy
root-bootstrap repair. It is not a working remote secret-service deployment.
98 changes: 78 additions & 20 deletions docs/operations/keyvault.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,80 @@
# Keyvault operations

Keyvault is opt-in. Configure its database path and passphrase in Keyverse's
existing private configuration store. If the passphrase is absent, every
Keyvault administrator operation fails closed as unavailable.

Administrators can list secret metadata, set or rotate a value, inspect its
audit history, and delete it. The API never returns plaintext. Use the product
form only for values being created or rotated; after submission, show presence,
last change time, and outcome. Do not echo the submitted value in UI state,
logs, screenshots, notifications, or error text.

Each set or delete and its audit event commit in one database transaction. On
failure, retry only after checking metadata and audit history. Back up and
restore the Keyvault database as one unit. A wrong passphrase fails
authentication during decryption; it is not an empty vault.

Noema and contextual-orchestrator keep their current stores until a separate
consumer PR proves all of these together: signed workload identity, one
namespace-bound read scope, denied cross-namespace access, rotation, Keyverse
outage behavior, and rollback. An administrator session is never a substitute
for workload identity.
## Current delivery boundary

PR #129 is the canonical encrypted-store foundation, not a released workload
credential service. The administrator API exposes metadata and mutation
outcomes, never plaintext. Consumers must not use an operator token as workload
identity, query the Keyverse database, or import this branch as a dependency.

## Root bootstrap without a plaintext config-store key

The account-unification compatibility service now accepts the non-secret
`keyvault_passphrase_file` locator in its private config namespace. It rejects
any `keyvault_passphrase` entry, including an empty entry, instead of silently
falling back. The process environment and `.env` are not alternative credential
sources. With no locator the vault remains disabled; an invalid locator aborts
startup rather than making an encrypted vault look empty.

The supervisor must supply a regular POSIX file with mode `0400` or `0600`,
owned by root or the service's effective UID, and with one hard link. Every
path component must be absolute and free of symlinks, dot segments, and empty
segments. The reader uses descriptor-relative opens, no-follow and nonblocking
flags, rejects FIFOs/directories, bounds content to 4096 bytes, validates UTF-8,
and detects in-place changes while reading. One final LF or CRLF is removed;
other whitespace remains part of the credential. Errors do not echo the path,
raw content, or an underlying decoder/OS diagnostic. All opened descriptors
are closed on both success and failure.

Use a supervisor-managed, read-only private tmpfs credential mount outside the
repository, image, config DB, application secret DB, logs, and backup of those
DBs. Standard Kubernetes projected Secret symlinks are deliberately NOT accepted
by this compatibility reader: a trusted deployment controller must materialize
a private regular-file snapshot, or use a separately reviewed native adapter.
Do not weaken the reader to follow an arbitrary symlink. A filesystem file is
not an HSM: host/root compromise, crash dumps and Python string retention remain
risks. This change does not claim hardware custody or complete zeroization.

This root-only exception breaks the self-bootstrap cycle: a vault cannot fetch
the key needed to open itself from its own unavailable API. Production Rust
vault work must replace it with managed workload identity and an external
KMS/HSM envelope-key provider. Ordinary product credentials do not qualify for
this exception.

## Controlled migration

Before adopting this unreleased change, stop the affected vault service and
have an authorized operator transfer the existing root credential through a
private administrative channel to the supervisor. Keep its exact value: merely
changing the root key makes existing ciphertext unreadable. Configure only the
new locator, remove the plaintext entry, rehearse decryption and restart against
a private restored copy, then cut over. SQL DELETE alone is not secure erasure:
old SQLite pages, WALs and backups may retain the former value. Retire those
copies under the recovery/retention policy after a tested rewrap/rotation. This
session does not transfer, delete, rotate, or deploy real credentials.

Rollback must retain the protected bootstrap path; it must not recreate `.env`
or put the root credential back into the ordinary config DB. Actual key
rotation, durable rewrap/rollback and KMS integration remain release gates.

## Administration and consumer adoption

Administrators can list metadata, set or rotate a stored value, inspect audit
history and delete it. Never echo submitted values into UI state, logs,
screenshots, notifications, exceptions or exports. Configuration repr excludes
client, operator, registration and vault bootstrap credentials; this is not
permission to serialize configuration with `dataclasses.asdict`.

Each secret mutation and its audit event commits in one transaction. Back up
and restore that database as one unit. Wrong-key decryption is an error, not an
empty namespace. Root-key storage protection does not fix the parent's
installation-wide KDF salt, ciphertext context binding, or missing immutable
secret-version/lease model; those findings remain explicit in the gap register.

Noema, contextual-orchestrator and other consumers require signed workload
identity, namespace/key/version-scoped authorization, cross-tenant denial,
rotation/revocation, bounded lease behavior, outage and rollback tests, plus an
immutable Keyverse release before production adoption. A cache may be used only
within its issued lease and revocation policy; no expired-cache or dotenv
fallback is permitted. Application settings which are not secrets stay in typed
configuration, not in the secret-value store.
Loading