-
Notifications
You must be signed in to change notification settings - Fork 1
feat(security): stage Keyverse credential-resolution port #1630
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
seonghobae
wants to merge
10
commits into
autoresearch/frontend-sec-bump
Choose a base branch
from
feat/keyverse_credential_resolution_port_20260910
base: autoresearch/frontend-sec-bump
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
348f96c
test(security): define Keyverse credential resolution port contract
seonghobae df2b094
feat(security): add fail-closed credential resolution port
seonghobae 2b7c6a2
test(security): cover resolved credential redaction
seonghobae a49641e
docs(adr): define Keyverse credential resolution consumer boundary
seonghobae 81954c0
docs(adr): index Keyverse credential resolution boundary
seonghobae 1d22146
docs(security): ground Keyverse credential port in workload identity …
seonghobae f701848
chore(stack): adopt current frontend security owner
seonghobae ba820d1
test(credentials): reject non-string reference fields
seonghobae 821dc57
fix(credentials): validate reference field types
seonghobae 0d10215
docs(security): harden credential redirect boundary
seonghobae File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| """Application port for resolving credentials from an external authority. | ||
|
|
||
| The concrete Keyverse transport adapter intentionally does not live here yet. | ||
| Consumers can depend on this value-free contract while the owner publishes an | ||
| immutable workload credential-resolution API and release artifact. | ||
| """ | ||
| from __future__ import annotations | ||
|
|
||
| from dataclasses import dataclass, fields | ||
| from typing import Protocol | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class CredentialReference: | ||
| """Identify one credential without carrying its secret value.""" | ||
|
|
||
| authority: str | ||
| tenant_id: str | ||
| environment_name: str | ||
| secret_namespace: str | ||
| secret_key: str | ||
| secret_version: str | ||
| purpose_name: str | ||
|
|
||
| def __post_init__(self) -> None: | ||
| """Reject references that cannot be authorized unambiguously.""" | ||
| for reference_field in fields(self): | ||
| field_value = getattr(self, reference_field.name) | ||
| if not isinstance(field_value, str) or not field_value.strip(): | ||
| raise ValueError( | ||
| f"{reference_field.name} must be a non-blank credential reference field" | ||
| ) | ||
|
|
||
|
|
||
| @dataclass(frozen=True, repr=False) | ||
| class ResolvedCredential: | ||
| """Hold one resolved secret while keeping its value out of repr output.""" | ||
|
|
||
| secret_value: str | ||
| reference: CredentialReference | ||
|
|
||
| def __repr__(self) -> str: | ||
| """Render only value-free identity metadata.""" | ||
| return f"ResolvedCredential(reference={self.reference!r}, secret_value=<redacted>)" | ||
|
|
||
|
|
||
| class CredentialResolutionUnavailable(RuntimeError): | ||
| """Raised when the configured credential authority cannot resolve safely.""" | ||
|
|
||
|
|
||
| class CredentialResolver(Protocol): | ||
| """Resolve one credential reference through an external authority.""" | ||
|
|
||
| def resolve_credential(self, reference: CredentialReference) -> ResolvedCredential: | ||
| """Resolve one credential or fail closed without local fallback.""" | ||
| pass | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class UnavailableCredentialResolver: | ||
| """Fail closed until an immutable Keyverse workload adapter is available.""" | ||
|
|
||
| reason: str | ||
|
|
||
| def resolve_credential(self, reference: CredentialReference) -> ResolvedCredential: | ||
| """Reject resolution instead of consulting environment or dotenv state.""" | ||
| del reference | ||
| raise CredentialResolutionUnavailable(self.reason) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,112 @@ | ||
| """Contract tests for the Keyverse credential-resolution application port.""" | ||
| from __future__ import annotations | ||
|
|
||
| import pytest | ||
|
|
||
| from core.credential_resolution import ( | ||
| CredentialReference, | ||
| CredentialResolutionUnavailable, | ||
| ResolvedCredential, | ||
| UnavailableCredentialResolver, | ||
| ) | ||
|
|
||
|
|
||
| def _credential_reference() -> CredentialReference: | ||
| """Return one complete value-free Keyverse credential reference.""" | ||
| return CredentialReference( | ||
| authority="keyverse", | ||
| tenant_id="workspace-123", | ||
| environment_name="production", | ||
| secret_namespace="naruon/runtime", | ||
| secret_key="auth_session_hmac_secret", | ||
| secret_version="v1", | ||
| purpose_name="session-signing", | ||
| ) | ||
|
|
||
|
|
||
| def test_credential_reference_rejects_blank_identity_fields() -> None: | ||
| """Credential references must bind every authorization identity dimension.""" | ||
| for field_name in ( | ||
| "authority", | ||
| "tenant_id", | ||
| "environment_name", | ||
| "secret_namespace", | ||
| "secret_key", | ||
| "secret_version", | ||
| "purpose_name", | ||
| ): | ||
| values = { | ||
| "authority": "keyverse", | ||
| "tenant_id": "workspace-123", | ||
| "environment_name": "production", | ||
| "secret_namespace": "naruon/runtime", | ||
| "secret_key": "auth_session_hmac_secret", | ||
| "secret_version": "v1", | ||
| "purpose_name": "session-signing", | ||
| } | ||
| values[field_name] = " " | ||
| with pytest.raises(ValueError, match=field_name): | ||
| CredentialReference(**values) | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "invalid_value", | ||
| [ | ||
| pytest.param(None, id="none"), | ||
| pytest.param(1, id="integer"), | ||
| pytest.param(b"bytes", id="bytes"), | ||
| pytest.param([], id="list"), | ||
| ], | ||
| ) | ||
| def test_credential_reference_rejects_non_string_identity_fields( | ||
| invalid_value: object, | ||
| ) -> None: | ||
| """Runtime construction must reject non-string identity dimensions uniformly.""" | ||
| values: dict[str, object] = { | ||
| "authority": invalid_value, | ||
| "tenant_id": "workspace-123", | ||
| "environment_name": "production", | ||
| "secret_namespace": "naruon/runtime", | ||
| "secret_key": "auth_session_hmac_secret", | ||
| "secret_version": "v1", | ||
| "purpose_name": "session-signing", | ||
| } | ||
|
|
||
| with pytest.raises(ValueError, match="authority"): | ||
| CredentialReference(**values) | ||
|
|
||
|
|
||
| def test_credential_reference_repr_contains_no_secret_value() -> None: | ||
| """Value-free references are safe to log and review.""" | ||
| rendered = repr(_credential_reference()) | ||
|
|
||
| assert "workspace-123" in rendered | ||
| assert "auth_session_hmac_secret" in rendered | ||
| assert "secret_value" not in rendered | ||
|
|
||
|
|
||
| def test_resolved_credential_repr_redacts_secret_value() -> None: | ||
| """Resolved credential values never appear in ordinary object rendering.""" | ||
| credential = ResolvedCredential( | ||
| secret_value="do-not-log-this-secret", | ||
| reference=_credential_reference(), | ||
| ) | ||
|
|
||
| rendered = repr(credential) | ||
|
|
||
| assert "do-not-log-this-secret" not in rendered | ||
| assert "<redacted>" in rendered | ||
| assert "workspace-123" in rendered | ||
|
|
||
|
|
||
| def test_unavailable_resolver_fails_closed_without_fallback() -> None: | ||
| """An unreleased Keyverse data plane cannot fall back to env or dotenv.""" | ||
| resolver = UnavailableCredentialResolver( | ||
| reason="Keyverse workload credential API is not released" | ||
| ) | ||
|
|
||
| with pytest.raises( | ||
| CredentialResolutionUnavailable, | ||
| match="Keyverse workload credential API is not released", | ||
| ): | ||
| resolver.resolve_credential(_credential_reference()) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| # ADR-0018: Consume Keyverse credentials through a value-free application port | ||
|
|
||
| - Status: Proposed | ||
| - Date: 2026-09-10 | ||
| - Scope: Naruon runtime credential acquisition | ||
| - Related: ContextualWisdomLab/.github#2063, ContextualWisdomLab/keyverse#129, ContextualWisdomLab/keyverse#151, ContextualWisdomLab/keyverse#153 | ||
|
|
||
| ## Context | ||
|
|
||
| Naruon currently has bootstrap/runtime settings that can be sourced through Pydantic environment and dotenv discovery. Several tenant-scoped provider and mailbox credentials already live in Naruon-owned encrypted persistence, but root runtime material such as session-signing and encryption keys still requires a migration boundary. | ||
|
|
||
| Keyverse is the intended CWL credential authority. Its workload credential data plane, however, is not yet an immutable released dependency. Reading an open Keyverse PR, querying Keyverse tables, copying its encryption implementation, or silently falling back to `.env` after a failed future cutover would violate the repository and organization boundaries. | ||
|
|
||
| Naruon therefore needs a consumer-owned port before it needs a transport adapter. The port must express exactly which credential is requested without carrying its secret value in logs, configuration objects, PR fixtures, or interoperability metadata. | ||
|
|
||
| ## Decision | ||
|
|
||
| Naruon introduces `core.credential_resolution` as an application port with these value-free identity dimensions: | ||
|
|
||
| - credential authority; | ||
| - tenant/workspace identity; | ||
| - deployment environment; | ||
| - secret namespace; | ||
| - secret key; | ||
| - immutable/versioned secret identifier; | ||
| - runtime purpose. | ||
|
|
||
| The port returns a resolved credential only through an implementation of `CredentialResolver`. Until Keyverse publishes and Naruon pins an immutable workload credential-resolution contract, `UnavailableCredentialResolver` is the only integration state introduced by this ADR and fails closed. | ||
|
|
||
| This PR does **not** replace current bootstrap settings and does not claim `.env` removal. It creates the seam required for a later shadow-verification and cutover stack. The future Keyverse adapter must authenticate the workload with released identity semantics, bind the request to the reference above, verify authenticated HTTPS and authority before attaching workload credentials, disable redirects by default, honor version/revocation/lease rules, reject stale or expired material, and provide no environment/dotenv/plaintext-database fallback. If an owner-released contract later requires redirects, Naruon must re-authorize every destination after redirect resolution and must not forward workload credentials until the redirected HTTPS endpoint and authority have passed the same verification policy. | ||
|
|
||
| ## Alternatives considered | ||
|
|
||
| ### Import Keyverse source or query its database | ||
|
|
||
| Rejected. That couples Naruon to owner internals and an unreleased schema rather than a released bounded-context contract. | ||
|
|
||
| ### Keep using environment variables as the long-term credential interface | ||
|
|
||
| Rejected. Environment and dotenv are process bootstrap transports, not an auditable credential authority with namespace, version, revocation, and workload authorization semantics. | ||
|
|
||
| ### Build a Naruon-local second vault | ||
|
|
||
| Rejected. Naruon owns its product/runtime policy but not the CWL-wide credential authority. A second vault would duplicate custody, rotation, revocation, and audit responsibilities. | ||
|
|
||
| ## Consequences | ||
|
|
||
| Naruon can prepare callers, test doubles, outage behavior, and configuration migration without pretending the Keyverse data plane is already released. The cost is a staged migration: existing root runtime configuration remains until the owner contract is released and an adapter passes shadow, rotation, revocation, outage, rollback, and clean-start acceptance. | ||
|
|
||
| The `Proposed` status is intentional. It must not be promoted to `Accepted` solely because this PR merges; acceptance requires an immutable Keyverse owner release plus Naruon integration evidence on the protected branch. | ||
|
|
||
| ## Verification requirements | ||
|
|
||
| Before production cutover: | ||
|
|
||
| 1. pin an immutable Keyverse workload credential API/client/schema release; | ||
| 2. verify the workload identity and tenant/environment/purpose binding end to end; | ||
| 3. exercise current and rotated secret versions and revoked/expired denial; | ||
| 4. verify Keyverse outage fails closed without `.env`, environment, local-file, or local-vault secret fallback; | ||
| 5. verify resolved values are absent from logs, traces, metrics, artifacts, browser bundles, exception rendering, and LLM inputs; | ||
| 6. verify redirect handling is disabled unless a released contract requires it, and then re-authorize each redirected HTTPS destination before any workload credential is attached; | ||
| 7. run Naruon's protected-branch backend, security, CodeQL, dependency, image, coverage, and independent-review gates on one exact head; | ||
| 8. only then remove the corresponding legacy dotenv/environment discovery paths in a successor change. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # Keyverse credential-resolution consumer boundary — standards doctoring | ||
|
|
||
| Observed Naruon base: `develop@042b0c70531b229af3acbd0421a2f23098d848b3` | ||
| Consumer PR: `#1630` | ||
| Decision record: `docs/adr/0018-keyverse-credential-resolution-port.md` | ||
|
|
||
| ## Evidence and interpretation | ||
|
|
||
| NIST SP 800-207 rejects implicit trust based only on network location and treats authentication and authorization as explicit decisions before access to an enterprise resource. SP 800-207A carries that principle into cloud-native applications and specifically describes application/service identity as an access-control input; it cites workload identity infrastructure such as SPIFFE as part of the enforcement architecture. | ||
|
|
||
| SPIFFE's stable Workload API specification standardizes how a running workload obtains and validates cryptographic workload identity. Its caller-identification requirement is relevant to the future Keyverse adapter: merely reaching a local or network endpoint is not sufficient authority to resolve a secret. The credential request must be bound to an authenticated workload identity and the requested resource dimensions. | ||
|
|
||
| OWASP's Secrets Management Cheat Sheet recommends centralized secret storage/provisioning and explicitly treats auditing, rotation, revocation, and expiration as lifecycle requirements. It calls for audit evidence covering who or what requested a secret, approval/rejection, use, expiration, authentication/authorization errors, and administrative updates. | ||
|
|
||
| These sources support the following Naruon-side constraints, without defining Keyverse's owner implementation: | ||
|
|
||
| 1. `CredentialReference` remains value-free and names tenant, environment, namespace, key, version, purpose, and authority so authorization can be resource-specific rather than inferred from network reachability. | ||
| 2. The future adapter must present a verifiable workload identity and fail closed when identity, authorization, version, revocation, expiration, or authority validation fails. | ||
| 3. Rotation and revocation are owner lifecycle semantics. Naruon must consume them through the released Keyverse contract rather than reconstructing them from local timestamps or a duplicate vault. | ||
| 4. Secret values must not appear in reference metadata or routine object rendering. Audit/event records should identify the request and outcome without carrying the secret itself. | ||
| 5. An unavailable or unreleased Keyverse data plane does not authorize fallback to `.env`, plaintext configuration, or a Naruon-local second credential authority. Existing bootstrap mechanisms remain an explicitly tracked migration state until a verified cutover; they are not silently reclassified as the target architecture. | ||
|
|
||
| ## What these sources do not establish | ||
|
|
||
| The cited standards do not prove that the current Keyverse open PRs provide a production workload secret API, nor do they select a particular transport, KMS, HSM, lease duration, or secret-storage schema for Keyverse. Those are owner decisions and require immutable release evidence before Naruon adopts them. | ||
|
|
||
| ## APA 7 references | ||
|
|
||
| Chandramouli, R., & Butcher, Z. (2023). *A zero trust architecture model for access control in cloud-native applications in multi-cloud environments* (NIST Special Publication 800-207A). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-207A | ||
|
|
||
| OWASP Foundation. (n.d.). *Secrets management cheat sheet*. OWASP Cheat Sheet Series. https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html | ||
|
|
||
| Rose, S., Borchert, O., Mitchell, S., & Connelly, S. (2020). *Zero trust architecture* (NIST Special Publication 800-207). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-207 | ||
|
|
||
| SPIFFE. (n.d.). *SPIFFE Workload API*. https://spiffe.io/docs/latest/spiffe-specs/spiffe_workload_api/ |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.