Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions backend/core/credential_resolution.py
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)
112 changes: 112 additions & 0 deletions backend/tests/test_credential_resolution.py
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())
63 changes: 63 additions & 0 deletions docs/adr/0018-keyverse-credential-resolution-port.md
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
Comment thread
seonghobae marked this conversation as resolved.
- 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.
3 changes: 3 additions & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ govern implementation.
| [ADR-0002](0002-fitted-topic-artifact-consumption.md) | Conditionally consume only a versioned fitted topic artifact through a fail-closed adapter | Proposed | Target `PLANNED`; runtime `BLOCKED-UPSTREAM` |
| [ADR-0003](0003-separate-topic-measurement-from-agenda-generation.md) | Keep statistical measurement separate from agenda generation | Proposed | Target and future capability `PLANNED`; no implementation authorization |
| [ADR-0004](0004-status-weighted-calendar-conflicts.md) | Evaluate CalDAV VEVENT overlaps by occupying status; cancelled does not occupy | Accepted | `ACCEPTED-NARUON-POLICY`; advisory evaluate API only |
| [ADR-0018](0018-keyverse-credential-resolution-port.md) | Stage Keyverse credential consumption behind a value-free, fail-closed application port | Proposed | Consumer seam only; production cutover remains `BLOCKED-UPSTREAM` until immutable Keyverse workload release |

ADR-0018 intentionally leaves room for ADR numbers already proposed on parallel open branches. It must be reconciled against protected `develop` after those stacks land; its number does not imply that absent ADR-0005 through ADR-0017 are accepted on this branch.

The complete topic-intelligence requirements, architecture, contract, UML,
conceptual ERD, security, test, and operability graph is indexed at
Expand Down
35 changes: 35 additions & 0 deletions docs/doctoring/keyverse-credential-resolution-port.md
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/