diff --git a/INTENT.md b/INTENT.md index 48544c9..a63f704 100644 --- a/INTENT.md +++ b/INTENT.md @@ -1,3 +1,11 @@ +--- +layer: Staff +role: null +security_layer_declaration: layer.yaml +standard_version: "0.7" +declared_at: "2026-09-09" +--- + # INTENT > This file captures **why this repository exists**, @@ -137,3 +145,40 @@ This layer is expected to evolve toward: ## Guiding Question > **How can the stateful foundation an entire landscape relies on be made dependable, secure, and recoverable — while remaining free to evolve underneath everything built on it?** + +--- + +## Security layer declaration + +Declared in our own voice, per §2 of the NetKingdom Security Layer Model v0.7. +The machine-readable form is [layer.yaml](layer.yaml). + +**railiance-platform is Staff.** What this repository produces is +specifications, decisions, workplans, credential-change requests and acceptance +evidence. It is not deterministic in the §1 sense: the work here is judgment +about what should be provisioned, for whom, under what custody, and that is +Staff work however much of it lands as YAML. + +The obvious objection is that we operate OpenBao and the shared databases, and +operating a store looks like producing state. §4 answers it directly and we +accept the answer: there is deliberately no operator-of-third-party-Tooling +shape, because a clean operator shape would turn a tracked gap into a permanent +allowance. Someone must run OpenBao. That we are the someone is a declared gap +whose review keeps returning, not a claim to the Tooling layer. + +**We are PEP-shaped** (§5, §6.4). Applying a policy, seeding or rotating a +workload credential, and activating a delivery lane are protected side effects +caused on this side. Our unreachable-engine stance map is **not yet published**, +and that is recorded as an open obligation in `layer.yaml` rather than left +silent. Every protected side effect here is currently attended and +founder-gated, so no unattended branch has had to decide without a reachable +engine — which is why the gap has not bitten, and is not a substitute for the +map. + +**Conformance state: declared gap.** Six direct Tooling contacts, one of them a +§5.2 conduit and one a §5.1 read-only diagnostic; the rest are §5.3 gaps with +named intended owners and review dates. The uncatalogued contacts — the cluster, +State Hub, the forge and the backup destinations — are listed too, so the check +is total rather than selectively silent. + +If any of this is wrong, contest it. A correction is worth more than a label. diff --git a/SCOPE.md b/SCOPE.md index 4e09777..b693503 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -22,7 +22,7 @@ or authorization authority. See [ADR-0001](docs/adr/ADR-0001-s3-platform-service | Service | Implemented / evidenced | Limit and authority | | --- | --- | --- | | `apps-pg` | CNPG source, per-consumer database ACLs, 20-connection allowances, 15-second timeouts; daily backup, continuous WAL and 30-day retention; 56-second scratch restore and 14/14 boundary probes on 2026-08-20 | One instance; restart recovery, no HA or measured latency SLO. Ceiling 3, overflow `apps-pg-2` provisionable but demand-gated. S3 owns this CR and its controls. | -| `platform-pg` | Package-operated CNPG service, governed dynamic credentials, backup/restore evidence, Core Hub admission and private shadow evidence | `rapp-postgres` owns CR/provisioning/restore. S3 owns custody, placement policy and consumer disclosure. Ceiling 4 declarations, counting the probe; platform-pg is 4/4 and the deployed platform-pg-2 hosts sbom-nexus at 1/4. One instance is not HA. | +| `platform-pg` | Package-operated CNPG service, governed dynamic credentials, backup/restore evidence, Core Hub admission and private shadow evidence | `rapp-postgres` owns CR/provisioning/restore. S3 owns custody, placement policy and consumer disclosure. Ceiling 4 declarations, counting the probe; platform-pg is 4/4 and the deployed platform-pg-2 hosts sbom-nexus and canned-prompts at 2/4. One instance is not HA. | | OpenBao and credential delivery | CCR validation, policy/grant boundaries, approved apply interfaces, runtime KV/ESO and short-lived database lanes, ephemeral custody lifecycle | Package assets belong to `rapp-openbao`; custody/policy remains here. Existing helpers are not authority to mint arbitrary credentials. Operator-only access cutover and incident residuals remain open. | | Backup storage consumption | Governed off-cluster S3 backup destination and credential lane used by CNPG; encrypted upload tooling for other backup consumers | Resource procurement belongs to `resource-control`/`reef-storage`. This does not establish a general S3 service or deployed MinIO. One successful restore does not establish a recurring recovery guarantee. | | Cache / general object storage / messaging | Valkey deployment is gated; no supported consumer interface or deployed shared broker/general in-cluster object store is established by this review | Demand, reuse, ownership, service contract and recovery design must precede deployment. No engine/provider choice is made here. | diff --git a/assurance/placement-owners.json b/assurance/placement-owners.json index 96e723d..12e5d85 100644 --- a/assurance/placement-owners.json +++ b/assurance/placement-owners.json @@ -1,9 +1,45 @@ { - "vergabe": {"owner": "vergabe-teilnahme", "cell": "apps-pg", "service_class": "interactive", "evidence": "docs/evidence/RPF-WP-0019-isolation-2026-08-20.md"}, - "coulomb_social": {"owner": "coulomb-social", "cell": "apps-pg", "service_class": "interactive", "evidence": "docs/evidence/RPF-WP-0019-isolation-2026-08-20.md"}, - "audit-core": {"owner": "audit-core", "cell": "platform-pg", "evidence": "rapp-postgres/consumers/audit-core.yaml"}, - "tenant-engine": {"owner": "tenant-engine", "cell": "platform-pg", "evidence": "rapp-postgres/docs/evidence/tenant-engine-postgres-cutover-2026-08-21.md"}, - "core-hub": {"owner": "core-hub", "cell": "platform-pg", "evidence": "docs/evidence/core-hub-postgres-capacity-admission-2026-08-20.md"}, - "isolation-probe": {"owner": "rapp-postgres", "cell": "platform-pg", "evidence": "rapp-postgres/consumers/isolation-probe.yaml"}, - "sbom-nexus": {"owner": "sbom-nexus", "cell": "platform-pg-2", "evidence": "rapp-postgres/docs/evidence/RAPP-POSTGRES-WP-0005-T04-boundary-restore-2026-08-22.md"} + "vergabe": { + "owner": "vergabe-teilnahme", + "cell": "apps-pg", + "service_class": "interactive", + "evidence": "docs/evidence/RPF-WP-0019-isolation-2026-08-20.md" + }, + "coulomb_social": { + "owner": "coulomb-social", + "cell": "apps-pg", + "service_class": "interactive", + "evidence": "docs/evidence/RPF-WP-0019-isolation-2026-08-20.md" + }, + "audit-core": { + "owner": "audit-core", + "cell": "platform-pg", + "evidence": "rapp-postgres/consumers/audit-core.yaml" + }, + "tenant-engine": { + "owner": "tenant-engine", + "cell": "platform-pg", + "evidence": "rapp-postgres/docs/evidence/tenant-engine-postgres-cutover-2026-08-21.md" + }, + "core-hub": { + "owner": "core-hub", + "cell": "platform-pg", + "evidence": "docs/evidence/core-hub-postgres-capacity-admission-2026-08-20.md" + }, + "isolation-probe": { + "owner": "rapp-postgres", + "cell": "platform-pg", + "evidence": "rapp-postgres/consumers/isolation-probe.yaml" + }, + "sbom-nexus": { + "owner": "sbom-nexus", + "cell": "platform-pg-2", + "evidence": "rapp-postgres/docs/evidence/RAPP-POSTGRES-WP-0005-T04-boundary-restore-2026-08-22.md" + }, + "canned-prompts": { + "owner": "canned-prompts", + "cell": "platform-pg-2", + "service_class": "interactive", + "evidence": "rapp-postgres/docs/evidence/RAPP-POSTGRES-WP-0006-T03-boundary-2026-09-08.md" + } } diff --git a/credential-change-requests/CCR-2026-0020-approval-engine-operator-client-read.yaml b/credential-change-requests/CCR-2026-0020-approval-engine-operator-client-read.yaml index db25601..d4ae81f 100644 --- a/credential-change-requests/CCR-2026-0020-approval-engine-operator-client-read.yaml +++ b/credential-change-requests/CCR-2026-0020-approval-engine-operator-client-read.yaml @@ -48,6 +48,22 @@ review: reader therefore cannot be named here without an owner decision, and naming one anyway would hand the widest approval scope to a guessed identity. Held in_flight deliberately. + - at: '2026-09-09' + reviewer: key-cape (issuer view, not a decision) + decision: presenting_actor_shapes_offered + comment: >- + KeyCape notes the registration carries both approval:create and + approval:approve, so a single presenter can create an entry and then + approve it. That is a separation-of-duties property of whoever holds the + credential, not a defect in the token: KeyCape issues exactly the grants + approval-engine requested. Two shapes are supportable -- two registrations + with disjoint create and approve grants, enforced at issuance and + available today; or one registration with the holder constrained by + custody, which is where it sits now. Which is right is approval-engine's + call and the doctrine question is gate-house's. Recorded in key-cape + docs/approval-engine-provisioning-request.yaml under + presenting_actor_note. This does not name an actor and does not unblock + this request. target: domain: financials tenant: platform diff --git a/docs/openbao-open-questions-session.md b/docs/openbao-open-questions-session.md index dc41d76..23a2153 100644 --- a/docs/openbao-open-questions-session.md +++ b/docs/openbao-open-questions-session.md @@ -14,7 +14,7 @@ establish, and each has been answered so far with "I will not guess": | Question | Thread | What is undecidable from repo source | | --- | --- | --- | | Q1 | secrets-engine `546403e4` | Whether the legacy path `secret/coulomb/whynot-design/npm/publish` still exists and holds a real token | -| Q2 | secrets-engine `546403e4` | Which field backs the authoritative npm lane. My CCR says `NPM_AUTH_TOKEN`; secrets-engine says `npm_token`. Field names are not visible in KV v2 metadata | +| Q2 | secrets-engine `546403e4`, ops-warden `b6212006` | Which field backs the authoritative npm lane. Two dated live receipts here name `NPM_AUTH_TOKEN`; secrets-engine says `npm_token` and ops-warden has already changed their catalog to it. Both can be true if the path carries both fields. Field names are not visible in KV v2 metadata | | Q3 | internal | The 2026-09-09 activation receipt records `policy_applied: false` for both KeyCape approval lanes while `role_applied: true`. Reads verified, so the policies must exist — but existence and repo agreement were never confirmed | | Q4 | CCR-2026-0019 | Which netkingdom OIDC roles and bound group claims already exist, as input to the operator group claim that lane is missing | | Q5 | risk-nexus `ee702ac9` | Which fields the governed backup lane actually carries, as RPF-WP-0029-T02 context | @@ -62,12 +62,17 @@ the constraint risk-nexus set. ## What each answer unblocks -- **Q1 + Q2 → secrets-engine.** A pointer plus a field name closes their custody - question. If Q2 returns `npm_token`, my CCR-2026-0001 is wrong about the field - and I correct it; if it returns `NPM_AUTH_TOKEN`, their catalog entry is the one - that moves, as a reviewed lane change carrying its own approval. If Q1 finds a - populated legacy path, that is a stale duplicate holding a real token and - becomes its own disposition item — do not delete it inside this session. +- **Q1 + Q2 → secrets-engine and ops-warden.** Q2 is now the more urgent of the + two. ops-warden changed their catalog and playbook from `NPM_AUTH_TOKEN` to + `npm_token` on secrets-engine's statement, while this repository holds two dated + live receipts — including an attended founder fetch that exited zero — naming + `NPM_AUTH_TOKEN` as the field. If those receipts are right, that change points + the front door at a field that does not exist and the lane breaks on next use. + Q2 enumerates the real field names: `npm_token` only, `NPM_AUTH_TOKEN` only, or + both, and each outcome names exactly one side that has to move, as a reviewed + lane change carrying its own approval. If Q1 finds a populated legacy path, that + is a stale duplicate holding a real token and becomes its own disposition item — + do not delete it inside this session. - **Q3 → internal.** Either the policies match the repo, and the activation's `policy_applied: false` meant "no change needed", or they drift and that is a finding to fix under RPF-WP-0035 before anyone treats the verifier lanes as diff --git a/docs/workload-kv-access-lanes.md b/docs/workload-kv-access-lanes.md index 4ee0c80..973b488 100644 --- a/docs/workload-kv-access-lanes.md +++ b/docs/workload-kv-access-lanes.md @@ -64,6 +64,20 @@ caller-scoped lane. The sibling lanes `issue-core-ingestion-api-key` and `openrouter-llm-connect` remain draft and are tracked separately by `RAILIANCE-WP-0009` and `RAILIANCE-WP-0010`. +> **Field name contested, 2026-09-09 — do not change this on either side yet.** +> secrets-engine states the KV field is `npm_token` and that `NPM_AUTH_TOKEN` is +> only the environment variable their publication-scope policy injects; +> ops-warden has already corrected their catalog and playbook on that statement. +> This repository holds two dated live receipts naming `NPM_AUTH_TOKEN` as the +> OpenBao **field**, in `CCR-2026-0001`: a field-presence check on +> 2026-06-28T10:37:42Z, and an attended fetch by the founder on +> 2026-06-28T15:22:29Z that exited successfully with output to `/dev/null` — a +> `-field=` fetch that exits zero means that field exists. Both records can be +> true if the path carries **both** fields. Q2 of the attended session +> (`docs/openbao-open-questions-session.md`) enumerates the actual field names +> and settles it. Until then this table stays as written, because it is the side +> backed by receipts. + The fetch command returns the secret value to the authenticated caller. Run it only in an attended shell or through a process that consumes the value without logging it. diff --git a/layer.yaml b/layer.yaml new file mode 100644 index 0000000..d9ef9b6 --- /dev/null +++ b/layer.yaml @@ -0,0 +1,191 @@ +# railiance-platform — NetKingdom security layer declaration +# +# Framework: net-kingdom/canon/standards/security-layer-model_v0.7.md +# Companion: net-kingdom/SECURITY-COMPANION.md +# Declared: INTENT.md frontmatter and body prose (§2 — own voice, own repo) +# Form: adapted from ops-warden's reference layer.yaml, offered estate-wide. +# layer.yaml is a convention, not a schema; gate-house confirmed on +# 2026-09-09 that v0.7 defines no shape for this file. Following the +# practice because §5.3 requires gap records to be machine-readable. +# +# Conformance rule inherited from tenancy.yaml: accuracy, not altitude. A +# declared gap is TRACKED NON-CONFORMANCE (§11), never a claim of conformance. + +schema_version: "0.1" +framework: netkingdom-security-layer-model +standard_version: "0.7" +repository: railiance-platform +layer: staff +role: null +declared_by: INTENT.md +declared_at: "2026-09-09" + +# §8 self-assessed state. Declared gaps below; nothing here is claimed as +# conforming, and no contact is omitted to improve the label. +conformance_state: declared-gap + +# §5 / §6.4 — railiance-platform IS PEP-shaped. Applying an OpenBao policy, +# seeding or rotating a workload credential, and activating a delivery lane are +# protected side effects caused on this side. Being PEP-shaped does not change +# the Staff layer (§5 preamble). +pep_shaped: true +pep_stance: null # NOT YET PUBLISHED — see gaps.pep-stance-map below. + +# Every direct contact with a Tooling-layer system (§4: key-cape, OpenBao), +# grouped by capability rather than by file. 28 files in this repository invoke +# `bao`; they exercise the five capabilities below. +tooling_contacts: + + - id: openbao-attended-operator-login + shape: "5.2" # conduit + module: scripts/openbao-attended-exec.py + tooling: OpenBao + operation: "exec into `warden access openbao-platform-admin-login --exec -- `" + write: false + capability: "Run a reviewed owner command under the founder's own attended OIDC identity" + supplied_authority: none + note: >- + The §5.2 test holds: this repository presents no credential of its own, + cannot widen what the founder could already do, and every action + reconstructs as the founder. It supplies only a WSL browser launcher when + the host lacks one. ops-warden owns the envelope and self-revokes the + temporary token helper. + + - id: openbao-policy-and-auth-apply + shape: "5.3" # declared engine gap + module: scripts/openbao-apply-workload-kv-lanes.sh, scripts/openbao-apply-token-grants.py, scripts/credential-change.py + tooling: OpenBao + operation: "policy write; write auth//role/" + write: true + capability: "Apply the exact-path read policy and bound auth role for a reviewed credential lane" + intended_owner: secrets-engine + blocked_on: >- + secrets-engine owns credential abstraction, custody and lifecycle, and is + the layer this belongs in, but exposes no policy/role application surface. + SECRETS-WP-0006 is cutting the first native lanes; until a lane is served + there, application stays here behind an attended founder session. + review: "2026-12-09" + + - id: openbao-kv-custody-write + shape: "5.3" + module: scripts/keycape_approval_custody.py, scripts/state_hub_preflight_lane.py, scripts/provision-policy-nexus-forgejo-source.py + tooling: OpenBao + operation: "kv put / kv patch with compare-and-set, value on stdin, never argv" + write: true + capability: "Seed or rotate a workload credential under attended founder custody" + intended_owner: secrets-engine + blocked_on: >- + Same gap as above. No engine exposes an attended provisioning or rotation + surface for a value the founder holds. Every write here is CAS-guarded, + attended, and emits a mode-0600 metadata receipt with no value. + review: "2026-12-09" + + - id: openbao-read-diagnostic + shape: "5.1" # read-only diagnostic + module: scripts/keycape-approval-custody-preflight.py, scripts/openbao_open_questions_session.py, scripts/openbao-verify-token-grants.py + tooling: OpenBao + operation: "token capabilities; policy read; kv metadata get; kv get (field names only)" + write: false + capability: "Produce acceptance and drift evidence for a lane without mutating it" + note: >- + No engine exposes these reads. Values never reach argv, disk, stdout or a + receipt; where a data read is unavoidable to enumerate field names, only + sorted key names leave the process. This is a gap to close, not a licence. + review: "2026-12-09" + + - id: openbao-service-operation + shape: "5.3" + module: Makefile (openbao-deploy, openbao-overlay-apply), helm/openbao-values.yaml + tooling: OpenBao + operation: "helm upgrade of the OpenBao release; unseal and snapshot procedures" + write: true + capability: "Operate the OpenBao instance itself — deployment, listener, unseal, snapshot" + intended_owner: unassigned + blocked_on: >- + §4 is explicit that there is no operator-of-third-party-Tooling shape: + someone must run OpenBao and that stays a declared gap whose review keeps + returning. Recorded here so the absence is visible rather than assumed + benign. No engine is expected to take this; the review is the point. + review: "2026-12-09" + + - id: keycape-client-secret-custody + shape: "5.3" + module: scripts/keycape_approval_custody.py + tooling: key-cape + operation: "read/patch Secret sso/keycape-config and the client-secret Secrets it references" + write: true + capability: "Deliver a KeyCape client secret into the KeyCape runtime and verify the delivery matches custody" + intended_owner: secrets-engine + blocked_on: >- + KeyCape owns registration, issuance and disablement and deliberately ships + no command for custody writes (their position, which we accept). No engine + fronts the delivery half. Exercised once, attended, on 2026-09-09 under + CCR-2026-0017/0018. + review: "2026-12-09" + +# Contacts recorded so the check is total rather than selectively silent. §4 +# asks for uncatalogued infrastructure to be listed anyway. +non_tooling_clients: + + - target: Kubernetes / CloudNative-PG / External Secrets (railiance01) + module: Makefile (db-*, apps-pg-*), argocd/platform-addons/ + rationale: >- + Not catalogued as Tooling in §4. This is the substrate axis under §9 + (reef-*), and §9 states the axis-to-layer mapping is unsettled (statute + §20.3). Listed rather than mapped; we are not assuming a mapping. + note: >- + This is the largest single contact by volume and the one most likely to be + recatalogued. If §4 grows to cover the cluster, these become §5.3 entries + and this file changes shape, not just content. + + - target: State Hub + module: session protocol in .claude/rules/, scripts/*state_hub* + rationale: "Not catalogued in §4; the companion names it explicitly as outside the rule." + note: "Read model. The only sanctioned write is resolving a decision; progress events reflect work already done." + + - target: Forgejo, Scaleway object storage, Nextcloud + module: scripts/forgejo_package_prune.py, lib/railiance-backup-common.sh + rationale: "Not catalogued in §4. Backup destinations and the forge are consumed, not operated as security Tooling." + + - target: flex-auth / access-engine, approval-engine + module: docs/credential-lane-designs/, CCR review flow + rationale: "Engine APIs — §5 permits it; this is the shape §5 prescribes. Decisions and approvals are consumed as claims, never rendered or cached here." + +# §5 obligations this repository owes and has not met. Declared rather than +# omitted; the standard ranks a declared gap above silence (§8). +gaps: + + - id: pep-stance-map + obligation: "§5 obligation 3 — publish a total unreachable-engine stance map" + state: not-published + intended_path: pep-stance.yaml + blocked_on: >- + The map must be total over its axis with no implicit default, and this + repository does not own the zone axis it would enumerate. Every protected + side effect here is currently attended and founder-gated, so no unattended + branch has yet had to decide without a reachable engine — that is the + reason the gap has not bitten, not a substitute for the map. + build_to: "0.8" + note: >- + gate-house advised on 2026-09-09 to build to v0.8's tightened obligation 3 + rather than v0.7's and skip a migration: unknown must resolve to + fail_closed, the map must enumerate its axis rather than lean on a + catch-all, and the published-equals-shipped test is being promoted from + SHOULD to MUST. + review: "2026-10-09" + + - id: statute-13-1-registration + obligation: "§5 obligation 3 — register the stance map in statute §13.1" + state: not-registered + blocked_on: "Depends on pep-stance-map. A map only we can read is not published." + review: "2026-10-09" + + - id: conformance-check-script + obligation: "Mechanical check that this declaration matches the repository" + state: not-implemented + blocked_on: >- + ops-warden's scripts/check_layer_conformance.py and + tests/test_layer_conformance.py are the part that makes this file more + than a claim. Until an equivalent runs here, every row above is an + assertion and is marked as such by this gap rather than by silence. + review: "2026-10-09" diff --git a/tests/test_service_assurance.py b/tests/test_service_assurance.py index c868351..01db559 100644 --- a/tests/test_service_assurance.py +++ b/tests/test_service_assurance.py @@ -100,7 +100,9 @@ class AdmissionTests(unittest.TestCase): def test_current_occupancy_and_real_overflow(self): result = self.check()['cells'] self.assertEqual(len(result['platform-pg']['consumers']), 4) - self.assertEqual(result['platform-pg-2']['consumers'], ['sbom-nexus']) + # platform-pg-2 took canned-prompts on 2026-09-08 (RAPP-POSTGRES-WP-0006-T03), + # so the overflow cell is 2/4 rather than 1/4. + self.assertEqual(result['platform-pg-2']['consumers'], ['canned-prompts', 'sbom-nexus']) self.assertEqual(result['apps-pg-2']['consumers'], []) def test_unowned_consumer_fails(self): diff --git a/workplans/RPF-WP-0035-credential-lane-implementation.md b/workplans/RPF-WP-0035-credential-lane-implementation.md index 8a2202d..15f24eb 100644 --- a/workplans/RPF-WP-0035-credential-lane-implementation.md +++ b/workplans/RPF-WP-0035-credential-lane-implementation.md @@ -323,6 +323,23 @@ into this client-identity grant. ## Dependency review — 2026-09-06 +### 2026-09-09 tenant question resolved — no change needed + +The `tenant:coulomb` / `tenant:platform` discrepancy raised against T02 is not a +discrepancy. KeyCape confirms decision `5ed3fb35-eca9-413a-82b9-95171ba85bf6` +binds the *approval chain* to the landlord zone and was applied to exactly two +registrations, `secrets-engine-approval` and `approval-engine-operator`. +`codex-railiance-platform` is not in that chain and correctly stays +`tenant:coulomb`, as do `secrets-engine-openbao` and the human directory default. +KEY-WP-0013-T04 records this and their +`TestServiceRegistrationTenantsAreExactPerDecision` pins every reviewed client's +tenant against the real fixture, so a reintroduced alias fails their build. + +`docs/credential-lane-designs/secrets-engine-service-jwt.md` already says +`tenant:coulomb` and is therefore correct as written. Bind the exact role to +`tenant:coulomb`. Do not "correct" it to `tenant:platform` later; that would +break the issued claim. + SECRETS-WP-0008-T02 now records the local PIP claim/validation join implemented and tested. Its remaining gate is the unreachable approval-engine claim endpoint and access-engine Check (SECRETS-WP-0007-T04). Do not carry forward the old local