NK-WP-0054-T04: first live conformance run (OpenBao, PostgreSQL)
All checks were successful
Bounded-execution contracts / contracts (push) Successful in 2m31s
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

observe.py compares the service-identity declarations against
railiance-platform's value-safe live metadata (b0b6c0c) with a 7-day
freshness bound; read-only, offline, 9 tests. Result: 5 conformant,
2 unknown; undeclared keycape/platform-admin alias grants platform-admin
without aal2 (non-conformant), 14 undeclared roles unknown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 149903@bnt-lap001
Assistant-Session: 4c20a536-1452-4a85-bc09-e1987d9817d6
This commit is contained in:
tegwick 2026-11-07 14:32:19 +01:00
parent e1cad11a80
commit 4a684baabf
6 changed files with 702 additions and 1 deletions

View file

@ -203,6 +203,11 @@ service-identity-test: ## Run service-identity schema/validator tests (NK-WP-005
service-identity-check: ## Evaluate the checked-in service identity declarations (read-only, offline)
python3 tools/service-identity/validate.py tools/service-identity/declarations/*.yaml
SI_METADATA ?= ../railiance-platform/docs/evidence/2026-11-03-nk-wp-0054-t04-identity-metadata
service-identity-observe: ## Declared vs owner live metadata, read-only (NK-WP-0054-T04): SI_METADATA=dir
python3 tools/service-identity/observe.py --openbao $(SI_METADATA)/openbao-identity-metadata.json \
--postgresql $(SI_METADATA)/postgresql-identity-metadata.json
bounded-execution-admission-test: ## Run bounded-execution admission validator tests (NK-WP-0056-T01)
python3 -m pytest tools/bounded-execution-admission/tests

View file

@ -0,0 +1,43 @@
# NK-WP-0054-T04: first live conformance run, 2026-11-07
`report.json` was produced by `tools/service-identity/observe.py` with `--now 2026-11-07T14:00:00Z` (exit 1:
one non-conformant finding). The run was read-only and offline. It read no Secret and changed nothing.
**Inputs.** The railiance-platform owner metadata, at `b0b6c0c`, in
`docs/evidence/2026-11-03-nk-wp-0054-t04-identity-metadata/`:
| File | SHA-256 | collected_at |
|---|---|---|
| `openbao-identity-metadata.json` | `60da6df0934dcf9a9a5e25c7f05724a3b5edc719065ff5d5fe03e5537f58aa1b` | 2026-11-03T20:18:53Z |
| `postgresql-identity-metadata.json` | `e627278b02250d87986ae40e43e38c6ceaecdbc56652b447ac7324734f00541a` | 2026-11-03T20:18:54Z |
The declarations are those at this commit, all with `owner_confirmed: false`. The freshness bound is 7 days,
and both inputs were 89.7 h old.
## Result
Declared identities: 5 conformant, 2 unknown, 0 non-conformant.
| Identity | Verdict | Owner | Why |
|---|---|---|---|
| openbao/netkingdom-platform-admin | unknown | railiance-platform | `token_max_ttl` 0. Renewal falls back to the mount or system maximum, which the export omits. The declaration says a one-hour token. |
| bounded-executor/net-kingdom-bounded-executor | unknown | railiance-platform | `kubernetes/net-kingdom-bind-executor-svc-authelia-write` is absent. The export predates the 2026-11-04 go-live. |
| credential-broker, fluid-telegram-operators, secrets-engine-openbao, coding-agent | conformant | railiance-platform | Policies, aal2, groups, TTLs and the observe-only boundary all match. |
| postgresql/privacyidea | conformant | railiance-platform | No privilege flags; owns `privacyidea_db`. |
Undeclared observed roles: 1 non-conformant, 14 unknown.
- **Non-conformant:** `keycape/platform-admin` grants the full `platform-admin` policy and binds only
`groups: [net-kingdom-admins]`, with no `/assurance/level` aal2 claim. It still lists the retired
`bao.coulomb.social` callback. This is the "compatibility alias" from railiance-platform `docs/openbao.md`. It
shares KeyCape client `openbao-admin`, and its role is not declared in railiance-platform `openbao/auth/`.
So a second administrator login exists that the aal2 rule does not cover.
- **Unknown (SI-11, undeclared):**
- nine human OIDC KV-read roles on `netkingdom` without aal2: eight bound to `net-kingdom-admins` and one
to `whynot-design`;
- `kubernetes/external-secrets-issue-core`, which shares a declared policy;
- four PostgreSQL login roles: `postgres` (superuser, CNPG), `streaming_replica` (CNPG), `interhub` and
`probe`.
Not covered: KeyCape (its live view is KEY-WP-0045-T02, founder-run), LLDAP, Authelia and privacyIDEA. Posture
feedback does not consume these findings yet.

View file

@ -0,0 +1,232 @@
{
"as_of": "2026-11-07T14:00:00Z",
"freshness": [
{
"age_hours": 89.7,
"collected_at": "2026-11-03T20:18:53Z",
"fresh": true,
"max_age_days": 7,
"service": "openbao",
"source_kind": "live"
},
{
"age_hours": 89.7,
"collected_at": "2026-11-03T20:18:54Z",
"fresh": true,
"max_age_days": 7,
"service": "postgresql",
"source_kind": "live"
}
],
"identities": [
{
"evidence": "openbao",
"findings": [
{
"detail": "netkingdom/platform-admin has no role max ttl; renewal is bounded by the mount/system default, which the export does not include",
"identity": "netkingdom-platform-admin",
"owner": "railiance-platform",
"rule": "lifetime",
"verdict": "unknown"
}
],
"identity": "netkingdom-platform-admin",
"observed_as": [
"netkingdom/platform-admin"
],
"owner": "railiance-platform",
"service": "openbao",
"verdict": "unknown"
},
{
"evidence": "openbao",
"findings": [],
"identity": "credential-broker-keycape-client-secret-generate",
"observed_as": [
"netkingdom/credential-broker-keycape-client-secret-generate"
],
"owner": "railiance-platform",
"service": "openbao",
"verdict": "conformant"
},
{
"evidence": "openbao",
"findings": [],
"identity": "fluid-telegram-operators",
"observed_as": [
"netkingdom/fluid-telegram-hall-of-helix-workload-kv"
],
"owner": "railiance-platform",
"service": "openbao",
"verdict": "conformant"
},
{
"evidence": "openbao",
"findings": [],
"identity": "secrets-engine-openbao",
"observed_as": [
"keycape-services/secrets-engine-openbao"
],
"owner": "railiance-platform",
"service": "openbao",
"verdict": "conformant"
},
{
"evidence": "openbao",
"findings": [],
"identity": "coding-agent",
"observed_as": [
"netkingdom/coding-agent",
"approle/coding-agent-railiance-platform"
],
"owner": "railiance-platform",
"service": "openbao",
"verdict": "conformant"
},
{
"evidence": "openbao",
"findings": [
{
"detail": "role kubernetes/net-kingdom-bind-executor-svc-authelia-write is not in the export (missing, or created after collected_at)",
"identity": "net-kingdom-bounded-executor",
"owner": "railiance-platform (OpenBao role) for NetKingdom",
"rule": "observed",
"verdict": "unknown"
}
],
"identity": "net-kingdom-bounded-executor",
"observed_as": [
"kubernetes/net-kingdom-bind-executor-svc-authelia-write"
],
"owner": "railiance-platform (OpenBao role) for NetKingdom",
"service": "bounded-executor",
"verdict": "unknown"
},
{
"evidence": "postgresql",
"findings": [],
"identity": "privacyidea",
"observed_as": [
"net-kingdom-pg/privacyidea"
],
"owner": "railiance-platform",
"service": "postgresql",
"verdict": "conformant"
}
],
"summary": {
"conformant": 5,
"unknown": 2
},
"undeclared": [
{
"detail": "undeclared role grants admin policies ['platform-admin'] without binding aal2",
"identity": "keycape/platform-admin",
"owner": "railiance-platform",
"rule": "SI-11",
"verdict": "non-conformant"
},
{
"detail": "undeclared role shares declared policies ['workload-kv-read-issue-core-runtime']",
"identity": "kubernetes/external-secrets-issue-core",
"owner": "railiance-platform",
"rule": "SI-11",
"verdict": "unknown"
},
{
"detail": "undeclared human OIDC role without aal2 (groups ['net-kingdom-admins'], policies ['workload-kv-read-binky-company-email-imap'])",
"identity": "netkingdom/binky-company-email-imap-workload-kv-read",
"owner": "railiance-platform",
"rule": "SI-11",
"verdict": "unknown"
},
{
"detail": "undeclared human OIDC role without aal2 (groups ['net-kingdom-admins'], policies ['workload-kv-read-binky-qonto-api'])",
"identity": "netkingdom/binky-qonto-api-workload-kv-read",
"owner": "railiance-platform",
"rule": "SI-11",
"verdict": "unknown"
},
{
"detail": "undeclared human OIDC role without aal2 (groups ['net-kingdom-admins'], policies ['workload-kv-read-forgejo-admin'])",
"identity": "netkingdom/forgejo-admin-workload-kv-read",
"owner": "railiance-platform",
"rule": "SI-11",
"verdict": "unknown"
},
{
"detail": "undeclared human OIDC role without aal2 (groups ['net-kingdom-admins'], policies ['workload-kv-read-informed-decision-sitting-requester-client'])",
"identity": "netkingdom/informed-decision-sitting-requester-workload-kv-read",
"owner": "railiance-platform",
"rule": "SI-11",
"verdict": "unknown"
},
{
"detail": "undeclared human OIDC role without aal2 (groups ['net-kingdom-admins'], policies ['workload-kv-read-policy-nexus-forgejo-source'])",
"identity": "netkingdom/policy-nexus-forgejo-source-workload-kv-read",
"owner": "railiance-platform",
"rule": "SI-11",
"verdict": "unknown"
},
{
"detail": "undeclared human OIDC role without aal2 (groups ['net-kingdom-admins'], policies ['workload-kv-read-railiance-backup-offsite-lane'])",
"identity": "netkingdom/railiance-backup-workload-kv-read",
"owner": "railiance-platform",
"rule": "SI-11",
"verdict": "unknown"
},
{
"detail": "undeclared human OIDC role without aal2 (groups ['net-kingdom-admins'], policies ['workload-kv-read-secrets-engine-approval-client'])",
"identity": "netkingdom/secrets-engine-approval-client-workload-kv-read",
"owner": "railiance-platform",
"rule": "SI-11",
"verdict": "unknown"
},
{
"detail": "undeclared human OIDC role without aal2 (groups ['net-kingdom-admins'], policies ['workload-kv-read-secrets-engine-requester-client'])",
"identity": "netkingdom/secrets-engine-requester-workload-kv-read",
"owner": "railiance-platform",
"rule": "SI-11",
"verdict": "unknown"
},
{
"detail": "undeclared human OIDC role without aal2 (groups ['whynot-design'], policies ['workload-kv-read-whynot-design-npm-publish'])",
"identity": "netkingdom/whynot-design-workload-kv-read",
"owner": "railiance-platform",
"rule": "SI-11",
"verdict": "unknown"
},
{
"detail": "undeclared login role (flags none, owns ['interhub'])",
"identity": "net-kingdom-pg/interhub",
"owner": "railiance-platform",
"rule": "SI-11",
"verdict": "unknown"
},
{
"detail": "undeclared login role (flags ['superuser', 'create_role', 'create_db', 'replication', 'bypass_rls'], owns ['postgres'])",
"identity": "net-kingdom-pg/postgres",
"owner": "railiance-platform",
"rule": "SI-11",
"verdict": "unknown"
},
{
"detail": "undeclared login role (flags none, owns ['probe'])",
"identity": "net-kingdom-pg/probe",
"owner": "railiance-platform",
"rule": "SI-11",
"verdict": "unknown"
},
{
"detail": "undeclared login role (flags ['replication'], owns nothing)",
"identity": "net-kingdom-pg/streaming_replica",
"owner": "railiance-platform",
"rule": "SI-11",
"verdict": "unknown"
}
],
"undeclared_summary": {
"non-conformant": 1,
"unknown": 14
}
}

View file

@ -0,0 +1,267 @@
#!/usr/bin/env python3
# /// script
# dependencies = ["PyYAML>=6,<7"]
# ///
"""Live conformance check: declared service identities against owner metadata (NK-WP-0054-T04).
Compares the checked-in declarations (`tools/service-identity/declarations/`) with value-safe, owner-provided
metadata (railiance-platform `service-identity-metadata.v1`, exported by `scripts/service_identity_metadata_export.py`).
Each mapped identity ends `conformant`, `non-conformant` or `unknown`; every finding names its owner. Observed roles
that no declaration covers are reported too, because an undeclared login path is itself a finding.
Offline and read-only: it reads two JSON files and the declarations, never contacts a service, never reads a Secret
and changes nothing. The result is a proposal input for posture feedback, not authority.
python3 tools/service-identity/observe.py --openbao FILE --postgresql FILE [--now ISO] [--max-age-days 7] [--json]
"""
from __future__ import annotations
import argparse
import datetime as dt
import json
import pathlib
import sys
from typing import Any
import yaml
ROOT = pathlib.Path(__file__).resolve().parents[2]
DECLARATIONS = ROOT / "tools/service-identity/declarations"
SCHEMA = "railiance-platform.service-identity-metadata.v1"
AAL2 = "/assurance/level"
# Declared identity -> the OpenBao roles that realize it, with the facts the declaration states about them.
# Kept here, not in the declaration schema, until the owner confirms the declarations (owner_confirmed: false).
OPENBAO_MAP: dict[tuple[str, str], list[dict[str, Any]]] = {
("openbao", "netkingdom-platform-admin"): [
{"mount": "netkingdom", "role": "platform-admin", "policies": ["platform-admin", "operator-custody"],
"groups": ["net-kingdom-admins"], "ttl": 3600}],
("openbao", "credential-broker-keycape-client-secret-generate"): [
{"mount": "netkingdom", "role": "credential-broker-keycape-client-secret-generate",
"policies": ["credential-broker-keycape-client-secret-generate-issuer"], "groups": ["net-kingdom-admins"],
"ttl": 900}],
("openbao", "fluid-telegram-operators"): [
{"mount": "netkingdom", "role": "fluid-telegram-hall-of-helix-workload-kv",
"policies": ["workload-kv-fluid-telegram-hall-of-helix-operator"], "groups": ["fluid-telegram-operators"],
"ttl": 900, "max_ttl": 1800, "kv_data_paths": 4, "no_list_delete": True}],
("openbao", "secrets-engine-openbao"): [
{"mount": "keycape-services", "role": "secrets-engine-openbao", "policies": ["secrets-engine-login-self"],
"ttl": 300, "bound_subject": "service:secrets-engine"}],
("openbao", "coding-agent"): [
{"mount": "netkingdom", "role": "coding-agent",
"policies": ["agent-high-risk-boundary", "workload-kv-read-issue-core-runtime"], "ttl": 900, "max_ttl": 900,
"observe_only": True},
{"mount": "approle", "role": "coding-agent-railiance-platform",
"policies": ["agent-high-risk-boundary", "workload-kv-read-issue-core-runtime"], "ttl": 900, "max_ttl": 900,
"observe_only": True}],
("bounded-executor", "net-kingdom-bounded-executor"): [
{"mount": "kubernetes", "role": "net-kingdom-bind-executor-svc-authelia-write",
"service_account": ("sso", "nk-bind-executor")}],
}
# Policies that make a role an administrator of OpenBao itself.
ADMIN_POLICIES = {"platform-admin", "operator-custody"}
def load_metadata(path: pathlib.Path, service: str, now: dt.datetime, max_age: dt.timedelta) -> tuple[dict, dict]:
doc = json.loads(path.read_text())
if doc.get("schema") != SCHEMA or doc.get("service") != service:
raise SystemExit(f"{path}: not {SCHEMA} for {service}")
if doc.get("value_safe") is not True:
raise SystemExit(f"{path}: not marked value_safe; refusing to read it")
collected = dt.datetime.fromisoformat(doc["collected_at"].replace("Z", "+00:00"))
age = now - collected
freshness = {"service": service, "collected_at": doc["collected_at"], "source_kind": doc.get("source_kind"),
"age_hours": round(age.total_seconds() / 3600, 1), "max_age_days": max_age.days,
"fresh": dt.timedelta(0) <= age <= max_age}
return doc[service], freshness
def finding(identity: str, owner: str, verdict: str, rule: str, detail: str) -> dict[str, str]:
return {"identity": identity, "owner": owner, "verdict": verdict, "rule": rule, "detail": detail}
def check_role(spec: dict, role: dict | None, declared: dict, ident: str, owner: str) -> list[dict]:
where = f"{spec['mount']}/{spec['role']}"
if role is None:
return [finding(ident, owner, "unknown", "observed",
f"role {where} is not in the export (missing, or created after collected_at)")]
out = []
if "policies" in spec and sorted(role.get("token_policies", [])) != sorted(spec["policies"]):
out.append(finding(ident, owner, "non-conformant", "grants",
f"{where} policies {role.get('token_policies')} != declared {spec['policies']}"))
claims = role.get("bound_claims") or {}
if declared.get("second_factor") == "required" and claims.get(AAL2) != "aal2":
out.append(finding(ident, owner, "non-conformant", "second_factor",
f"{where} does not bind {AAL2}=aal2"))
if "groups" in spec and sorted(claims.get("groups") or []) != sorted(spec["groups"]):
out.append(finding(ident, owner, "non-conformant", "consumers",
f"{where} binds groups {claims.get('groups')} != declared {spec['groups']}"))
if "ttl" in spec and role.get("token_ttl_seconds", 0) > spec["ttl"]:
out.append(finding(ident, owner, "non-conformant", "lifetime",
f"{where} token ttl {role.get('token_ttl_seconds')}s > declared {spec['ttl']}s"))
if "max_ttl" in spec and role.get("token_max_ttl_seconds") != spec["max_ttl"]:
out.append(finding(ident, owner, "non-conformant", "lifetime",
f"{where} max ttl {role.get('token_max_ttl_seconds')}s != declared {spec['max_ttl']}s"))
if "ttl" in spec and "max_ttl" not in spec and not role.get("token_max_ttl_seconds") \
and not role.get("token_explicit_max_ttl_seconds"):
out.append(finding(ident, owner, "unknown", "lifetime",
f"{where} has no role max ttl; renewal is bounded by the mount/system default, "
"which the export does not include"))
if "bound_subject" in spec and role.get("bound_subject") != spec["bound_subject"]:
out.append(finding(ident, owner, "non-conformant", "subject",
f"{where} bound_subject {role.get('bound_subject')} != {spec['bound_subject']}"))
if "service_account" in spec:
ns, sa = spec["service_account"]
if role.get("bound_service_account_names") != [sa] or role.get("bound_service_account_namespaces") != [ns]:
out.append(finding(ident, owner, "non-conformant", "consumers", f"{where} is not bound to exactly {ns}/{sa}"))
rules = role.get("effective_rules") or []
if spec.get("observe_only"):
data_reads = [r["path"] for r in rules if "/data/" in r["path"]
and set(r["capabilities"]) & {"read", "create", "update", "delete", "list"}
and "deny" not in r["capabilities"]]
if data_reads:
out.append(finding(ident, owner, "non-conformant", "grants", f"{where} can read KV data: {data_reads}"))
elif not role.get("effective_complete"):
out.append(finding(ident, owner, "unknown", "grants", f"{where} effective rules are incomplete"))
if "kv_data_paths" in spec:
data = [r for r in rules if "/data/" in r["path"]]
if len(data) != spec["kv_data_paths"] or any(r["wildcard"] for r in data):
out.append(finding(ident, owner, "non-conformant", "grants",
f"{where} has {len(data)} KV data paths (declared {spec['kv_data_paths']} exact)"))
if spec.get("no_list_delete") and any({"list", "delete"} & set(r["capabilities"]) for r in data):
out.append(finding(ident, owner, "non-conformant", "grants", f"{where} allows list or delete"))
return out
def check_openbao(meta: dict, decls: dict[str, dict]) -> tuple[list[dict], list[dict]]:
roles = {(r["mount"], r["name"]): r for r in meta["roles"]}
results, mapped = [], set()
for (service, ident), specs in OPENBAO_MAP.items():
doc = decls.get(service)
declared = next((i for i in (doc or {}).get("identities", []) if i["name"] == ident), None)
if declared is None:
continue
owner = declared.get("owner", doc.get("owner"))
findings = []
for spec in specs:
mapped.add((spec["mount"], spec["role"]))
findings += check_role(spec, roles.get((spec["mount"], spec["role"])), declared, ident, owner)
verdict = ("non-conformant" if any(f["verdict"] == "non-conformant" for f in findings)
else "unknown" if findings else "conformant")
results.append({"service": service, "evidence": "openbao", "identity": ident, "owner": owner, "verdict": verdict,
"observed_as": [f"{s['mount']}/{s['role']}" for s in specs], "findings": findings})
# Undeclared roles: alternative paths to a declared identity's privileges, and human logins without aal2.
declared_policies = {p for specs in OPENBAO_MAP.values() for s in specs for p in s.get("policies", [])}
undeclared = []
for key, role in sorted(roles.items()):
if key in mapped:
continue
where = f"{key[0]}/{key[1]}"
policies = set(role.get("token_policies", []))
claims = role.get("bound_claims") or {}
human = role.get("role_type") == "oidc"
if policies & ADMIN_POLICIES:
undeclared.append(finding(where, "railiance-platform", "non-conformant", "SI-11",
f"undeclared role grants admin policies {sorted(policies & ADMIN_POLICIES)}"
+ ("" if claims.get(AAL2) == "aal2" else " without binding aal2")))
elif policies & declared_policies:
undeclared.append(finding(where, "railiance-platform", "unknown", "SI-11",
f"undeclared role shares declared policies {sorted(policies & declared_policies)}"))
elif human and claims.get(AAL2) != "aal2":
undeclared.append(finding(where, "railiance-platform", "unknown", "SI-11",
f"undeclared human OIDC role without aal2 (groups {claims.get('groups')}, "
f"policies {sorted(policies)})"))
return results, undeclared
def check_postgresql(meta: dict, decls: dict[str, dict]) -> tuple[list[dict], list[dict]]:
roles = {r["name"]: r for r in meta["roles"]}
owners = {d["name"]: d["owner"] for d in meta["databases"]}
doc = decls.get("postgresql") or {}
results, declared_names = [], set()
for ident in doc.get("identities", []):
name, owner = ident["name"], ident.get("owner", doc.get("owner"))
declared_names.add(name)
role, findings = roles.get(name), []
if role is None:
findings.append(finding(name, owner, "unknown", "observed", "role not in the export"))
else:
for flag in ("superuser", "create_role", "create_db", "replication", "bypass_rls"):
if role.get(flag):
findings.append(finding(name, owner, "non-conformant", "grants", f"role has {flag}"))
if ident.get("interactive_login") is False and role.get("member_of"):
findings.append(finding(name, owner, "unknown", "grants", f"member of {role['member_of']}"))
for grant in ident.get("grants", []):
if grant["mode"] == "ADMINISTER" and owners.get(grant["scope"]) != name:
findings.append(finding(name, owner, "non-conformant", "grants",
f"declared owner of {grant['scope']}, observed {owners.get(grant['scope'])}"))
verdict = ("non-conformant" if any(f["verdict"] == "non-conformant" for f in findings)
else "unknown" if findings else "conformant")
results.append({"service": "postgresql", "evidence": "postgresql", "identity": name, "owner": owner, "verdict": verdict,
"observed_as": [f"{meta['cluster']}/{name}"], "findings": findings})
undeclared = []
for name, role in sorted(roles.items()):
if name in declared_names:
continue
flags = [f for f in ("superuser", "create_role", "create_db", "replication", "bypass_rls") if role.get(f)]
owns = sorted(db for db, o in owners.items() if o == name)
undeclared.append(finding(f"{meta['cluster']}/{name}", "railiance-platform", "unknown", "SI-11",
f"undeclared login role (flags {flags or 'none'}, owns {owns or 'nothing'})"))
return results, undeclared
def run(openbao: pathlib.Path, postgresql: pathlib.Path, now: dt.datetime, max_age_days: int) -> dict:
decls = {d["service"]: d for d in (yaml.safe_load(p.read_text()) for p in sorted(DECLARATIONS.glob("*.yaml")))}
max_age = dt.timedelta(days=max_age_days)
ob_meta, ob_fresh = load_metadata(openbao, "openbao", now, max_age)
pg_meta, pg_fresh = load_metadata(postgresql, "postgresql", now, max_age)
ob_results, ob_undeclared = check_openbao(ob_meta, decls)
pg_results, pg_undeclared = check_postgresql(pg_meta, decls)
results = ob_results + pg_results
stale = [f["service"] for f in (ob_fresh, pg_fresh) if not f["fresh"]]
if stale: # stale evidence cannot establish conformance
for r in results:
if r["verdict"] == "conformant" and r["evidence"] in stale:
r["verdict"] = "unknown"
r["findings"].append(finding(r["identity"], r["owner"], "unknown", "freshness", "evidence is stale"))
counts: dict[str, int] = {}
for r in results:
counts[r["verdict"]] = counts.get(r["verdict"], 0) + 1
return {"as_of": now.isoformat(timespec="seconds").replace("+00:00", "Z"), "freshness": [ob_fresh, pg_fresh],
"summary": counts, "identities": results, "undeclared": ob_undeclared + pg_undeclared,
"undeclared_summary": {"non-conformant": sum(f["verdict"] == "non-conformant" for f in ob_undeclared + pg_undeclared),
"unknown": sum(f["verdict"] == "unknown" for f in ob_undeclared + pg_undeclared)}}
def main(argv: list[str] | None = None) -> int:
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("--openbao", type=pathlib.Path, required=True)
ap.add_argument("--postgresql", type=pathlib.Path, required=True)
ap.add_argument("--now", help="ISO time for the freshness bound (default: now)")
ap.add_argument("--max-age-days", type=int, default=7)
ap.add_argument("--json", action="store_true")
args = ap.parse_args(argv)
now = dt.datetime.fromisoformat(args.now.replace("Z", "+00:00")) if args.now else dt.datetime.now(dt.UTC)
report = run(args.openbao, args.postgresql, now, args.max_age_days)
if args.json:
print(json.dumps(report, indent=2, sort_keys=True))
else:
for f in report["freshness"]:
print(f"evidence {f['service']}: collected {f['collected_at']}, {f['age_hours']} h old, "
f"{'fresh' if f['fresh'] else 'STALE'} (bound {f['max_age_days']} d)")
for r in report["identities"]:
print(f"{r['service']}/{r['identity']}: {r['verdict']} [owner {r['owner']}]")
for f in r["findings"]:
print(f" - {f['verdict']} {f['rule']}: {f['detail']}")
for f in report["undeclared"]:
print(f"undeclared {f['identity']}: {f['verdict']} {f['rule']} [owner {f['owner']}]: {f['detail']}")
print(f"summary: {report['summary']}; undeclared: {report['undeclared_summary']} (as of {report['as_of']})")
nonconf = report["summary"].get("non-conformant", 0) + report["undeclared_summary"]["non-conformant"]
return 1 if nonconf else 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -0,0 +1,145 @@
"""Live conformance check against synthetic owner metadata (NK-WP-0054-T04)."""
from __future__ import annotations
import copy
import datetime as dt
import importlib.util
import json
import pathlib
import pytest
HERE = pathlib.Path(__file__).resolve().parent
spec = importlib.util.spec_from_file_location("observe", HERE.parent / "observe.py")
O = importlib.util.module_from_spec(spec)
spec.loader.exec_module(O)
NOW = dt.datetime(2026, 11, 7, 12, tzinfo=dt.UTC)
COLLECTED = "2026-11-05T12:00:00Z"
def oidc(name, policies, groups, aal2=True, ttl=900, max_ttl=900, rules=()):
claims = {"groups": groups} | ({"/assurance/level": "aal2"} if aal2 else {})
return {"mount": "netkingdom", "name": name, "role_type": "oidc", "token_policies": policies,
"bound_claims": claims, "token_ttl_seconds": ttl, "token_max_ttl_seconds": max_ttl,
"effective_complete": True, "effective_rules": list(rules)}
def kv(path, *caps):
return {"path": path, "capabilities": list(caps), "wildcard": False}
AGENT_RULES = [kv("platform/data/workloads/x", "deny"), kv("platform/metadata/workloads/x", "read")]
def openbao_doc():
roles = [
oidc("platform-admin", ["platform-admin", "operator-custody"], ["net-kingdom-admins"], ttl=3600, max_ttl=3600),
oidc("credential-broker-keycape-client-secret-generate",
["credential-broker-keycape-client-secret-generate-issuer"], ["net-kingdom-admins"]),
oidc("fluid-telegram-hall-of-helix-workload-kv", ["workload-kv-fluid-telegram-hall-of-helix-operator"],
["fluid-telegram-operators"], max_ttl=1800,
rules=[kv(f"platform/data/workloads/ft/{i}", "create", "read", "update") for i in range(4)]),
{"mount": "keycape-services", "name": "secrets-engine-openbao", "role_type": "jwt",
"token_policies": ["secrets-engine-login-self"], "bound_subject": "service:secrets-engine",
"token_ttl_seconds": 300, "token_max_ttl_seconds": 300, "effective_rules": []},
{"mount": "netkingdom", "name": "coding-agent", "role_type": "jwt",
"token_policies": ["agent-high-risk-boundary", "workload-kv-read-issue-core-runtime"],
"token_ttl_seconds": 900, "token_max_ttl_seconds": 900, "effective_complete": True,
"effective_rules": AGENT_RULES},
{"mount": "approle", "name": "coding-agent-railiance-platform", "role_type": "approle",
"token_policies": ["agent-high-risk-boundary", "workload-kv-read-issue-core-runtime"],
"token_ttl_seconds": 900, "token_max_ttl_seconds": 900, "effective_complete": True,
"effective_rules": AGENT_RULES},
{"mount": "kubernetes", "name": "net-kingdom-bind-executor-svc-authelia-write", "role_type": "kubernetes",
"bound_service_account_names": ["nk-bind-executor"], "bound_service_account_namespaces": ["sso"],
"token_policies": ["x"], "effective_rules": []},
]
return {"schema": O.SCHEMA, "service": "openbao", "value_safe": True, "source_kind": "live",
"collected_at": COLLECTED, "openbao": {"auth_mounts": [], "policies": [], "roles": roles}}
def pg_doc():
role = {"name": "privacyidea", "can_login": True, "superuser": False, "create_role": False, "create_db": False,
"replication": False, "bypass_rls": False, "member_of": []}
return {"schema": O.SCHEMA, "service": "postgresql", "value_safe": True, "source_kind": "live",
"collected_at": COLLECTED,
"postgresql": {"cluster": "pg", "roles": [role],
"databases": [{"name": "privacyidea_db", "owner": "privacyidea"}]}}
def run(tmp_path, ob=None, pg=None, now=NOW):
a, b = tmp_path / "ob.json", tmp_path / "pg.json"
a.write_text(json.dumps(ob or openbao_doc()))
b.write_text(json.dumps(pg or pg_doc()))
return O.run(a, b, now, 7)
def verdicts(report):
return {r["identity"]: r["verdict"] for r in report["identities"]}
def test_matching_metadata_is_conformant(tmp_path):
report = run(tmp_path)
assert set(verdicts(report).values()) == {"conformant"}, report
assert report["undeclared"] == []
def test_missing_aal2_binding_is_non_conformant(tmp_path):
ob = openbao_doc()
del ob["openbao"]["roles"][0]["bound_claims"]["/assurance/level"]
assert verdicts(run(tmp_path, ob=ob))["netkingdom-platform-admin"] == "non-conformant"
def test_undeclared_admin_path_without_aal2_is_non_conformant(tmp_path):
ob = openbao_doc()
alias = copy.deepcopy(ob["openbao"]["roles"][0])
alias.update(mount="keycape", token_policies=["platform-admin"])
del alias["bound_claims"]["/assurance/level"]
ob["openbao"]["roles"].append(alias)
report = run(tmp_path, ob=ob)
[f] = report["undeclared"]
assert f["identity"] == "keycape/platform-admin" and f["verdict"] == "non-conformant" and f["owner"]
assert O.main(["--openbao", str(tmp_path / "ob.json"), "--postgresql", str(tmp_path / "pg.json"),
"--now", "2026-11-07T12:00:00Z"]) == 1
def test_unbounded_max_ttl_is_unknown_not_a_pass(tmp_path):
ob = openbao_doc()
ob["openbao"]["roles"][0]["token_max_ttl_seconds"] = 0
assert verdicts(run(tmp_path, ob=ob))["netkingdom-platform-admin"] == "unknown"
def test_agent_data_read_is_non_conformant(tmp_path):
ob = openbao_doc()
ob["openbao"]["roles"][4]["effective_rules"] = [kv("platform/data/workloads/x", "read")]
assert verdicts(run(tmp_path, ob=ob))["coding-agent"] == "non-conformant"
def test_missing_role_is_unknown(tmp_path):
ob = openbao_doc()
ob["openbao"]["roles"].pop()
assert verdicts(run(tmp_path, ob=ob))["net-kingdom-bounded-executor"] == "unknown"
def test_postgres_privilege_flag_and_undeclared_role(tmp_path):
pg = pg_doc()
pg["postgresql"]["roles"][0]["create_db"] = True
pg["postgresql"]["roles"].append({"name": "postgres", "superuser": True, "member_of": []})
report = run(tmp_path, pg=pg)
assert verdicts(report)["privacyidea"] == "non-conformant"
assert [f["identity"] for f in report["undeclared"]] == ["pg/postgres"]
def test_stale_evidence_cannot_be_conformant(tmp_path):
report = run(tmp_path, now=NOW + dt.timedelta(days=30))
assert set(verdicts(report).values()) == {"unknown"}
assert not any(f["fresh"] for f in report["freshness"])
def test_refuses_metadata_not_marked_value_safe(tmp_path):
ob = openbao_doc()
ob["value_safe"] = False
with pytest.raises(SystemExit):
run(tmp_path, ob=ob)

View file

@ -195,7 +195,7 @@ status: wait
priority: medium
assignee: claude-code
depends_on: [NK-WP-0054-T03]
blocking_reason: "OpenBao and PostgreSQL live metadata delivered 2026-11-03 (railiance-platform b0b6c0c). Next: run the conformance check against it with the freshness bound on collected_at; KeyCape live view (KEY-WP-0045-T02) is still the founder's."
blocking_reason: "First run 2026-11-07 (OpenBao, PostgreSQL): keycape/platform-admin alias is an undeclared admin login without aal2, routed to railiance-platform. Remaining: owner answers on that and the 14 undeclared roles, a fresh OpenBao export after the 2026-11-04 go-live, the founder's KeyCape live view (KEY-WP-0045-T02), then the posture-feedback feed."
state_hub_task_id: "c8db4607-9306-572b-a3cb-c35ca5989ec7"
```
@ -231,6 +231,15 @@ The founder's live export commands are in RPF-WP-0067-T03.
net-kingdom-pg, 5 roles, 4 databases), schema `railiance-platform.service-identity-metadata.v1`. Collected by the
founder; no Secret read, secret-shape guard passed. Apply the evidence-freshness bound to `collected_at`.
**2026-11-07 first run:** `tools/service-identity/observe.py` (`make service-identity-observe`, 9 tests) compares
the declarations against that metadata, with a 7-day freshness bound.
[Report](../docs/evidence/2026-11-07-nk-wp-0054-t04-conformance/README.md): declared identities 5 conformant,
2 unknown (platform-admin has no role max TTL; the executor's OpenBao role postdates the export). Undeclared:
**1 non-conformant**, `keycape/platform-admin`, the compatibility alias mount. It grants `platform-admin` without
an aal2 binding and keeps the retired `bao.coulomb.social` callback. There are also 14 unknown: nine human KV-read roles without
aal2, one ESO role sharing a declared policy, and four PostgreSQL roles. All owners are railiance-platform; the
findings are routed to them. KeyCape, LLDAP, Authelia and privacyIDEA are not covered yet.
## Split the directory bind from the human admin
```task