From 1945e166857ed61c9c668fad5861125f56b9b1c2 Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 29 Aug 2026 11:57:47 +0200 Subject: [PATCH] Declare Engine/Lifecycle against security layer model v0.7 Replace the gate-house review note with this repository's own declaration: INTENT.md frontmatter, layer.yaml, and a published PEP stance map. SCOPE.md and agent boundary docs now match that layer. The review under history/ identifies the implementation remainder; SECRETS-WP-0008 is the follow-on workplan. SECRETS-IN-0001 is closed. The layer is not contested. Catalog "custody" is a finding: OpenBao owns custody, this engine owns the lifecycle API over it. SSH-CA signing is accepted as a proposed engine API and declined as a Staff lane. Assistant: grok Assistant-Session: 01a04cea-cb33-7c63-bad7-c1b0f9f0076b --- .claude/rules/repo-boundary.md | 11 +- .claude/rules/repo-identity.md | 2 +- AGENTS.md | 19 +- INTENT.md | 206 +++++++++++----- README.md | 15 +- SCOPE.md | 103 ++++++-- docs/netkingdom-security-infrastructure.md | 8 + ...6-08-29-layer-model-scope-intent-review.md | 163 +++++++++++++ intakes/intakes.md | 12 +- layer.yaml | 144 ++++++++++++ pep-stance.yaml | 85 +++++++ scripts/check_layer_conformance.py | 178 ++++++++++++++ tests/test_layer_conformance.py | 117 +++++++++ ...-0008-layer-model-lifecycle-conformance.md | 222 ++++++++++++++++++ 14 files changed, 1177 insertions(+), 108 deletions(-) create mode 100644 history/2026-08-29-layer-model-scope-intent-review.md create mode 100644 layer.yaml create mode 100644 pep-stance.yaml create mode 100644 scripts/check_layer_conformance.py create mode 100644 tests/test_layer_conformance.py create mode 100644 workplans/SECRETS-WP-0008-layer-model-lifecycle-conformance.md diff --git a/.claude/rules/repo-boundary.md b/.claude/rules/repo-boundary.md index 9b4eeda..78f4b65 100644 --- a/.claude/rules/repo-boundary.md +++ b/.claude/rules/repo-boundary.md @@ -1,12 +1,15 @@ ## Repo boundary -This repo owns the **secrets-engine** workflow/interaction layer only. It does -not own: +This repo owns the **secrets-engine** Lifecycle engine only (NetKingdom +security layer model v0.7: Engine / Lifecycle). It does not own: - Secret custody, policy, lease, and audit backend → OpenBao / railiance-platform -- SSH certificate issuance → ops-warden (`warden sign`) +- SSH certificate issuance (Staff PEP) → ops-warden (`warden sign`) - Tunnels and remote transport → ops-bridge -- Authorization decisions → flex-auth +- Authorization decisions → access-engine (`flex-auth`) +- Approval objects → approval-engine +- Evidence custody and integrity → audit-core - Identity and claim lifecycle → user-engine / key-cape +- Security doctrine and the layer model → gate-house / net-kingdom canon - Cross-system security boundary doc → net-kingdom/docs/ - Request history and progress index → State Hub (read model) diff --git a/.claude/rules/repo-identity.md b/.claude/rules/repo-identity.md index df32a41..2737de0 100644 --- a/.claude/rules/repo-identity.md +++ b/.claude/rules/repo-identity.md @@ -1,4 +1,4 @@ -**Purpose:** secrets-engine is a headless, multi-application, multi-tenant secrets workflow and automation layer that orchestrates approved secret custody, delivery, and lifecycle work across build, test, and production stages, with OpenBao as the initial enforcement backend. +**Purpose:** secrets-engine is the Lifecycle engine for approved credential work: a deterministic API over OpenBao that orchestrates cataloged custody, delivery, and lifecycle actions across build, test, and production. It does not render authorization decisions. **Domain:** infotech **Repo slug:** secrets-engine diff --git a/AGENTS.md b/AGENTS.md index 4694a14..85f170c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## Repo Identity -**Purpose:** secrets-engine is a headless, multi-application, multi-tenant secrets workflow and automation layer that orchestrates approved secret custody, delivery, and lifecycle work across build, test, and production stages, with OpenBao as the initial enforcement backend. +**Purpose:** secrets-engine is the Lifecycle engine for approved credential work: a deterministic API over OpenBao that orchestrates cataloged custody, delivery, and lifecycle actions across build, test, and production. It does not render authorization decisions. **Domain:** infotech **Repo slug:** secrets-engine @@ -166,17 +166,24 @@ Read `docs/netkingdom-security-infrastructure.md` before changing secret catalogs, OpenBao policies, auth roles, delivery modes, or ops-warden routing. Core rules: -- OpenBao is the custody, policy, lease, and audit backend. -- secrets-engine owns workflow, catalog, decision checks, safe delivery, and - non-secret evidence. -- flex-auth decides authorization; user-engine/key-cape own identity and claims. +- Layer: Engine / Lifecycle (`INTENT.md`, `layer.yaml`). PEP stance: + `pep-stance.yaml`. +- OpenBao is the custody, policy, lease, and audit backend (Tooling). +- secrets-engine is the Lifecycle API over that backend: catalog, decision + consumption, safe delivery, and non-secret evidence. It does not render + authorization decisions. +- `access-engine` (`flex-auth`) decides authorization; user-engine/key-cape own + identity and claims; `audit-core` owns evidence custody. - ops-warden routes non-SSH credential requests here and must not vend secrets. + SSH certificate issuance remains ops-warden's Staff PEP. - ops-bridge may consume scoped delivery for remote execution but must not store secret material. - info-tech-canon is the source for canonical terminology and stage/policy - concepts as they harden. + concepts as they harden. gate-house owns the layer model. - Never write raw secret values to Git, State Hub, chat, prompts, workplans, or normal logs. +- Never claim local evidence or a missing record proves occurrence or + non-occurrence. diff --git a/INTENT.md b/INTENT.md index 12bda6b..b9af15c 100644 --- a/INTENT.md +++ b/INTENT.md @@ -1,40 +1,78 @@ +--- +layer: Engine +role: Lifecycle +standard: net-kingdom/canon/standards/security-layer-model_v0.7.md +companion: net-kingdom/SECURITY-COMPANION.md +declaration: layer.yaml +pep_stance: pep-stance.yaml +declared_at: "2026-08-29" +--- + # INTENT -> **NetKingdom layering review — 2026-08-28.** This repository's role was reviewed -> against the NetKingdom IT-security layer model: **Taxonomy → Tooling → Engines → -> Staff**, layered by determinism and by the kind of artifact each layer produces. -> Findings and the argument behind them: -> `gate-house/history/2026-08-28-security-layer-model-and-gate-house-recut.md`. -> The model is `net-kingdom/canon/standards/security-layer-model_v0.1.md` (proposed), -> ratified by `gate-house/decisions/decisions.md` GH-DEC-2026-001. +> **Layer: Engine. Role: Lifecycle.** This is this repository's own declaration +> under the accepted NetKingdom Security Layer Model +> (`net-kingdom/canon/standards/security-layer-model_v0.7.md`) and its working +> companion (`net-kingdom/SECURITY-COMPANION.md`). The machine-readable form +> required by §11 is `layer.yaml`. A layer stated about this repository by +> another repository is not a declaration; this file, in this repository's +> voice, is. > -> The layer rule that binds every repository: **Staff never touches tooling -> directly. It acts only through engine APIs.** +> secrets-engine is a **deterministic API over OpenBao** for one modeled +> concept: the cataloged credential lane and its lifecycle. The same +> authoritative inputs — catalog entry, access-engine decision record, and +> OpenBao state — must yield the same plan, apply, delivery, and lifecycle +> result. That is why this repository is an Engine, not Staff, even though +> operators and agents invoke it at runtime. > -> **This repository is Engine — deterministic API for a modeled concept.** Add the layer label. Credential custody, materialization, and lifecycle remain secrets-engine's. Two boundaries to make explicit: the decision that authority exists is access-engine's, and the doctrine governing when and how authority may be materialized is gate-house's. secrets-engine implements; it does not author the rules or render the decision. +> **It is not a decision point.** `access-engine` (today `flex-auth`) renders +> whether authority exists. gate-house authors the doctrine that may reach that +> decision only as a request claim or a versioned policy rule. OpenBao is +> Tooling: custody, policy enforcement, leases, and backend audit. This engine +> materializes an already-rendered decision into scoped OpenBao work. A +> credential is an artifact of authority, never its source. > -> *This note records what should change. The body below is not yet adapted.* +> **It is PEP-shaped.** Apply, provision, exec delivery, handoff, suspend, +> deactivate, and destroy cause protected side effects, so the four PEP +> obligations in §6.4 bind: no side effect without a decision record or a +> recorded stance; no replay of a verdict outside its binding and lifetime; +> a published unreachable-engine stance at `pep-stance.yaml`; reconstructability +> bounded by §9.6. Being PEP-shaped does not change the layer. +> +> **Proposed surfaces, not yet assented as shipped APIs.** ops-warden has +> declared its SSH-CA signing write as an engine gap with intended owner +> secrets-engine. kings-guard has proposed secret-use evidence against this +> engine. This repository **accepts both as proposed Lifecycle surfaces** and +> **declines operational SSH-lane stewardship**: `ops-warden` remains the Staff +> PEP that requests a certificate after a decision. What belongs here is the +> engine API those Staff repositories should call instead of OpenBao. Until +> those surfaces exist, the gaps stay proposed, not silently taken. +> +> **Finding on the §4 catalog cell.** The catalog row currently says this +> repository owns "credential abstraction, custody, lifecycle". Custody belongs +> to OpenBao. This engine owns the lifecycle API *over* that custody. The +> wording should not assign a Tooling property to an Engine. > This file captures why this repository exists, the direction it is moving > toward, and the kind of system it is meant to become. It is intentionally > aspirational and stable, not a description of current implementation. +> See `SCOPE.md` for what is implemented today. ## One-liner -**secrets-engine is the secure interaction and automation layer for approved -secret custody, scoped OpenBao capability handoff, delivery, and lifecycle work -across build, test, and production, with OpenBao as the initial enforcement -backend.** +**secrets-engine is the Lifecycle engine for approved credential work: a +deterministic API that turns an access-engine decision into scoped OpenBao +custody, delivery, and lifecycle actions across build, test, and production.** ## Why This Exists Secret management is too important to be handled through scattered manual UI steps, copied commands, chat handoffs, and ad hoc token files. -OpenBao is the right place to enforce custody, policy, lease, and audit. It is -not, by itself, the right daily interaction surface for operators, agents, -workloads, and approval workflows. Direct use of OpenBao UI and CLI surfaces has -already produced avoidable friction: +OpenBao is the right Tooling for custody, policy, lease, and audit. It is not, +by itself, the right daily interaction surface for operators, agents, workloads, +and approval workflows. Direct use of OpenBao UI and CLI surfaces has already +produced avoidable friction: - approved decisions still require a human to bridge low-level OpenBao details; - agents and operators hit interface mismatches before reaching the security @@ -42,111 +80,145 @@ already produced avoidable friction: - build, test, and production need different levels of privilege and ceremony; - secret values must never move through chat, Git, prompts, State Hub messages, or workplans; -- ops-warden needs a reliable front door for credential and scoped capability - use, not direct secret custody responsibility. +- Staff repositories such as ops-warden need an Engine API for credential and + scoped-capability work, not a direct OpenBao client and not secret-vending + responsibility of their own. -This repository exists to capture that complexity once, behind a stable and -auditable interface. +This repository exists to capture that complexity once, behind a stable, +deterministic, and auditable interface. ## The Mission -To provide a decision-aware secrets workflow that can: +To provide a decision-aware Lifecycle engine that can: -- translate approved requests into narrowly scoped OpenBao changes or capability +- consume an access-engine decision and refuse to proceed without one, or + record the application of a declared unreachable-engine stance in its place; +- translate that decision into narrowly scoped OpenBao changes or capability grants; - operate with distinct build, test, and production privilege layers; - deliver secrets and scoped capabilities to commands and workloads without printing or storing raw values in coordination systems; -- record enough non-secret evidence for review, audit, and troubleshooting; +- emit non-secret evidence under the §9.6 bound — the archive proves records + it holds were not altered or truncated after arrival, not that every event + was sent, and not that absence means non-occurrence; - make routine secure work fast enough that operators and agents actually use - the secure path. + the Engine path instead of touching OpenBao. ## Core Principles -### 1. Decision First +### 1. Decision elsewhere, materialization here -Secret establishment, access, rotation, and deactivation start from an explicit -request and decision. The engine may automate the work after approval; it does -not silently create new secret authority. +Secret establishment, access, rotation, and deactivation start from an +access-engine decision record naming the request it was rendered for. +This engine may automate the work after that decision; it does not render, +cache, or substitute one. Compiled catalog data that would determine an +outcome before the engine runs is still deciding, and is forbidden. -### 2. OpenBao Enforces, secrets-engine Orchestrates +### 2. OpenBao is Tooling; this engine is the API over it -OpenBao remains the vault, policy, lease, and audit backend. secrets-engine owns -the workflow, catalog, validation, delivery, scoped capability handoff, and -operator/agent interface. +OpenBao remains the vault, policy, lease, and audit backend. secrets-engine +owns the catalog, validation, delivery, scoped capability handoff, lifecycle +operations, and the operator/agent/Staff interface to those operations. Staff +must not hold a direct OpenBao client for work this engine is meant to expose. -### 3. Stage-aware Privilege +### 3. Stage-aware privilege Build, test, and production are separate security contexts. Each has its own OpenBao role, policy boundary, approval expectation, TTL limits, and delivery -rules. +rules. Stage is the current equivalent scope for the PEP stance map until +security-zone membership is consumed as a claim. -### 4. No Secret Values in Coordination Surfaces +### 4. No secret values in coordination surfaces Git, State Hub, workplans, chat, prompts, issue comments, and normal logs carry only non-secret metadata. Raw values are delivered through OpenBao, wrapped responses, local exec-time injection, or short-lived bootstrap files with strict permissions. -### 5. Least Friction Without Broad Power +### 5. Least friction without broad power -The common path should be one clear command or review action. That ease must not -require handing platform-root or platform-admin power to routine automation. +The common path should be one clear command or Engine API call. That ease must +not require handing platform-root or platform-admin power to routine +automation, and it must not mint standing credentials for agents. Agent +authority is per task, time-bounded, and attributable to the principal the +agent acts for. -### 6. Bootstrap Honestly, Then Harden +### 6. Bootstrap honestly, then harden During infrastructure setup it is acceptable to use temporary root-created OpenBao credentials stored outside repositories with mode 0600 and explicit -revocation. Those bootstrap shortcuts must be tracked as temporary and replaced -by narrower auth roles. +revocation. Those bootstrap shortcuts must be tracked as temporary, must never +become an implicit fallback, and must be replaced by narrower service identity +bound to OpenBao. -### 7. Prefer Exec-time Delivery +### 7. Prefer exec-time delivery When a workload or operator command needs a secret, the default delivery mode is -process-local injection for the duration of that command. The engine should make -this easier than copying or inspecting a secret value. +process-local injection for the duration of that command. Every such allow has +an explicit lifetime — a TTL, or a binding to a session that ends. The engine +should make this easier than copying or inspecting a secret value. -### 8. Auditable and Reversible +### 8. Evidence is bounded, not omniscient Every apply, read, delivery, lease, verification, revocation, and deactivation -has non-secret evidence that can be reviewed later. +emits non-secret evidence. Load-bearing events — those a control would be +unsound without — require atomic local emission and a declared cadence or +heartbeat. Attributive events should seek atomicity; where they trade it away, +the trade is declared and completeness is not claimed. Evidence belongs in +`audit-core`. Local JSONL and State Hub notes are not a substitute for that +engine. + +### 9. Adaptive systems may only tighten + +This engine may shorten a lifetime, refuse a delivery, suspend a lane, or +require a fresh decision. It must not manufacture authority, widen a grant, or +treat silence from `access-engine` as permission. ## What This Is secrets-engine is: -- a workflow layer for approved secret changes, scoped capability grants, and - access; -- a CLI and service API for operators, agents, and automation; +- the Lifecycle engine for cataloged credential lanes; +- a CLI today and a service API for operators, agents, CI, and Staff + repositories that must not speak OpenBao; - a typed catalog of KV secret lanes, auth-capability lanes, grants, delivery modes, and stage policies; - an OpenBao policy/auth-role applier with strict local validation; - an exec-time secret delivery and scoped capability handoff helper; -- an audit evidence writer for State Hub and local logs; -- the credential interaction surface that ops-warden can route to. +- a PEP at every protected side effect, with a published unreachable-engine + stance; +- the credential interaction surface that ops-warden routes to after a + decision. ## What This Is Not secrets-engine is not: -- a replacement for OpenBao; +- a replacement for OpenBao, or the owner of custody; - an identity provider or MFA system; -- an authorization decision engine; +- an authorization decision engine, an approval store, or a maturity engine; - an application-specific secret store; - a place to persist raw secret values outside OpenBao; - a prompt-time secret injection mechanism for LLMs; -- a bypass around review, approval, or production custody. +- a bypass around review, approval, or production custody; +- the operational owner of the SSH certificate lane — that Staff PEP remains + ops-warden's; this engine may later expose the signing API it should call; +- an observation or containment system — kings-guard proposes, this engine + does not watch production, and actuation is unowned estate-wide. ## System Boundary | Concern | Primary owner | secrets-engine responsibility | | --- | --- | --- | -| Secret custody, leases, audit backend | OpenBao / railiance-platform | Use through least-privilege roles and validated paths. | +| Secret custody, leases, backend audit | OpenBao (Tooling) / railiance-platform | Lifecycle API over least-privilege roles and validated paths. | +| Authorization decisions | access-engine (`flex-auth`) | Consume a decision record; never render or cache one. | +| Approval object | approval-engine | Consume an approval as an input claim; never store or mutate it. | +| Security doctrine | gate-house | Apply doctrine only as it arrives on the decision or in a versioned policy package. | | Human and service identity | key-cape / user-engine | Consume identity claims; do not replace identity lifecycle. | -| Authorization decisions | flex-auth / State Hub decision model | Require and verify decisions before privileged actions. | -| SSH certificate issuance | ops-warden | Provide routed credential/capability access; do not make ops-warden vend secrets. | +| SSH certificate issuance (Staff PEP) | ops-warden | Route and, when the surface exists, expose the OpenBao SSH-CA API; do not take the lane. | +| Evidence custody and integrity | audit-core | Emit non-secret events under the §9.6 bound; do not become the archive. | | Workload secret consumption | Workload repos / CI / runtime | Provide safe delivery and handoff contracts plus catalog entries. | -| Request history and progress | State Hub | Write non-secret evidence and decision links only. | +| Request history and progress | State Hub | Write non-secret progress pointers only; not a security state plane. | ## Direction of Evolution @@ -161,10 +233,16 @@ The repository should evolve through these phases: 4. **Hardening:** replace bootstrap token files with OIDC/service auth, wrapped delivery, short leases, dual control for production provisioning, and routine rotation/deactivation workflows. -5. **Service mode:** expose an API that ops-warden, agents, CI, and future UI - surfaces can use without knowing OpenBao internals. +5. **Layer-model conformance:** published PEP stance equals shipped behaviour; + live side effects hold an access-engine decision record or a recorded stance; + evidence classified and emitted to audit-core; proposed SSH-CA and secret-use + surfaces accepted or declined in this repository's own contract, not only in + other repositories' gap registers. +6. **Service mode:** expose a deterministic API that ops-warden, agents, CI, and + future UI surfaces can use without knowing OpenBao internals. ## Guiding Question **How can approved secret work become low-friction for humans and agents while -keeping raw values, OpenBao privileges, and production impact tightly bounded?** +keeping raw values, OpenBao privileges, and production impact tightly bounded +to a decision this engine did not render?** diff --git a/README.md b/README.md index bbf0571..cc7beda 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,20 @@ Headless, multi-application, multi-tenant secrets workflow and automation layer for approved secret custody, delivery, and lifecycle work across build, test, and production stages. -OpenBao remains the custody and enforcement backend. `secrets-engine` owns the -operator and agent interaction model: catalog, decision checks, plan/apply, -guarded provisioning, verification, delivery, evidence, lifecycle metadata, and -native-access deactivation. Local evidence can be inspected through an +**Layer: Engine / Lifecycle** under the accepted NetKingdom Security Layer +Model (`layer.yaml`). OpenBao remains the custody and enforcement backend. +`secrets-engine` is the deterministic API over it: catalog, decision +consumption, plan/apply, guarded provisioning, verification, delivery, +evidence, lifecycle metadata, and native-access deactivation. It does not +render authorization decisions. Local evidence can be inspected through an allowlisted per-lane `audit` summary without exposing record detail. ## Start Here -- [INTENT.md](INTENT.md) - why this repository exists. +- [INTENT.md](INTENT.md) - why this repository exists, including the Engine / + Lifecycle declaration. +- [layer.yaml](layer.yaml) - machine-readable layer declaration and proposed + surfaces. - [ProductRequirementsDocument.md](ProductRequirementsDocument.md) - product requirements and MVP scope. - [NetKingdom security infrastructure boundary pointer](docs/netkingdom-security-infrastructure.md) diff --git a/SCOPE.md b/SCOPE.md index 104400a..9cd589a 100644 --- a/SCOPE.md +++ b/SCOPE.md @@ -3,15 +3,20 @@ > Implemented capability boundary for agents and contributors. Aspirational > direction belongs in `INTENT.md`; current work and operational gates belong in > `workplans/`. +> +> **Layer declaration (accepted model v0.7):** Engine / Lifecycle. Machine-readable +> form: `layer.yaml`. PEP stance: `pep-stance.yaml`. This file states what the +> implementation currently does inside that layer; it does not restate doctrine. ## One-liner -secrets-engine is a decision-gated Python CLI that validates non-secret secret -lane metadata and orchestrates narrowly scoped OpenBao policy, AppRole, -provisioning, verification, delivery, routing, handoff, and revocation actions. +secrets-engine is the Lifecycle engine for cataloged credential work: a +decision-gated Python CLI that validates non-secret lane metadata and +orchestrates narrowly scoped OpenBao policy, AppRole, provisioning, +verification, delivery, routing, handoff, and revocation actions. -It is an OpenBao workflow client, not a vault, authorization service, identity -provider, credential broker, or general secrets API. +It is a deterministic API over OpenBao, not a vault, not a policy decision +point, not an identity provider, and not a general secrets API. ## Implemented Capabilities @@ -33,15 +38,19 @@ provider, credential broker, or general secrets API. - Resolves a catalog lane by catalog id or `approval.decision_ref`. - Resolves legacy lane decisions from State Hub by id, with tracked local YAML - mirrors retained only for non-production and explicit throwaway demos. -- Fails every production live command closed while State Hub lacks the durable - action-authorization endpoint. A local mirror can unlock a prod-labeled lane - only when an explicit unsafe-demo switch, disabled Hub URL, and loopback - OpenBao target are all present. Plans and `apply --dry-run` remain usable. + mirrors retained only for non-production and explicit throwaway demos. This is + not an access-engine decision record. +- Fails every production live command closed while there is no durable + access-engine / ActionAuthorization record. That fail-closed row is the + published unreachable-engine stance for `prod` in `pep-stance.yaml`. A local + mirror can unlock a prod-labeled lane only when an explicit unsafe-demo + switch, disabled Hub URL, and loopback OpenBao target are all present. Plans + and `apply --dry-run` remain usable. - Builds and validates the flex-auth `ActionAuthorization` profile, including exact lane/stage/action/target/actor/purpose matching, bounded validity, State Hub authority, request digest and decision binding, accepted policy package/version, and an independently required distinct-approver threshold. + Validation is consume-only; this process does not evaluate policy. - Renders guarded OpenBao plans for exact consumer ACL policies and AppRoles. - Applies policy and AppRole metadata idempotently. Existing mounts render a non-mutating check and are never created by apply. @@ -116,7 +125,8 @@ cannot be recovered through that AppRole. event on a best-effort basis. Posts carry stable idempotency/source headers. Each requested State Hub delivery receives an append-only local `delivered`, `queued`, `failed`, or `skipped-no-topic` companion record; edge-relay queued - receipts retain only the non-secret outbox id. + receipts retain only the non-secret outbox id. This trail is attributive: + completeness is not claimed, and it is not `audit-core`. - Every live privileged CLI handler records an attempt before lane-approval resolution and a terminal success, verification failure, rejection, interruption, or typed backend/input failure. Failure evidence contains the @@ -211,9 +221,11 @@ verification, `approle-login` is auth-capability handoff metadata, and - First-class rotate, compromise, reactivate, lease-status, or audit report commands; lifecycle operations currently execute plans without persistent lane state. -- Resolution of a durable State Hub action authorization and wiring its - validated approval threshold to each production handler. -- Direct flex-auth evaluation, JWT signature verification, or identity +- Resolution of a durable access-engine decision record / State Hub + ActionAuthorization and wiring its validated approval threshold to each + production handler. The consumer validator exists; the serving endpoint does + not, so live production remains fail-closed. +- Direct access-engine evaluation, JWT signature verification, or identity authentication. KeyCape claims receive only a consumer preflight; OpenBao is responsible for cryptographic JWT validation. - Runtime tenancy isolation; `org`, `repo`, consumers, and stages are catalog @@ -221,32 +233,55 @@ verification, `approle-login` is auth-capability handoff metadata, and - Management or health verification of ESO, Kubernetes Secrets, deployments, provider accounts, SSH issuance, tunnels, or remote transport. - Any backend other than the local `bao`/`vault` CLI speaking to OpenBao. +- An SSH-CA signing engine API. ops-warden still signs through its declared + OpenBao gap; this repository has accepted that surface as proposed only. +- A secret-use evidence engine API for kings-guard. `route` and `audit` are + operator summaries over local JSONL, not an observation surface. +- Emission to `audit-core`. Evidence today is local JSONL plus best-effort + State Hub progress notes, classified attributive, completeness not claimed. +- Named stance-application records (stage, failure mode, decision id present + only where rendered). Fail-closed production currently surfaces as a + `DecisionError` on the privileged-evidence path. +- Security-zone membership as a request claim. PEP scope is catalog stage. ## System Boundary -- **OpenBao / railiance-platform** owns custody, policy enforcement, leases, and - audit. secrets-engine invokes it through supplied credentials. -- **flex-auth / State Hub decisions** own authorization. secrets-engine only - resolves and enforces recorded decision status. +- **OpenBao / railiance-platform** (Tooling) owns custody, policy enforcement, + leases, and backend audit. secrets-engine is the Lifecycle API over it. +- **access-engine (`flex-auth`)** owns authorization decisions. secrets-engine + consumes a decision record or applies its published unreachable-engine stance; + it does not evaluate policy. +- **approval-engine** owns the durable approval object. secrets-engine may + consume an approval as an input claim and must not store or mutate one. +- **gate-house** owns security doctrine. Doctrine reaches this engine only as it + already reached the decision, never as a side channel. +- **audit-core** owns evidence custody and integrity. Local JSONL and State Hub + notes are not that archive. - **user-engine / key-cape** own identity, OIDC, MFA, and claims. -- **ops-warden** issues SSH certificates and routes non-SSH credential needs; it - does not vend their values. +- **ops-warden** issues SSH certificates (Staff PEP) and routes non-SSH + credential needs here; it does not vend their values. The SSH-CA write + surface is a proposed engine API, not a transferred lane. - **ops-bridge** owns tunnels and remote execution transport and may consume a scoped delivery path. - **workload/platform repositories** own ESO/Kubernetes delivery, provider rotation, and application health. - **info-tech-canon / net-kingdom** own canonical terminology and the - cross-system security boundary. + cross-system security boundary. **gate-house** owns the layer model. Canonical boundary: `net-kingdom/docs/secrets-engine-security-infrastructure-boundary.md`. +Layer model: `net-kingdom/canon/standards/security-layer-model_v0.7.md`. +Working companion: `net-kingdom/SECURITY-COMPANION.md`. ## Security Rules - Never place raw secret values in Git, State Hub, chat, prompts, workplans, normal logs, or evidence. -- Never treat catalog admission, a dry-run, or an old workload CCR as approval - for a new production auth surface. +- Never render or cache an authorization decision. Catalog admission, a dry-run, + an old workload CCR, or a local fixture is not an access-engine allow. +- Never treat silence from `access-engine` as permission. Production live + actions fail closed; any unreachable-engine residue must be the published + stance, recorded, never implicit. - Never mutate an existing shared mount or replace workload delivery by implication. - Never add KV destruction back to ordinary `revoke`; irreversible custody @@ -254,7 +289,22 @@ Canonical boundary: approved action. The live path remains disabled until that approval contract exists. - Keep bootstrap and handoff material outside repositories with mode 0600 and - explicit expiry/revocation handling. + explicit expiry/revocation handling. Bootstrap is not an implicit fallback + from service identity. +- Never claim that local evidence or a missing record proves occurrence or + non-occurrence. Completeness is not claimed. + +## Layer-model obligations (current vs intended) + +| Obligation | Current | Intended | +| --- | --- | --- | +| Layer declaration | `INTENT.md` frontmatter + `layer.yaml` | Keep in this repository's own voice | +| One decision point | Consumer validator for ActionAuthorization; production live fail-closed | Consume an access-engine decision record before every protected side effect | +| PEP stance | `pep-stance.yaml`; prod fail-closed, build/test fail-open relative to access-engine | Published map equals shipped behaviour; stance application recorded by name | +| Evidence bound | Attributive local JSONL + best-effort State Hub | Load-bearing vs attributive classified; load-bearing to `audit-core` with cadence | +| SSH-CA surface | Proposed; not shipped | Engine API for ops-warden's Staff PEP; lane stewardship stays with ops-warden | +| Secret-use evidence | `route` / `audit` over local JSONL | Engine surface of lease/revocation/mount/rotation metadata | +| Agent credential | Bootstrap token file still accepted; KeyCape scaffold unwired | Per-task, time-bounded service identity; no standing engine credential | ## Where Current Work Lives @@ -262,6 +312,7 @@ Canonical boundary: - `docs/hardening-backlog.md` tracks exit from bootstrap mode. - `history/` contains dated capability and intent assessments. - `INTENT.md` remains the stable aspirational direction. +- `layer.yaml` and `pep-stance.yaml` are the layer-model declaration surface. ## Provided Capabilities @@ -298,7 +349,7 @@ title: Non-secret routing and evidence pointers description: Reports decision/readiness metadata and records scrubbed local and best-effort State Hub evidence without returning secret values. Append-only delivery receipts expose State Hub failures, and an allowlisted audit command summarizes lane operations and cleanup. KV verification can attest a - supplied real unrelated identity, but the engine does not own identity selection. This is not a durable - remote audit store. + supplied real unrelated identity, but the engine does not own identity selection. This is attributive + local evidence, not audit-core and not a secret-use observation API. keywords: [routing, evidence, state-hub, audit, secrets] ``` diff --git a/docs/netkingdom-security-infrastructure.md b/docs/netkingdom-security-infrastructure.md index abd048f..02aad6b 100644 --- a/docs/netkingdom-security-infrastructure.md +++ b/docs/netkingdom-security-infrastructure.md @@ -17,6 +17,14 @@ belongs to NetKingdom because it defines cross-system security infrastructure responsibilities and boundaries across OpenBao, flex-auth, user-engine/key-cape, ops-warden, ops-bridge, info-tech-canon, State Hub, and agents. +The accepted layer model and working companion, which this repository now +declares against as Engine / Lifecycle, live at: + +```text +net-kingdom/canon/standards/security-layer-model_v0.7.md +net-kingdom/SECURITY-COMPANION.md +``` + secrets-engine consumes that boundary and implements the secrets workflow, catalog, stage policies, OpenBao apply/delivery mechanics, and evidence model that the canonical document assigns to it. diff --git a/history/2026-08-29-layer-model-scope-intent-review.md b/history/2026-08-29-layer-model-scope-intent-review.md new file mode 100644 index 0000000..0e14ba1 --- /dev/null +++ b/history/2026-08-29-layer-model-scope-intent-review.md @@ -0,0 +1,163 @@ +# Review: Security layer model v0.7 vs INTENT, SCOPE, and implementation + +Date: 2026-08-29 +Author: grok +Standard: `net-kingdom/canon/standards/security-layer-model_v0.7.md` (accepted) +Companion: `net-kingdom/SECURITY-COMPANION.md` v0.2 +Compared: adapted `INTENT.md`, updated `SCOPE.md`, current CLI implementation +Intake closed by this session: `SECRETS-IN-0001` + +## Executive assessment + +The accepted layer model confirms what this repository already was in practice +and had not yet declared in its own voice: **Engine, role Lifecycle** — a +deterministic API over OpenBao for cataloged credential lanes. The gate-house +review note at the top of `INTENT.md` was not a declaration. That note is now +replaced by frontmatter, prose, `layer.yaml`, and a published PEP stance map. + +The intent remains directionally sound. The layer model does not move the +system boundary (OpenBao stays Tooling; `access-engine` stays the only PDP; +ops-warden stays the SSH Staff PEP). It **tightens the contract** this engine +owes at every protected side effect, and it **names two proposed surfaces** +this repository had not answered: an SSH-CA signing API, and secret-use +evidence for kings-guard. + +`SCOPE.md` and the implementation already describe a guarded OpenBao CLI MVP +with one proven native workload pilot. Against the new intent they are +**aligned on layer and ownership, behind on PEP, decision, and evidence +obligations**. Several of those obligations overlap active work in +`SECRETS-WP-0007`; the new work is the layer-model remainder, not a restart of +CLI hardening. + +A finding is raised against the §4 catalog cell, not against the proposed +layer: the catalog says this repository owns "custody". Custody is OpenBao's. +This engine owns the lifecycle API over that custody. + +## What was declared + +| Artifact | Content | +| --- | --- | +| `INTENT.md` frontmatter | `layer: Engine`, `role: Lifecycle` | +| `INTENT.md` body | Own-voice declaration, PEP shape, evidence bound, proposed surfaces | +| `layer.yaml` | Machine-readable declaration; owned OpenBao contact; no decision surface | +| `pep-stance.yaml` | Unreachable-engine stance by catalog stage; prod fail-closed | +| `scripts/check_layer_conformance.py` | Mechanical check of the above | +| `tests/test_layer_conformance.py` | Declaration + published map equals shipped prod fail-closed | + +Contest of the proposed **layer** (Engine): none. The determinism test holds. + +Contest of **catalog wording**: yes — "custody" should not appear as this +engine's owned property. + +Answers to proposed ownerships: + +| Proposed surface | Answer | +| --- | --- | +| SSH-CA signing write (`ops-warden` §5.3 gap) | **Accept** as a Lifecycle engine API. **Decline** operational SSH-lane stewardship. ops-warden remains the Staff PEP. | +| Secret-use evidence (`kings-guard`) | **Accept** as a proposed Lifecycle engine surface, bounded by §9.6. Not shipped. | + +## INTENT vs SCOPE + +No contradiction on layer, owners, or forbidden claims. Drift is maturity: + +| Intent claim | SCOPE today | Gap kind | +| --- | --- | --- | +| Engine / Lifecycle declaration | Declared | Closed this session | +| Consume an access-engine decision before every protected side effect | Consumer validator exists; production live fail-closed because the durable object is not served | Implementation (WP-0007-T04 + WP-0008-T02) | +| Published PEP stance equals shipped behaviour, stance application recorded | Map published; prod fail-closed matches; records are `DecisionError`, not named stance fields | Implementation (WP-0008-T01) | +| Evidence to `audit-core` under §9.6 | Local JSONL + best-effort State Hub; attributive; completeness not claimed | Implementation (WP-0008-T03) | +| SSH-CA engine API | Not implemented; ops-warden still writes OpenBao | Proposed surface (WP-0008-T04) | +| Secret-use evidence API | `route` / `audit` summarize local JSONL | Proposed surface (WP-0008-T05) | +| No standing agent credential | Bootstrap token file still accepted; KeyCape scaffold unwired to OpenBao | Hardening (WP-0007-T05 / WP-0008-T06) | +| Service API | CLI only | Deferred until WP-0007 exit criteria | +| Every allow has a lifetime | Delivery sessions self-revoke; decision TTL waits on durable authorization | Partial | +| Doctrine as claims | Catalog/policies are local YAML | Later; do not compile outcomes here | + +`SCOPE.md` remains the implemented-capability ledger. Volatile workplan status +stays out of it. The new durable additions are the layer table, the updated +system boundary, and the explicit not-implemented layer-model surfaces. + +## INTENT vs implementation + +The strongest implemented slice is unchanged: + +```text +catalog -> approval-status or fail-closed prod gate -> guarded plan + -> policy/AppRole apply -> bounded presence/capability verification + -> CLI-local exec/handoff -> non-secret route/evidence pointer +``` + +That slice is a Lifecycle engine **shape**. It is not yet a conforming PEP. + +### Scorecard against the accepted model + +| Rule | Assessment | Evidence | Material gap | +| --- | --- | --- | --- | +| §3.3 Lifecycle | Aligned in shape | OpenBao isolated in `openbao.py`; catalog/plan/delivery/lifecycle are the API | Service API absent; CLI is the only surface | +| §6 one decision point | Partial, fail-closed | `authorization.py` validates a supplied object and does not evaluate policy. Production live commands refuse without a durable ActionAuthorization. Build/test still accept State Hub status or a local fixture | Local/hub status is not an access-engine decision record. Risk of compiled early decision if fixtures return for production | +| §6.1 compiled data | Watch | Catalog admission and publication-scope policy clamp lanes; they must not become the allow | Keep catalog descriptive; effects stay in the decision | +| §6.4 PEP obligation 1 | Partial | Privileged handlers record attempt/terminal evidence; production refusal is a `DecisionError` | Stance application is not recorded as named fields (stage, failure mode, decision id present only where rendered) | +| §6.4 PEP obligation 2 | Aligned as declared | No verdict cache; `verdict_caching: none` | Must remain none when ActionAuthorization is live | +| §6.4 PEP obligation 3 | Published, not yet loaded | `pep-stance.yaml` is total over build/test/prod/unknown; tests pin prod fail-closed to `_require_lane_approval` | Code still hardcodes the gate rather than loading the map. Build/test fail-open is relative to access-engine, not a manufactured allow — that residue is itself a gap | +| §6.4 PEP obligation 4 | Attributive | Local JSONL + hub receipts; OpenBao remains backend audit | Completeness not claimed (correct). No `audit-core` emit | +| §9.6 load-bearing vs attributive | Undeclared in code | `layer.yaml` classifies today's trail as attributive | Destroy, revoke, and dual-control provision will become load-bearing once they are production-live. They need atomic local outbox + cadence/heartbeat **before** any control branches on them | +| §9.7 lifetime | Partial | AppRole sessions self-revoke; ActionAuthorization has `expires_at` in the consumer schema | Production never reaches the lifetime because live prod is disabled. Bootstrap tokens remain standing credentials | +| §20.2 credentials after a decision | Principle aligned; production blocked | Fail-closed is the honest residue while the PDP object is unserved | Do not reopen production on hub status | +| Staff must not touch OpenBao | External | ops-warden still has the SSH-CA write gap with intended owner here | Engine surface not shipped; do not take the Staff lane | +| Agent rule 1 (no standing credential) | Not met | `--bootstrap-token-file` and `BAO_TOKEN` are the live auth path | KeyCape scaffold exists and is deliberately unwired | +| Observation / actuation | Correctly not claimed | Companion §10: nothing is observed in production; nothing can be contained automatically | Do not plan as if either existed | + +### Overlap with active workplans + +`SECRETS-WP-0007` already owns CLI hardening that the 2026-08-23 assessment +called P0/P1: merge-safe provision (done), lifecycle split (in progress), +exact-action authorization (in progress), session self-revocation (in +progress), verification/evidence (in progress). Those tasks stay there. + +What the layer model **adds**, and what `SECRETS-WP-0008` is for: + +1. Bind the published PEP stance to shipped behaviour and record stance + application by name. +2. Treat the durable ActionAuthorization as an **access-engine decision + record** under §6.4, not as a State Hub status check, once WP-0007-T04's + endpoint exists. +3. Classify evidence and emit it to `audit-core`. +4. Answer the two proposed surfaces (SSH-CA API, secret-use evidence) in this + repository's own contract. +5. Retire standing engine credentials as a layer obligation, not only as + bootstrap hygiene. + +Do not start service mode, runtime tenancy, or a second custody backend from +this review. Companion §10 still holds: nothing is observed in production, and +nothing can be contained automatically. + +## Recommended sequence + +1. Keep `SECRETS-WP-0007` as the CLI-hardening track. Do not reopen live + production on coarse hub status. +2. Land `SECRETS-WP-0008-T01` — load `pep-stance.yaml` as the source of the + fail-closed gate and record stance application fields. +3. When the durable authorization object is served, consume it as a decision + record (`WP-0008-T02` / `WP-0007-T04`) with digest+lifetime replay rules. +4. Classify evidence; emit load-bearing classes to `audit-core` with a local + outbox; declare cadence or heartbeat. +5. Publish the SSH-CA engine contract with ops-warden before writing a client + replacement. Keep their Staff PEP. +6. Publish the secret-use evidence contract with kings-guard. `audit` is not + that contract. +7. Wire service identity only after the platform-owned OpenBao JWT role exists. + Bootstrap remains break-glass. + +## Conclusion + +`INTENT.md` now declares Engine / Lifecycle in this repository's own voice and +accepts the two proposed surfaces without taking Staff work. `SCOPE.md` is +honest about the CLI MVP and the new not-implemented obligations. The +implementation is a Lifecycle engine in shape and a partial PEP in behaviour: +production fail-closed is the correct residue, build/test still run on a +pre-decision-record approval path, and evidence is attributive local notes +rather than `audit-core`. + +The repository should not advertise itself as a service API, a second PDP, a +custody backend, an SSH issuer, or an observation system. Follow-on work is +`SECRETS-WP-0008`. diff --git a/intakes/intakes.md b/intakes/intakes.md index f6dc6ab..bd5a3af 100644 --- a/intakes/intakes.md +++ b/intakes/intakes.md @@ -7,13 +7,21 @@ id: SECRETS-IN-0001 kind: intake title: 'Declaration requested: state this repository''s layer in INTENT.md (security layer model §11)' -status: open +status: closed origin: cross-repo -origin_ref: net-kingdom security-layer-model_v0.4 §11 +origin_ref: net-kingdom security-layer-model_v0.7 §11 priority: low owner: secrets-engine requested_by: gate-house proposed_layer: Engine +resolution: >- + Declared Engine / Lifecycle in INTENT.md frontmatter and layer.yaml on + 2026-08-29 against accepted v0.7. Layer not contested. Catalog cell + "custody" is a finding: OpenBao owns custody; this engine owns the + lifecycle API over it. SSH-CA signing accepted as a proposed engine API + and declined as a Staff lane. Secret-use evidence accepted as proposed. + Review: history/2026-08-29-layer-model-scope-intent-review.md. + Workplan: SECRETS-WP-0008. description: 'A conformance sweep on 2026-08-28 found this repository has no layer declaration of its own. It carries a layering review note gate-house wrote into the top of its INTENT.md on 2026-08-24, and that note names a layer — but the words diff --git a/layer.yaml b/layer.yaml new file mode 100644 index 0000000..a60d205 --- /dev/null +++ b/layer.yaml @@ -0,0 +1,144 @@ +# secrets-engine — NetKingdom security layer declaration +# +# Framework: net-kingdom/canon/standards/security-layer-model_v0.7.md +# Companion: net-kingdom/SECURITY-COMPANION.md +# Voice: INTENT.md (this repository's own declaration, §11) +# Validate: python3 scripts/check_layer_conformance.py +# +# §11 requires a machine-readable form because prose cannot distinguish a +# declaration from a transcribed review. ops-warden offered the layer.yaml +# shape estate-wide; this file is that shape adapted for a Lifecycle engine +# rather than a Staff repository. +# +# §5 (Staff never touches Tooling) does not apply to this repository's OpenBao +# client. A Lifecycle engine is the deterministic API over Tooling it fronts. +# OpenBao contact here is owned, not a §5.3 gap. + +schema_version: "0.1" +framework: netkingdom-security-layer-model +standard_version: "0.7" +repository: secrets-engine +layer: engine +role: lifecycle +declared_by: INTENT.md +declared_at: "2026-08-29" + +# §3.3: a Lifecycle engine is a deterministic API over Tooling it owns. +# OpenBao remains the vault; this engine fronts it. +owned_tooling: + - id: openbao-lifecycle + tooling: OpenBao + modules: + - src/secrets_engine/openbao.py + operations: + - "bao policy/auth/kv subprocess adapter" + - "CAS-aware KV create/patch via JSON input files, never argv values" + note: >- + This is the owned Lifecycle contact, not a Staff §5 shape. A new direct + OpenBao client outside the listed modules is a finding. + +# §6: no repository other than access-engine exposes an authorization decision +# surface. secrets-engine consumes and validates a supplied ActionAuthorization +# object; it does not evaluate policy. +decision_surfaces_exposed: none + +# §6.4 — apply, provision, exec, handoff, revoke, and lifecycle mutations +# cause protected side effects. Being PEP-shaped does not change the layer. +pep_shaped: true +pep_stance: pep-stance.yaml +protected_actions: + - apply + - provision + - verify + - exec + - handoff + - revoke + - lifecycle-suspend + - lifecycle-deactivate + - lifecycle-destroy + +# §13 proposed capabilities. Owner status is proposed, not assented, until +# the surface exists in this repository's own contract. +proposed_capabilities: + + - id: ssh-ca-signing-surface + state: unowned-capability + capability: >- + Deterministic engine API for OpenBao SSH-CA signing, so a Staff PEP + (ops-warden) does not hold a direct VaultCA write. + intended_owner: secrets-engine + owner_status: proposed + blocked_on: >- + No engine API exists yet. ops-warden continues to sign through its + declared §5.3 gap. This repository accepts the engine surface and + declines operational ownership of the SSH access lane. + review: "2026-11-28" + consequence: >- + ops-warden's ssh-ca-signing-write gap remains the production path. + + - id: secret-use-evidence + state: unowned-capability + capability: >- + Lease, revocation, mount, rotation, and delivery-session metadata + exposed as an engine surface for kings-guard secret-abuse posture. + intended_owner: secrets-engine + owner_status: proposed + blocked_on: >- + Local JSONL plus best-effort State Hub notes exist; they are not an + engine API and they are not audit-core. Completeness is not claimed. + review: "2026-11-28" + consequence: >- + kings-guard secret-abuse posture stays fixture-driven. + +# Contacts that are not Tooling, recorded so the check is total. +non_tooling_clients: + + - id: state-hub-decisions-and-progress + target: state-hub + layer: not-catalogued + modules: + - src/secrets_engine/decisions.py + - src/secrets_engine/evidence.py + operation: "HTTP GET /decisions/{id} and POST /progress/" + write: true + note: >- + Outside §5 by the scope rule: State Hub is not a §4 Tooling row. + Decisions consumed from it are a gap relative to access-engine, not a + Tooling contact. Progress notes are not a security state plane. + + - id: access-engine-action-authorization + target: access-engine (flex-auth) + layer: engine + modules: + - src/secrets_engine/authorization.py + operation: "Local validation of a supplied ActionAuthorization object" + write: false + note: >- + Engine-to-engine consume, not a decision surface. The durable State Hub + endpoint that would serve the object is not yet available; production + live actions therefore fail closed (see pep-stance.yaml). + + - id: key-cape-service-auth-scaffold + target: key-cape + layer: tooling + modules: + - src/secrets_engine/service_auth.py + operation: "HTTPS client_credentials exchange; not connected to OpenBao" + write: false + note: >- + key-cape is catalogued Tooling. This contact is an identity-token + preflight only and is deliberately not a write into OpenBao. Wiring it + as OpenBao JWT login remains blocked on the platform-owned role. + +# §9.6 — what this repository may claim about evidence it writes today. +evidence_bound: + kind: attributive + proves: + - local JSONL records that were written were not rewritten by this engine + does_not_prove: + - that a record was ever sent to audit-core + - absence of a record as evidence of non-occurrence + - completeness of the trail + destination_today: "local JSONL plus best-effort State Hub progress notes" + intended_destination: audit-core + completeness_claimed: false diff --git a/pep-stance.yaml b/pep-stance.yaml new file mode 100644 index 0000000..88790c6 --- /dev/null +++ b/pep-stance.yaml @@ -0,0 +1,85 @@ +# secrets-engine — PEP unreachable-engine stance map +# +# Framework: net-kingdom/canon/standards/security-layer-model_v0.7.md §6.4, §9.3 +# Companion: net-kingdom/SECURITY-COMPANION.md §5 +# Voice: INTENT.md / layer.yaml +# Validate: pytest tests/test_layer_conformance.py +# +# §6.4 obligation 3 requires a declared unreachable-engine stance that is +# total, scoped per zone or equivalent, carries no implicit default and no +# per-call discretion, and is published rather than held in code comments. +# Catalog stage is the equivalent scope until security-zone membership arrives +# as a claim on the decision. +# +# The property that makes this worth reading: tests/test_layer_conformance.py +# asserts this map against the shipped production fail-closed gate in +# secrets_engine.cli._require_lane_approval. A published map free to drift +# from the code is worse than none. + +schema_version: "0.1" +framework: netkingdom-security-layer-model +standard_version: "0.7" +repository: secrets-engine +pep_shape: true +declared_by: INTENT.md +decision_engine: access-engine # flex-auth until the governed rename +scope: catalog-stage # equivalent scope; not security-zones yet + +protected_action: >- + OpenBao metadata apply, KV provision, verification, exec delivery, + auth-capability handoff, native revoke, and lifecycle + suspend/deactivate/destroy + +# Total by construction over every catalog stage plus unknown. +# No implicit default — an unlisted value is a config error, not a fallback. +# "fail_closed" means no protected side effect without a durable +# access-engine / ActionAuthorization record. +# "fail_open" here means the engine-unreachable residue: proceed only under +# the existing lane-approval check (State Hub or local fixture), which is +# itself a tracked gap relative to access-engine, not a manufactured allow. +stance: + build: fail_open + test: fail_open + prod: fail_closed + unknown: fail_closed + +# Explicit, three-factor exception. Not a stance row and not a default. +# All three must hold or the production gate remains closed. +demo_exception: + id: unsafe-local-demo + requires: + - "SECRETS_ENGINE_UNSAFE_DEMO=1" + - "hub URL empty" + - "OpenBao target is loopback" + applies_to: prod + note: >- + Throwaway local demonstration only. Not a production residue and not + available when any one factor is missing. + +on_apply: + recorded_fields: + - stage + - action + - approval_status # rejected when the stance refuses + - error_type # DecisionError when fail_closed fires + - decision_id # present only where a decision was resolved + written_to: + - "local JSONL (.evidence/evidence-YYYY-MM-DD.jsonl)" + - "best-effort State Hub progress note" + never_recorded: "any secret material, token, password, or raw value" + gap: >- + Stance application is currently recorded as a DecisionError on the + privileged-evidence path, not as named stance fields (stage, failure + mode, decision id present only where rendered). Closing that gap is + SECRETS-WP-0008-T01. + +# §6.4 obligation 2 — the verdict is never cached. A retry of the same +# canonical request digest inside the decision lifetime is the decision +# being used as issued; a different resource is not. +verdict_caching: none + +# §6.4 obligation 4 / §9.6 — attributive today. No control branches on the +# presence of a local evidence record. Completeness is not claimed. +reconstructability: + bound: "§9.6 attributive — completeness is not claimed" + declared_at: layer.yaml#evidence_bound diff --git a/scripts/check_layer_conformance.py b/scripts/check_layer_conformance.py new file mode 100644 index 0000000..b612faf --- /dev/null +++ b/scripts/check_layer_conformance.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +"""Check secrets-engine against the NetKingdom security layer model (§3.3, §6, §11). + +Read-only. This is the Engine/Lifecycle adaptation of the ops-warden reference +checker. §5 Staff shapes do not apply to the owned OpenBao contact. + +Mechanical checks: + +- a machine-readable declaration exists and says Engine / Lifecycle +- INTENT.md frontmatter matches that declaration +- no authorization decision surface is exposed +- the PEP stance map is published at the path named in the declaration +- every OpenBao subprocess adapter lives in a module listed as owned tooling + +Exit 0 clean, 1 finding, 2 declaration malformed. +""" +from __future__ import annotations + +import argparse +import re +import sys +from pathlib import Path + +import yaml + +ROOT = Path(__file__).resolve().parents[1] +SRC = ROOT / "src" / "secrets_engine" +DECL = ROOT / "layer.yaml" +INTENT = ROOT / "INTENT.md" + +BAO_ARGV = re.compile( + r"""\[\s*(?:["']bao["']|bao_bin\b|bao_binary\b|self\.bao_bin)\s*,""" +) +DECISION_SURFACE = re.compile( + r"""\b(evaluate_policy|check_permission|render_decision|pdp_decide)\b""" +) + + +def load_declaration() -> dict: + if not DECL.exists(): + print(f"MISSING: {DECL} — secrets-engine must declare in its own voice (§11)") + raise SystemExit(2) + decl = yaml.safe_load(DECL.read_text()) + for key in ( + "layer", + "role", + "repository", + "standard_version", + "owned_tooling", + "decision_surfaces_exposed", + "pep_shaped", + "pep_stance", + ): + if key not in decl: + print(f"MALFORMED: layer.yaml has no {key!r}") + raise SystemExit(2) + if decl["layer"] != "engine": + print(f"MALFORMED: declared layer is {decl['layer']!r}, expected 'engine'") + raise SystemExit(2) + if decl["role"] != "lifecycle": + print(f"MALFORMED: declared role is {decl['role']!r}, expected 'lifecycle'") + raise SystemExit(2) + if decl["repository"] != "secrets-engine": + print(f"MALFORMED: repository is {decl['repository']!r}") + raise SystemExit(2) + return decl + + +def intent_frontmatter() -> dict: + text = INTENT.read_text(encoding="utf-8") + if not text.startswith("---"): + print("MALFORMED: INTENT.md has no YAML frontmatter (§11 / companion §2)") + raise SystemExit(2) + end = text.find("\n---", 3) + if end < 0: + print("MALFORMED: INTENT.md frontmatter is unclosed") + raise SystemExit(2) + return yaml.safe_load(text[3:end]) or {} + + +def owned_modules(decl: dict) -> set[str]: + names: set[str] = set() + for entry in decl.get("owned_tooling", []): + for module in entry.get("modules", []): + names.add(Path(module).name) + return names + + +def scan_bao_modules() -> dict[str, list[int]]: + found: dict[str, list[int]] = {} + for path in sorted(SRC.rglob("*.py")): + hits: list[int] = [] + for n, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + stripped = line.strip() + if stripped.startswith("#"): + continue + if BAO_ARGV.search(line): + hits.append(n) + if hits: + found[path.name] = hits + return found + + +def scan_decision_surfaces() -> dict[str, list[int]]: + found: dict[str, list[int]] = {} + for path in sorted(SRC.rglob("*.py")): + hits: list[int] = [] + for n, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + if DECISION_SURFACE.search(line): + hits.append(n) + if hits: + found[path.name] = hits + return found + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--report", action="store_true") + args = ap.parse_args() + + decl = load_declaration() + front = intent_frontmatter() + ok = True + + if str(front.get("layer", "")).lower() != "engine": + ok = False + print("FINDING: INTENT.md frontmatter layer is not Engine") + if str(front.get("role", "")).lower() != "lifecycle": + ok = False + print("FINDING: INTENT.md frontmatter role is not Lifecycle") + + if decl["decision_surfaces_exposed"] not in {None, "none"}: + ok = False + print("FINDING: decision_surfaces_exposed is not none — §6 forbids a second PDP") + + surfaces = scan_decision_surfaces() + if surfaces: + ok = False + print("FINDING: possible authorization decision surface in source:") + for name, lines in sorted(surfaces.items()): + print(f" src/secrets_engine/{name}: line(s) {', '.join(map(str, lines[:6]))}") + + stance = ROOT / str(decl["pep_stance"]) + if decl.get("pep_shaped") and not stance.is_file(): + ok = False + print(f"FINDING: pep_shaped but {decl['pep_stance']} is missing (§6.4 / §13.1)") + + found = scan_bao_modules() + owned = owned_modules(decl) + undeclared = {name: lines for name, lines in found.items() if name not in owned} + if undeclared: + ok = False + print("FINDING: OpenBao adapter outside owned_tooling modules:") + for name, lines in sorted(undeclared.items()): + print(f" src/secrets_engine/{name}: line(s) {', '.join(map(str, lines[:6]))}") + + if args.report: + print( + f"{decl['repository']} — layer: {decl['layer']} " + f"role: {decl['role']} (model v{decl['standard_version']})" + ) + print(f"declared by {decl['declared_by']}") + print(f"pep stance: {decl['pep_stance']}") + print(f"owned OpenBao modules: {sorted(owned)}") + print(f"OpenBao argv adapters found: {sorted(found)}") + + if ok and not args.report: + print( + "PASS — Engine/Lifecycle declaration present, no decision surface, " + f"{len(found)} OpenBao adapter module(s) owned." + ) + elif ok: + print("\nPASS — declaration, owned tooling, and PEP stance path hold.") + return 0 if ok else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_layer_conformance.py b/tests/test_layer_conformance.py new file mode 100644 index 0000000..302da57 --- /dev/null +++ b/tests/test_layer_conformance.py @@ -0,0 +1,117 @@ +"""Layer-model conformance (security-layer-model_v0.7 §3.3, §6, §11). + +The declaration must be this repository's own, machine-readable, and equal to +the shipped production fail-closed gate. A published stance map that may drift +from the code is worse than none. +""" +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path +from types import SimpleNamespace + +import pytest +import yaml + +from secrets_engine.catalog import validate_entry +from secrets_engine.cli import _require_lane_approval +from secrets_engine.errors import DecisionError + +from tests.test_catalog import VALID + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "scripts" / "check_layer_conformance.py" +DECL = ROOT / "layer.yaml" +STANCE = ROOT / "pep-stance.yaml" +INTENT = ROOT / "INTENT.md" + + +def _decl() -> dict: + return yaml.safe_load(DECL.read_text(encoding="utf-8")) + + +def _stance() -> dict: + return yaml.safe_load(STANCE.read_text(encoding="utf-8")) + + +def test_declares_engine_lifecycle_in_its_own_voice(): + assert DECL.exists(), "no layer.yaml — §11 requires a machine-readable declaration" + data = _decl() + assert data["repository"] == "secrets-engine" + assert data["layer"] == "engine" + assert data["role"] == "lifecycle" + assert data["declared_by"] == "INTENT.md" + assert data["decision_surfaces_exposed"] == "none" + assert data["pep_shaped"] is True + + +def test_intent_frontmatter_matches_declaration(): + text = INTENT.read_text(encoding="utf-8") + assert text.startswith("---"), "companion §2 requires INTENT.md frontmatter" + end = text.find("\n---", 3) + front = yaml.safe_load(text[3:end]) + assert front["layer"] == "Engine" + assert front["role"] == "Lifecycle" + assert front["declaration"] == "layer.yaml" + assert front["pep_stance"] == "pep-stance.yaml" + + +def test_checker_passes_on_the_real_tree(): + result = subprocess.run( + [sys.executable, str(SCRIPT)], + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, result.stdout + result.stderr + + +def test_proposed_capabilities_carry_gap_record_fields(): + for cap in _decl()["proposed_capabilities"]: + for field in ("capability", "intended_owner", "blocked_on", "review", "state"): + assert cap.get(field), f"{cap.get('id')} missing {field}" + assert cap["state"] == "unowned-capability" + assert cap["owner_status"] == "proposed" + + +def test_stance_map_is_total_over_catalog_stages(): + stance = _stance()["stance"] + required = {"build", "test", "prod", "unknown"} + assert required <= set(stance), f"stance not total; missing {required - set(stance)}" + assert set(stance.values()) <= {"fail_open", "fail_closed"} + assert stance["prod"] == "fail_closed" + assert stance["unknown"] == "fail_closed" + assert _stance()["verdict_caching"] == "none" + + +def test_published_prod_stance_equals_shipped_fail_closed(monkeypatch): + """pep-stance.yaml prod: fail_closed must equal _require_lane_approval.""" + assert _stance()["stance"]["prod"] == "fail_closed" + entry = validate_entry(dict(VALID, stage="prod", approval={"model": "bootstrap-only"})) + cfg = SimpleNamespace(hub_url="http://127.0.0.1:8000", bao_addr="http://127.0.0.1:8200") + monkeypatch.delenv("SECRETS_ENGINE_UNSAFE_DEMO", raising=False) + with pytest.raises(DecisionError, match="live production remains disabled"): + _require_lane_approval(cfg, entry, "apply") + + +def test_build_stage_is_not_the_production_fail_closed_gate(tmp_path, monkeypatch): + """build is fail_open relative to access-engine: lane approval still applies.""" + assert _stance()["stance"]["build"] == "fail_open" + (tmp_path / ".decisions").mkdir() + (tmp_path / ".decisions" / "x.yaml").write_text( + "id: x\ntitle: approved\nstatus: resolved\nsuperseded_by: null\n" + ) + import secrets_engine.cli as cli + + monkeypatch.setattr(cli, "repo_root", lambda: tmp_path) + entry = validate_entry( + dict( + VALID, + stage="build", + path="build/team/thing", + approval={"model": "decision", "decision_ref": "x"}, + ) + ) + cfg = SimpleNamespace(hub_url="", bao_addr="http://127.0.0.1:8200") + assert _require_lane_approval(cfg, entry, "apply").id == "x" diff --git a/workplans/SECRETS-WP-0008-layer-model-lifecycle-conformance.md b/workplans/SECRETS-WP-0008-layer-model-lifecycle-conformance.md new file mode 100644 index 0000000..3bd6978 --- /dev/null +++ b/workplans/SECRETS-WP-0008-layer-model-lifecycle-conformance.md @@ -0,0 +1,222 @@ +--- +id: SECRETS-WP-0008 +type: workplan +title: "Evolve the Lifecycle engine to the accepted security layer model" +domain: infotech +repo: secrets-engine +status: ready +owner: grok +topic_slug: custodian +created: "2026-08-29" +updated: "2026-08-29" +--- + +# SECRETS-WP-0008 - Evolve the Lifecycle engine to the accepted security layer model + +## Goal + +Close the gaps between the 2026-08-29 layer declaration (Engine / Lifecycle) +and the obligations the accepted NetKingdom Security Layer Model v0.7 places on +a PEP-shaped Lifecycle engine. + +Source review: `history/2026-08-29-layer-model-scope-intent-review.md`. +Declaration surface already landed this session: `INTENT.md` frontmatter, +`layer.yaml`, `pep-stance.yaml`, and `tests/test_layer_conformance.py`. + +This workplan does **not** restart CLI hardening owned by `SECRETS-WP-0007`. +It adds the layer-model remainder: stance binding, decision-record consumption, +`audit-core` evidence, and the two proposed engine surfaces. + +## Non-goals + +- Taking ops-warden's SSH access lane or becoming a credential broker. +- Rendering or caching authorization decisions. +- Observation in production or automatic containment (companion §10: both are + at zero estate-wide). +- Service mode, runtime tenancy, or a second custody backend. +- Reopening live production on State Hub status or local fixtures. + +## Coordination + +- `SECRETS-WP-0007-T04` owns the consumer validator and the fail-closed + production gate until a durable ActionAuthorization object is served. +- `SECRETS-WP-0007-T05` owns delivery-session self-revocation and the KeyCape + scaffold; this workplan owns the layer-model "no standing credential" bar. +- railiance-platform owns production OpenBao and the JWT role. +- `access-engine` / State Hub own serving the decision record. +- `audit-core` owns evidence custody. +- ops-warden owns the SSH Staff PEP and must assent any SSH-CA API contract. +- kings-guard owns secret-abuse posture and must assent the evidence surface. +- gate-house owns statute §13.1 inventory of PEP stance maps. + +## Tasks + +## Publish stance application as shipped behaviour + +```task +id: SECRETS-WP-0008-T01 +status: todo +priority: high +``` + +`pep-stance.yaml` is published and tests pin `prod: fail_closed` to +`_require_lane_approval`. The gate is still hardcoded, and a refusal is +recorded as a `DecisionError` rather than named stance fields. + +Load the published map as the source of shipped defaults. When the +unreachable-engine residue is applied, record at least stage, failure mode, and +decision id present only where a decision was rendered. Keep the three-factor +unsafe-demo exception out of the stance table. Assert map equality in +`tests/test_layer_conformance.py` the way ops-warden's stance test does. + +Acceptance: + +- `_require_lane_approval` (or its successor) reads `pep-stance.yaml`. +- Changing the YAML without changing the code fails the test, and the reverse. +- Fail-closed production evidence includes named stance fields, never a secret. +- Build/test remain fail-open *relative to access-engine* and still require + the existing lane-approval check; that residue stays documented as a gap + until T02. + +## Consume access-engine decision records + +```task +id: SECRETS-WP-0008-T02 +status: wait +priority: high +``` + +Blocked on the durable ActionAuthorization serving path owned with +`SECRETS-WP-0007-T04` / State Hub / `access-engine`. + +Once that object is served, a protected side effect holds a decision record +identifying the request it was rendered for. Replay is permitted iff the +canonical request digest matches and the lifetime holds. Do not cache the +verdict. Do not treat State Hub status, a local fixture, or catalog admission +as the decision. + +Acceptance: + +- Every live privileged production handler passes one shared consume function. +- Wrong digest, expired lifetime, superseded status, or missing record fail + closed before OpenBao. +- Evidence carries the decision id; a fail-closed residue still records the + stance instead. +- Local fixtures cannot authorize steady-state production. + +## Emit evidence under the §9.6 bound + +```task +id: SECRETS-WP-0008-T03 +status: todo +priority: high +``` + +Classify each privileged action as load-bearing or attributive. Today's trail +is attributive local JSONL plus best-effort State Hub notes; completeness is +already not claimed and must stay that way until the destination is +`audit-core`. + +Load-bearing classes (at least live destroy, production revoke/suspend of a +control, and dual-control provision once those are enabled) require atomic +emission via a **local** outbox and a declared cadence or heartbeat. Do not +place a synchronous `audit-core` dependency inside the state-change +transaction. Attributive classes may keep the current non-blocking trade if +that trade stays declared. + +Acceptance: + +- A machine-readable classification exists next to the declaration. +- No control in this repository branches on presence or absence of a local + evidence record. +- Load-bearing emission is queued locally; an `audit-core` outage does not + block a revocation. +- Documentation uses the sound claim: the archive proves records it holds were + not altered or truncated after arrival. + +## Accept the SSH-CA signing engine surface + +```task +id: SECRETS-WP-0008-T04 +status: todo +priority: medium +``` + +ops-warden's `ssh-ca-signing-write` gap names this engine as intended owner. +Accept the **engine API**, decline the Staff lane. Publish a contract +ops-warden can call after an `access-engine` decision: sign a short-lived +certificate for an `adm`/`agt`/`atm` actor under the caller's request, present +no additional authority, reconstruct as the caller's action. + +Do not implement a replacement client until ops-warden assents the contract. +Do not unpark the standalone `warden-sign` AppRole as break-glass. + +Acceptance: + +- A non-secret contract document in `docs/` states request, decision binding, + lifetime, evidence, and non-responsibilities. +- ops-warden has a recorded chance to assent or contest. +- No OpenBao SSH-CA write is added to this CLI ahead of that assent. +- `layer.yaml` owner_status remains `proposed` until the surface ships. + +## Accept the secret-use evidence surface + +```task +id: SECRETS-WP-0008-T05 +status: todo +priority: medium +``` + +kings-guard's secret-abuse posture is fixture-driven because no engine exposes +lease, revocation, mount, rotation, and delivery-session metadata. `route` and +`audit` are operator summaries over local JSONL and are not that surface. + +Publish a read-only engine contract of non-secret metadata, bounded by §9.6: +it cannot prove omission at source. Coordinate destination with `audit-core`. +Do not give kings-guard a vault client, and do not return values. + +Acceptance: + +- Contract lists fields, freshness, and the evidence bound. +- kings-guard has a recorded chance to assent or contest. +- No secret material, token, or accessor is in the surface. +- `layer.yaml` owner_status remains `proposed` until the surface ships. + +## No standing engine credential + +```task +id: SECRETS-WP-0008-T06 +status: wait +priority: medium +``` + +Blocked on the platform-owned OpenBao JWT mount/role (railiance-platform) and +on `SECRETS-WP-0007-T05` wiring. Companion §7 / statute §3.4: an agent holds +no long-lived credential of its own. Authority is per task, time-bounded, and +attributable to the principal it acts for. + +Bootstrap token files remain break-glass, heavily evidenced, never an implicit +fallback from service identity. + +Acceptance: + +- Steady-state CLI/API auth is the reviewed service identity, not + `--bootstrap-token-file`. +- Bootstrap is a named break-glass provider with distinct evidence. +- No fallback from JWT/service-auth failure into bootstrap or AppRole. +- Issued delivery sessions continue to self-revoke as in WP-0007-T05. + +## Exit criteria + +- The published PEP stance is the shipped default and stance application is + recorded by name. +- Production live side effects require an access-engine decision record, or + remain fail-closed with a recorded stance. +- Evidence classification and destination match §9.6; completeness is not + claimed. +- SSH-CA and secret-use surfaces are either contracted with the declaring + repositories or explicitly declined in this repository's own voice. +- Bootstrap is not the steady-state engine credential. +- `layer.yaml` / `pep-stance.yaml` / INTENT frontmatter stay in one voice. +- No raw secret values in Git, State Hub, chat, prompts, workplans, evidence, + or argv.