From fd2c5a59043825ebabc705479b5447b2b6abf652 Mon Sep 17 00:00:00 2001 From: tegwick Date: Wed, 14 Oct 2026 18:19:14 +0200 Subject: [PATCH] Name a dedicated reuse-surface hub-writer caller. CCR-2026-0005 does not name a principal, and the platform-admin entity must not receive the workload policy. Add the draft role, verifier, and plan, drop the retired operators/nextcloud description, and record that tegwick's MFA factor is enrolled. Assistant: grok Assistant-Session: 01a13b2d-5f6d-7a32-b2c9-384625c55ca6 --- .../CCR-DRAFT-REUSE-SURFACE-HUB-WRITER.yaml | 109 +++++++++ ...e-surface-hub-writer-workload-kv-read.json | 23 ++ ...workload-kv-read-reuse-surface-runtime.hcl | 22 ++ docs/reuse-surface-hub-writer-attended.md | 30 +++ manifests/custody-descriptions.yaml | 15 +- plans/reuse-surface-hub-writer.md | 166 ++++++++++++++ scripts/verify-reuse-surface-hub-writer.py | 212 ++++++++++++++++++ src/ops_mason/reuse_surface_hub_writer.py | 89 ++++++++ tests/test_reuse_surface_hub_writer.py | 67 ++++++ ...-0004-credential-inventory-descriptions.md | 16 +- ...fluid-telegram-operator-credential-lane.md | 11 +- .../MASON-WP-0011-reuse-surface-hub-writer.md | 98 ++++++++ 12 files changed, 841 insertions(+), 17 deletions(-) create mode 100644 builds/reuse-surface-hub-writer/credential-change-requests/CCR-DRAFT-REUSE-SURFACE-HUB-WRITER.yaml create mode 100644 builds/reuse-surface-hub-writer/openbao/auth/reuse-surface-hub-writer-workload-kv-read.json create mode 100644 builds/reuse-surface-hub-writer/openbao/policies/workload-kv-read-reuse-surface-runtime.hcl create mode 100644 docs/reuse-surface-hub-writer-attended.md create mode 100644 plans/reuse-surface-hub-writer.md create mode 100644 scripts/verify-reuse-surface-hub-writer.py create mode 100644 src/ops_mason/reuse_surface_hub_writer.py create mode 100644 tests/test_reuse_surface_hub_writer.py create mode 100644 workplans/MASON-WP-0011-reuse-surface-hub-writer.md diff --git a/builds/reuse-surface-hub-writer/credential-change-requests/CCR-DRAFT-REUSE-SURFACE-HUB-WRITER.yaml b/builds/reuse-surface-hub-writer/credential-change-requests/CCR-DRAFT-REUSE-SURFACE-HUB-WRITER.yaml new file mode 100644 index 0000000..9b052ad --- /dev/null +++ b/builds/reuse-surface-hub-writer/credential-change-requests/CCR-DRAFT-REUSE-SURFACE-HUB-WRITER.yaml @@ -0,0 +1,109 @@ +id: CCR-DRAFT-REUSE-SURFACE-HUB-WRITER +kind: credential-change-request +schema_version: 1 +request_type: workload-kv-read +title: Name the dedicated reuse-surface hub-writer caller +status: proposed +created: '2026-10-14' +updated: '2026-10-14' +requester: + agent: ops-mason + reason: 'CUST-WP-0081 / State Hub message 08b254af. CCR-2026-0005 is active and + does not name a caller. Entity 4a1f663c-0621-0483-edd0-f852274f4b6b carries + default, operator-custody and platform-admin, so its token can reach the data + path, the metadata path and siblings. Do not attach workload-kv-read-reuse-surface-runtime + to that entity. Bind a new OIDC role to a different principal. No secret value + is rotated or read by this request.' +review: + required: true + required_approvers: + - platform-operator + - reuse-surface-owner + comments: [] +target: + domain: financials + tenant: reuse + workload: reuse-surface + environment: production + purpose: Attended read of the existing hub writer token by one dedicated caller. +openbao: + mount: platform + kv_path: platform/workloads/reuse/reuse-surface/runtime-secrets + fields: + - REUSE_SURFACE_TOKEN + - REUSE_SURFACE_FORGEJO_WEBHOOK_SECRET + policy_name: workload-kv-read-reuse-surface-runtime + policy_file: openbao/policies/workload-kv-read-reuse-surface-runtime.hcl + metadata_read: true + token_self_lifecycle: true + auth: + method: oidc + mount: netkingdom + role: reuse-surface-hub-writer-workload-kv-read + allowed_redirect_uris: + - http://localhost:8250/oidc/callback + - http://127.0.0.1:8250/oidc/callback + - http://127.0.0.1:18200/ui/vault/auth/netkingdom/oidc/callback + oidc_scopes: + - openid + - profile + - email + - groups + user_claim: sub + groups_claim: groups + bound_claims_type: string + bound_claims: + groups: + - reuse-surface-hub-writers + /assurance/level: aal2 + bound_claims_confirmed: false + policies: + - workload-kv-read-reuse-surface-runtime + ttl: 15m + token_max_ttl: 30m + token_explicit_max_ttl: 30m + token_no_default_policy: true + token_type: service +access_frontdoor: + type: ops-warden + catalog_id: reuse-surface-hub-write-token + selector: reuse-surface federation hub write bearer token + command: 'After a passing member receipt, run warden access reuse-surface-hub-write-token + --exec --field REUSE_SURFACE_TOKEN -- with the unprinted lane + token. Do not use --fetch.' + resolvable: false + readiness: pending-review +risk: + classification: high + notes: + - REUSE_SURFACE_TOKEN writes to the production federation hub. This request does + not read or rotate it. + - The platform-admin entity must not receive this policy. OpenBao merges entity + policies into every token for that entity. + - The ESO policy workload-kv-read-reuse-surface-runtime-eso stays on its Kubernetes + role. This role does not attach it. + - bound_claims_confirmed stays false until key-cape confirms group reuse-surface-hub-writers + and a member entity other than 4a1f663c-0621-0483-edd0-f852274f4b6b. + - Public bao.coulomb.social ingress is not a callback. Use the workstation tunnel. +verification: + positive: + - A member token's policy set is exactly workload-kv-read-reuse-surface-runtime. + - That token's capabilities are read on the exact data path and the exact metadata path. + negative: + - The same token is deny on platform/data/workloads/reuse/reuse-surface, its metadata + parent, and platform/data/workloads/issue-core/issue-core/issue-core-runtime. + - A principal outside reuse-surface-hub-writers is refused by the role. + - Entity 4a1f663c-0621-0483-edd0-f852274f4b6b is not the member token's entity. + activation_conditions: + - platform-operator and reuse-surface-owner approve this caller binding. + - key-cape confirms the group and that the member entity has no platform-admin or + operator-custody identity policy. + - Founder applies policy and role through the delegated applier. No KV write. + - scripts/verify-reuse-surface-hub-writer.py member and non-member both pass. + Receipts stay mode 0600 and contain no token or field value. +lifecycle: + deactivate: delete auth/netkingdom/role/reuse-surface-hub-writer-workload-kv-read. + Leave the KV path, the ESO role, and CCR-2026-0005 delivery in place. + rotate: unchanged from CCR-2026-0005. This request does not rotate field values. + compromised: detach the role, revoke outstanding tokens for the role, and follow + the CCR-2026-0005 compromise procedure if a field value was exposed. diff --git a/builds/reuse-surface-hub-writer/openbao/auth/reuse-surface-hub-writer-workload-kv-read.json b/builds/reuse-surface-hub-writer/openbao/auth/reuse-surface-hub-writer-workload-kv-read.json new file mode 100644 index 0000000..b6cd641 --- /dev/null +++ b/builds/reuse-surface-hub-writer/openbao/auth/reuse-surface-hub-writer-workload-kv-read.json @@ -0,0 +1,23 @@ +{ + "role_type": "oidc", + "allowed_redirect_uris": [ + "http://localhost:8250/oidc/callback", + "http://127.0.0.1:8250/oidc/callback", + "http://127.0.0.1:18200/ui/vault/auth/netkingdom/oidc/callback" + ], + "oidc_scopes": ["openid", "profile", "email", "groups"], + "user_claim": "sub", + "groups_claim": "groups", + "bound_claims_type": "string", + "bound_claims": { + "groups": ["reuse-surface-hub-writers"], + "/assurance/level": "aal2" + }, + "token_policies": ["workload-kv-read-reuse-surface-runtime"], + "token_no_default_policy": true, + "token_type": "service", + "token_ttl": "15m", + "token_max_ttl": "30m", + "token_explicit_max_ttl": "30m", + "verbose_oidc_logging": false +} diff --git a/builds/reuse-surface-hub-writer/openbao/policies/workload-kv-read-reuse-surface-runtime.hcl b/builds/reuse-surface-hub-writer/openbao/policies/workload-kv-read-reuse-surface-runtime.hcl new file mode 100644 index 0000000..6dbcce3 --- /dev/null +++ b/builds/reuse-surface-hub-writer/openbao/policies/workload-kv-read-reuse-surface-runtime.hcl @@ -0,0 +1,22 @@ +# Caller policy for the reuse-surface hub writer. +# Exact data and metadata read. Sibling and parent paths are not granted. +# Self lookup and revoke match credential-change.py generated_policy_hcl when +# metadata_read and token_self_lifecycle are true. No capabilities-self grant: +# the attended verifier queries sys/capabilities with the operator token. +# Do not attach this policy to entity 4a1f663c-0621-0483-edd0-f852274f4b6b. + +path "platform/data/workloads/reuse/reuse-surface/runtime-secrets" { + capabilities = ["read"] +} + +path "platform/metadata/workloads/reuse/reuse-surface/runtime-secrets" { + capabilities = ["read"] +} + +path "auth/token/lookup-self" { + capabilities = ["read"] +} + +path "auth/token/revoke-self" { + capabilities = ["update"] +} diff --git a/docs/reuse-surface-hub-writer-attended.md b/docs/reuse-surface-hub-writer-attended.md new file mode 100644 index 0000000..a8b9774 --- /dev/null +++ b/docs/reuse-surface-hub-writer-attended.md @@ -0,0 +1,30 @@ +# reuse-surface hub-writer attended verification + +Plan: `plans/reuse-surface-hub-writer.md`. Draft CCR: +`builds/reuse-surface-hub-writer/credential-change-requests/CCR-DRAFT-REUSE-SURFACE-HUB-WRITER.yaml`. + +Run this only after the founder has applied the role and key-cape has +confirmed group `reuse-surface-hub-writers`. The member must not be OpenBao +entity `4a1f663c-0621-0483-edd0-f852274f4b6b`. + +The script leaves `~/.vault-token` in place and uses it only to call +`sys/capabilities`. The lane token is not printed and is revoked before the +script exits. A passing receipt is the sanitized verification record. + +```bash +export BAO_ADDR=http://127.0.0.1:18200 +python3 scripts/verify-reuse-surface-hub-writer.py member +python3 scripts/verify-reuse-surface-hub-writer.py non-member +``` + +`member` passes when the token's policies are exactly +`workload-kv-read-reuse-surface-runtime`, the entity is not the admin entity, +the runtime-secrets data and metadata paths are `read`, the parent paths and +`platform/data/workloads/issue-core/issue-core/issue-core-runtime` are `deny`, +and revoke succeeded. + +`non-member` passes when the role refuses the login. Use a private window so +an existing KeyCape session is not reused. + +Do not run `bao kv get` on this path to prove the lane. Do not attach the +workload policy to the admin entity. diff --git a/manifests/custody-descriptions.yaml b/manifests/custody-descriptions.yaml index 94891d7..b1853d4 100644 --- a/manifests/custody-descriptions.yaml +++ b/manifests/custody-descriptions.yaml @@ -1,16 +1,3 @@ -operators/nextcloud: - description: '' - missing: - - description - - used_by - - rotation - - on_loss - on_loss: '' - owner: railiance-platform - review: draft - rotation: '' - sources: [] - used_by: '' operators/nextcloud/backup: description: Operator login (BACKUP_USERNAME, BACKUP_PASSWORD) for the dedicated Nextcloud Storage Share account Backup on nx4069.your-storageshare.de, the independent secondary backup copy lane. @@ -501,9 +488,11 @@ platform/workloads/reuse/reuse-surface/runtime-secrets: sources: - CCR-2026-0005-reuse-surface-runtime-secrets-lane.yaml - ops-warden catalog reuse-surface-hub-write-token + - ops-mason plan reuse-surface-hub-writer (MASON-WP-0011; role not applied) used_by: ExternalSecret reuse/reuse-surface-runtime -> Secret reuse-surface-env; ClusterSecretStore openbao-reuse uses private railiance01 OpenBao and exact reuse/reuse-surface-eso Kubernetes identity. Caller-facing OIDC policy workload-kv-read-reuse-surface-runtime remains separately retained. + The dedicated caller is OIDC role reuse-surface-hub-writer-workload-kv-read, not the platform-admin entity. platform/workloads/secrets-engine/approval-client: description: Hold the secrets-engine-approval confidential client secret in platform custody and project it into the KeyCape runtime so KeyCape can verify presented client_credentials without the value living diff --git a/plans/reuse-surface-hub-writer.md b/plans/reuse-surface-hub-writer.md new file mode 100644 index 0000000..921fb80 --- /dev/null +++ b/plans/reuse-surface-hub-writer.md @@ -0,0 +1,166 @@ +--- +id: reuse-surface-hub-writer +demand_source: the-custodian/workplans/CUST-WP-0081 and State Hub message 08b254af-59d4-4d1a-9544-fed3dcaaf213 +consumer_repo: reuse-surface +credential_type: openbao-oidc-kv-read +status: draft +approved_by: null +approved_at: null +created: "2026-10-14" +updated: "2026-10-14" +--- + +# Construction plan: dedicated reuse-surface hub-writer caller + +## 1. Demand + +CUST-WP-0081 needs to register reviewed capabilities at the reuse-surface hub. +The write bearer already exists at +`platform/workloads/reuse/reuse-surface/runtime-secrets`, field +`REUSE_SURFACE_TOKEN`, delivered to the hub by External Secrets under +CCR-2026-0005. `warden access reuse-surface-hub-write-token --exec` is the +attended fetch. On 2026-10-14 that fetch returned OpenBao HTTP 403 after +flex-auth decision `decision:f730e3da14beebdd`. The helper did not run. + +The operator then confirmed OpenBao CLI entity +`4a1f663c-0621-0483-edd0-f852274f4b6b` carries `default`, `operator-custody` +and `platform-admin`. `bao token capabilities` on that token reports +create, delete, list, read and update on the exact data path, the exact +metadata path, and an unrelated sibling. That token is not a dedicated +caller. OpenBao adds entity policies to every token for the entity, so +attaching `workload-kv-read-reuse-surface-runtime` to this entity would not +produce a token that carries only that policy. The request forbids that +attachment and forbids sending any secret value. + +## 2. Existing-structure survey + +Live `plan-survey.py` was not run. There is no `ops-mason-survey` grant in +this session, and the admin token must not be used as the survey credential. +This section is from the repositories on disk at 2026-10-14. + +- Policy `workload-kv-read-reuse-surface-runtime` + (`railiance-platform/openbao/policies/workload-kv-read-reuse-surface-runtime.hcl`) + grants read on the exact data path and the exact metadata path. It grants + nothing else. Sibling denial is the absence of any other grant. No auth + role in railiance-platform names this policy. +- ESO uses the separate policy `workload-kv-read-reuse-surface-runtime-eso` + and Kubernetes role `reuse-surface-runtime-eso` (CCR-2026-0005, updated + 2026-09-05). That lane is the hub pod's delivery path, not the workstation + caller. +- `agent-high-risk-boundary` denies data read and allows metadata read on + this path for tokens that carry it. `platform-admin` grants `platform/*`. + A token with both would be denied on the data path, because deny wins. + The reported capability set does not include deny, so the 403 and the + capability report are not the same effective token. The verifier must + measure the token that `warden access` will actually use. +- Catalog entry `reuse-surface-hub-write-token` is active and + `resolvable: true`. Its `auth_method` says the caller's own OpenBao token, + either operator OIDC or a token carrying the workload policy. It does not + name a role or an entity. CCR-2026-0005 does not name one either. +- Redirects that use `https://bao.coulomb.social` are stale. The public + ingress is gone. Fluid-telegram's role uses localhost, 127.0.0.1:8250, and + the 127.0.0.1:18200 tunnel. + +**Does an existing lane already satisfy this demand?** The KV path and the +read policy do. The caller binding does not. The change is a role bound to +a different principal, plus the two self-lifecycle stanzas the platform +generator emits for `token_self_lifecycle: true`. No new secret, no new KV +path, no change to the ESO role. + +## 3. Proposed changes + +SEC-DSM ยง37, in order: + +- Q1. The hub still requires its existing bearer. Reading that bearer from + OpenBao can use a person or service identity. Do not create a secret to + stand in for the caller. +- Q2. OpenBao accepts OIDC on `auth/netkingdom`. Use that. A KeyCape group + selects the principal. The principal's OpenBao entity must not be + `4a1f663c-0621-0483-edd0-f852274f4b6b` and must not carry `platform-admin` + or `operator-custody`. +- Q3. The OpenBao token is a 15-minute service token from the role. The hub + bearer stays the static field already in KV. No new issuance layer. +- Q4. Rotation of the hub bearer stays CCR-2026-0005. This plan does not + rotate it. +- Q5. No new persistent secret. Owner of the binding is railiance-platform. + The consumer is the attended hub-registration helper. Authority is the + OIDC role. Delivery is `warden access --exec` with the lane token. + Revocation is `auth/token/revoke-self` and deletion of the role. + +Preference hierarchy: federated identity for the read, existing vaulted +static bearer for the hub. Not a new static secret and not a new AppRole. + +1. `modify` policy `workload-kv-read-reuse-surface-runtime`. Add + `auth/token/lookup-self` read and `auth/token/revoke-self` update. Leave + the two KV paths as read. Do not add a glob deny: in OpenBao a deny on a + prefix also denies the more specific path. Sibling denial remains "not + granted", checked on a parent path and on + `platform/data/workloads/issue-core/issue-core/issue-core-runtime`. +2. `create` OIDC role `auth/netkingdom/role/reuse-surface-hub-writer-workload-kv-read`. + `token_policies` is only the workload policy, `token_no_default_policy` is + true, TTL 15m, max 30m, group `reuse-surface-hub-writers`, assurance + `aal2`. Callbacks are the workstation URIs above. +3. `create` KeyCape group `reuse-surface-hub-writers` and one member whose + OpenBao entity is not the forbidden entity and has no admin identity + policies. key-cape owns that group. `bound_claims_confirmed` stays false + until they confirm it. +4. No KV write. No attachment of this policy, or of + `workload-kv-read-reuse-surface-runtime-eso`, to entity + `4a1f663c-0621-0483-edd0-f852274f4b6b`. + +Artifacts: `builds/reuse-surface-hub-writer/`. The CCR id is a draft. +railiance-platform assigns the real id and applies it. ops-mason does not +install the policy or the role. + +## 4. Review notes + +The 403 is not evidence that the path is closed to every token. It is +evidence that the token `warden access` used could not read the field. +The later capability check describes a different, broader token. The member +receipt has to show both facts on one token: exact policy set, and deny on +a sibling. A pass on the admin entity is a failure. + +`credential-change.py` will not keep a hand-written `sys/capabilities-self` +stanza. The verifier therefore asks `sys/capabilities` with the operator +token and puts the lane token only in a mode-0600 body file that it +deletes. The receipt never contains the token. + +Catalog `reuse-surface-hub-write-token` stays active. This plan does not +ask ops-warden to vend the value. After both verifier modes pass, ops-warden +can name the role in `auth_method`. Until then the entry's caller is unnamed +and the draft front door stays `pending-review`. + +## 5. Attended apply and proof + +Founder, after platform-operator and reuse-surface-owner approval, and after +key-cape confirms the group: + +1. Copy the policy file over + `railiance-platform/openbao/policies/workload-kv-read-reuse-surface-runtime.hcl` + and apply the CCR with the delegated applier. Do not run `bao kv put`. +2. Keep an operator token in `~/.vault-token` for the capability query. + In a private window, as the group member: + +```bash +python3 scripts/verify-reuse-surface-hub-writer.py member +python3 scripts/verify-reuse-surface-hub-writer.py non-member +``` + +3. Receipts are mode 0600 under + `~/.local/state/railiance/receipts/reuse-surface-hub-writer-*.txt`. + Copy the sanitized text into + `docs/evidence/` only after both results are `pass`. That copy is the + verification receipt. It is not written in advance. +4. Reply to message `08b254af` with the receipt text. Then a registration + command may use the same role's unprinted token: + +```bash +warden access reuse-surface-hub-write-token --exec --field REUSE_SURFACE_TOKEN -- +``` + +Set `BAO_TOKEN` to the lane token for that one command. Do not use `--fetch`. +Do not export the field. Revoke the lane token when the command returns. + +## 6. Result + +Not applied. No live receipt yet. diff --git a/scripts/verify-reuse-surface-hub-writer.py b/scripts/verify-reuse-surface-hub-writer.py new file mode 100644 index 0000000..c022db6 --- /dev/null +++ b/scripts/verify-reuse-surface-hub-writer.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +"""Attended proof of the reuse-surface hub-writer caller. + + ./scripts/verify-reuse-surface-hub-writer.py member + ./scripts/verify-reuse-surface-hub-writer.py non-member + +Logs in on auth/netkingdom role reuse-surface-hub-writer-workload-kv-read with +``bao login -token-only``, so the operator's token helper is left alone. The +lane token stays in memory. Capability queries run as the operator token +already present in the environment or token helper, because the workload +policy has no sys/capabilities-self grant. The receipt records policies, +entity id, path capabilities, and pass/fail. It does not record the token or +any KV value. + +Sign in with a private browser window. A reused platform-admin session is the +wrong principal for ``member``. +""" + +from __future__ import annotations + +import json +import os +import re +import stat +import subprocess +import sys +import tempfile +from datetime import datetime, timezone +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) + +from ops_mason.reuse_surface_hub_writer import ( + DATA, + METADATA, + MUST_DENY, + PARENT_DATA, + PARENT_METADATA, + POLICY, + ROLE, + SIBLING, + assess, +) + +BAO_ADDR = os.environ.get("BAO_ADDR", "http://127.0.0.1:18200") +TOKEN_RE = re.compile(r"\b(?:hvs|hvb|hvr)\.[A-Za-z0-9_-]{8,}\b") +PATHS = [DATA, METADATA, PARENT_DATA, PARENT_METADATA, SIBLING] + + +def redact(text: str) -> str: + return TOKEN_RE.sub("[redacted-token]", text or "") + + +def receipts_dir() -> Path: + home = Path(os.environ.get("HOME", str(Path.home()))) + path = home / ".local/state/railiance/receipts" + path.mkdir(parents=True, mode=0o700, exist_ok=True) + return path + + +def operator_token() -> str: + for name in ("BAO_TOKEN", "VAULT_TOKEN"): + value = os.environ.get(name, "").strip() + if value: + return value + helper = Path(os.environ.get("HOME", str(Path.home()))) / ".vault-token" + if helper.is_file(): + return helper.read_text(encoding="utf-8").strip() + return "" + + +def run_bao(args: list[str], token: str, stdin: str | None = None) -> subprocess.CompletedProcess[str]: + env = os.environ.copy() + env["BAO_ADDR"] = BAO_ADDR + env["BAO_TOKEN"] = token + env.pop("VAULT_TOKEN", None) + return subprocess.run( + ["bao", *args], + input=stdin, + text=True, + capture_output=True, + env=env, + check=False, + ) + + +def login() -> str: + proc = run_bao( + ["login", "-token-only", "-method=oidc", "-path=netkingdom", f"role={ROLE}"], + token="", + ) + token = (proc.stdout or "").strip() + if proc.returncode != 0 or not token or TOKEN_RE.fullmatch(token) is None: + sys.stderr.write(redact(proc.stderr or "login refused\n")) + return "" + return token + + +def lookup(token: str) -> dict: + proc = run_bao(["token", "lookup", "-format=json"], token) + if proc.returncode != 0: + raise RuntimeError(redact(proc.stderr or "token lookup failed")) + payload = json.loads(proc.stdout) + return payload.get("data") or payload + + +def capabilities(operator: str, lane: str) -> dict[str, list[str]]: + if not operator: + raise RuntimeError( + "no operator token for sys/capabilities " + "(BAO_TOKEN, VAULT_TOKEN, or ~/.vault-token)" + ) + body = json.dumps({"token": lane, "paths": PATHS}) + tmp = tempfile.NamedTemporaryFile("w", prefix="rs-caps-", delete=False) + try: + os.fchmod(tmp.fileno(), stat.S_IRUSR | stat.S_IWUSR) + tmp.write(body) + tmp.close() + proc = run_bao(["write", "-format=json", "sys/capabilities", f"@{tmp.name}"], operator) + finally: + try: + os.remove(tmp.name) + except OSError: + pass + if proc.returncode != 0: + raise RuntimeError(redact(proc.stderr or "sys/capabilities failed")) + payload = json.loads(proc.stdout) + data = payload.get("data") or payload + found: dict[str, list[str]] = {} + for path in PATHS: + value = data.get(path) + if isinstance(value, list): + found[path] = value + return found + + +def revoke(token: str) -> bool: + proc = run_bao(["token", "revoke", "-self"], token) + return proc.returncode == 0 + + +def write_receipt(name: str, lines: list[str]) -> Path: + path = receipts_dir() / name + path.write_text("\n".join(lines) + "\n", encoding="utf-8") + path.chmod(0o600) + return path + + +def main(argv: list[str]) -> int: + mode = argv[1] if len(argv) > 1 else "" + if mode not in {"member", "non-member"}: + sys.stderr.write("usage: verify-reuse-surface-hub-writer.py member|non-member\n") + return 2 + sys.stderr.write( + f"Sign in as the {mode} identity in a private browser window.\n" + "A platform-admin session is the wrong principal for member.\n" + ) + operator = operator_token() + now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + lane = login() + report: dict = { + "mode": mode, + "login": "ok" if lane else "refused", + "policies": [], + "identity_policies": [], + "entity_id": "", + "capabilities": {}, + "revoked": None, + } + try: + if lane: + info = lookup(lane) + report["policies"] = list(info.get("policies") or []) + report["identity_policies"] = list(info.get("identity_policies") or []) + report["entity_id"] = str(info.get("entity_id") or "") + if mode == "member": + report["capabilities"] = capabilities(operator, lane) + except (RuntimeError, json.JSONDecodeError, OSError) as exc: + report["error"] = redact(str(exc)) + finally: + if lane: + report["revoked"] = revoke(lane) + lane = "" + failures = assess(report) + if report.get("error"): + failures = [report["error"], *failures] + lines = [ + f"mode={mode}", + f"at={now}", + f"addr={BAO_ADDR}", + f"role={ROLE}", + f"policy={POLICY}", + f"login={report['login']}", + f"entity_id={report['entity_id']}", + "policies=" + ",".join(report["policies"]), + "identity_policies=" + ",".join(report["identity_policies"]), + ] + for path in PATHS: + caps = (report["capabilities"] or {}).get(path) + shown = ",".join(caps) if isinstance(caps, list) else "not-run" + lines.append(f"{path}: {shown}") + lines.append("revoked=" + ("yes" if report["revoked"] else "no" if report["revoked"] is False else "n/a")) + lines.append("result=" + ("pass" if not failures else "fail")) + for item in failures: + lines.append("failure=" + item) + receipt = write_receipt(f"reuse-surface-hub-writer-{mode}.txt", lines) + sys.stderr.write(f"receipt: {receipt}\n") + return 0 if not failures else 1 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/src/ops_mason/reuse_surface_hub_writer.py b/src/ops_mason/reuse_surface_hub_writer.py new file mode 100644 index 0000000..58ded33 --- /dev/null +++ b/src/ops_mason/reuse_surface_hub_writer.py @@ -0,0 +1,89 @@ +"""Decide whether a reuse-surface hub-writer token is the dedicated caller. + +The check is metadata only. It never accepts a secret value, a login token, or +a command that reads ``platform/data/*``. +""" + +from __future__ import annotations + +POLICY = "workload-kv-read-reuse-surface-runtime" +ROLE = "reuse-surface-hub-writer-workload-kv-read" +FORBIDDEN_ENTITY_ID = "4a1f663c-0621-0483-edd0-f852274f4b6b" +FORBIDDEN_POLICIES = frozenset( + { + "default", + "platform-admin", + "operator-custody", + "workload-kv-read-reuse-surface-runtime-eso", + } +) + +DATA = "platform/data/workloads/reuse/reuse-surface/runtime-secrets" +METADATA = "platform/metadata/workloads/reuse/reuse-surface/runtime-secrets" +PARENT_DATA = "platform/data/workloads/reuse/reuse-surface" +PARENT_METADATA = "platform/metadata/workloads/reuse/reuse-surface" +SIBLING = "platform/data/workloads/issue-core/issue-core/issue-core-runtime" + +EXACT_READ = (DATA, METADATA) +MUST_DENY = (PARENT_DATA, PARENT_METADATA, SIBLING) + + +def _caps(report: dict, path: str) -> list[str]: + raw = (report.get("capabilities") or {}).get(path) + if not isinstance(raw, list): + return [] + return sorted(str(item) for item in raw) + + +def assess(report: dict) -> list[str]: + """Return the reasons a login report fails. An empty list is a pass. + + ``non-member`` passes only when login was refused. ``member`` passes only + when the token's policy set is exactly the workload policy, the token is + not the platform-admin entity, the two exact paths are read-only, the + parent and a sibling workload are denied, and the token was revoked. + """ + mode = report.get("mode") + if mode not in {"member", "non-member"}: + return [f"unknown mode {mode!r}"] + login = report.get("login") + if mode == "non-member": + if login == "refused": + return [] + return ["non-member login succeeded; the role accepted the wrong principal"] + + failures: list[str] = [] + if login != "ok": + failures.append(f"member login was {login!r}") + return failures + + policies = set(report.get("policies") or []) + identity_policies = set(report.get("identity_policies") or []) + effective = policies | identity_policies + if effective != {POLICY}: + failures.append( + "token policies are not exactly " + f"{POLICY} (effective={sorted(effective)})" + ) + overlap = effective & FORBIDDEN_POLICIES + if overlap: + failures.append(f"forbidden policies attached: {sorted(overlap)}") + entity_id = str(report.get("entity_id") or "") + if entity_id == FORBIDDEN_ENTITY_ID: + failures.append( + "token belongs to the platform-admin entity; do not attach this policy to it" + ) + if not entity_id: + failures.append("token lookup did not report an entity id") + + for path in EXACT_READ: + caps = _caps(report, path) + if caps != ["read"]: + failures.append(f"{path} capabilities {caps or ['']}, want ['read']") + for path in MUST_DENY: + caps = _caps(report, path) + if caps != ["deny"]: + failures.append(f"{path} capabilities {caps or ['']}, want ['deny']") + if report.get("revoked") is not True: + failures.append("token was not revoked") + return failures diff --git a/tests/test_reuse_surface_hub_writer.py b/tests/test_reuse_surface_hub_writer.py new file mode 100644 index 0000000..03f8e6a --- /dev/null +++ b/tests/test_reuse_surface_hub_writer.py @@ -0,0 +1,67 @@ +from ops_mason.reuse_surface_hub_writer import ( + DATA, + FORBIDDEN_ENTITY_ID, + METADATA, + PARENT_DATA, + POLICY, + SIBLING, + assess, +) + + +def member(**overrides): + report = { + "mode": "member", + "login": "ok", + "policies": [POLICY], + "identity_policies": [], + "entity_id": "11111111-1111-1111-1111-111111111111", + "capabilities": { + DATA: ["read"], + METADATA: ["read"], + PARENT_DATA: ["deny"], + "platform/metadata/workloads/reuse/reuse-surface": ["deny"], + SIBLING: ["deny"], + }, + "revoked": True, + } + report.update(overrides) + return report + + +def test_member_exact_policy_passes(): + assert assess(member()) == [] + + +def test_admin_entity_and_broad_capabilities_fail(): + failures = assess( + member( + policies=["default", "operator-custody", "platform-admin", POLICY], + identity_policies=["platform-admin"], + entity_id=FORBIDDEN_ENTITY_ID, + capabilities={ + DATA: ["create", "delete", "list", "read", "update"], + METADATA: ["create", "delete", "list", "read", "update"], + PARENT_DATA: ["create", "delete", "list", "read", "update"], + "platform/metadata/workloads/reuse/reuse-surface": ["read"], + SIBLING: ["create", "delete", "list", "read", "update"], + }, + ) + ) + assert any("platform-admin entity" in item for item in failures) + assert any("forbidden policies" in item for item in failures) + assert any(SIBLING in item for item in failures) + assert any("not exactly" in item for item in failures) + + +def test_missing_revoke_fails(): + assert any("not revoked" in item for item in assess(member(revoked=False))) + + +def test_non_member_refusal_passes_and_unexpected_login_fails(): + assert assess({"mode": "non-member", "login": "refused"}) == [] + assert assess({"mode": "non-member", "login": "ok"}) + + +def test_unknown_mode_fails(): + assert assess({"mode": "other"}) diff --git a/workplans/MASON-WP-0004-credential-inventory-descriptions.md b/workplans/MASON-WP-0004-credential-inventory-descriptions.md index f4b68a2..678b419 100644 --- a/workplans/MASON-WP-0004-credential-inventory-descriptions.md +++ b/workplans/MASON-WP-0004-credential-inventory-descriptions.md @@ -9,7 +9,7 @@ flavor: planning owner: codex topic_slug: custodian created: "2026-08-28" -updated: "2026-10-03" +updated: "2026-10-14" related: - MASON-WP-0003 - NK-WP-0033 @@ -149,7 +149,7 @@ id: MASON-WP-0004-T03 status: wait depends_on: [MASON-WP-0004-T01] blocked_on: owners -blocking_reason: "Owners fill the gaps in manifests/custody-descriptions.yaml for the 9 draft entries (asked 2026-10-09)." +blocking_reason: "operators/nextcloud base entry removed 2026-10-14 after the founder retired it. Remaining draft gaps still need their owners." priority: low state_hub_task_id: "fff26914-d854-5854-b350-d7241812fb04" ``` @@ -203,6 +203,18 @@ fluid-telegram four from the approved plan (draft until fluid-telegram confirms, message `da7e711c`); asked email-connect for its new path (message `acb3ff22`). +2026-10-10: railiance-platform (message `389906a5`) confirmed `operators/*` +metadata writes are refused on purpose. Descriptions for that mount stay in +this file and are not mirrored into OpenBao. `operators/nextcloud/backup` +keeps the file-backed fields. + +2026-10-14: railiance-platform (message `f67d4dea`) said the founder deleted +`operators/nextcloud` on 2026-10-10. Removed that draft from +`manifests/custody-descriptions.yaml`. `operators/nextcloud/backup` stays. +Still open: `keycape-factor-issuer` rotation and `user-engine/runtime` +on_loss, plus any undescribed paths from the 2026-10-10 inventory whose +owners have not answered. + ## Make an undescribed path visible before it accumulates ```task diff --git a/workplans/MASON-WP-0005-fluid-telegram-operator-credential-lane.md b/workplans/MASON-WP-0005-fluid-telegram-operator-credential-lane.md index 1c22456..f15dbd5 100644 --- a/workplans/MASON-WP-0005-fluid-telegram-operator-credential-lane.md +++ b/workplans/MASON-WP-0005-fluid-telegram-operator-credential-lane.md @@ -9,7 +9,7 @@ flavor: planning owner: codex topic_slug: helix-forge created: "2026-09-04" -updated: "2026-10-05" +updated: "2026-10-14" state_hub_workstream_id: "483e56d6-6601-5377-9066-66225214c046" --- @@ -218,7 +218,7 @@ id: MASON-WP-0005-T05 status: progress depends_on: [MASON-WP-0005-T04] blocked_on: human -blocking_reason: "ops-mason checks and the non-member refusal pass (2026-10-09). tegwick has no MFA factor: it enrolls through user-engine self-service MFA (message ab89a591, USER-WP-0028-T03), then the founder runs scripts/verify-fluid-telegram-lane.sh member; fluid-telegram runs the salt create-once probe (FT-WP-0002)." +blocking_reason: "ops-mason checks and the non-member refusal pass. tegwick enrolled TOTP on 2026-10-11 (message 36b443ab). The founder still runs scripts/verify-fluid-telegram-lane.sh member via the 127.0.0.1:18200 tunnel. fluid-telegram runs the salt create-once probe (FT-WP-0002)." priority: high state_hub_task_id: "4c205be7-4f1f-5dd1-aafa-fc112fdd640e" ``` @@ -266,6 +266,13 @@ deliver it (USER-WP-0028-T03 / USER-WP-0027-T04, decision cef97ccd), with `tegwick` as the first attended real-user acceptance, and to reply when `tegwick` has an active factor. +2026-10-14: user-engine reports the factor is active (message `36b443ab`). +`tegwick` enrolled TOTP at 22:22 on 2026-10-11 and passed the portal half of +U07 at 22:30. The OpenBao half is still the founder running +`scripts/verify-fluid-telegram-lane.sh member` through +`http://127.0.0.1:18200`. `coulomb-social` has `mfaRequired: false`, so it is +not the check. This session did not start that login. + ## Hand off routing and keep the adapter lane separate ```task diff --git a/workplans/MASON-WP-0011-reuse-surface-hub-writer.md b/workplans/MASON-WP-0011-reuse-surface-hub-writer.md new file mode 100644 index 0000000..d18b489 --- /dev/null +++ b/workplans/MASON-WP-0011-reuse-surface-hub-writer.md @@ -0,0 +1,98 @@ +--- +id: MASON-WP-0011 +type: workplan +title: "Name a dedicated caller for the reuse-surface hub writer token" +domain: infotech +repo: ops-mason +status: active +flavor: planning +owner: codex +topic_slug: custodian +created: "2026-10-14" +updated: "2026-10-14" +--- + +# Name a dedicated caller for the reuse-surface hub writer token + +## Goal + +Give CUST-WP-0081 an attended OpenBao caller whose token carries only +`workload-kv-read-reuse-surface-runtime` on the exact runtime-secrets data +and metadata paths, with parent and sibling paths denied. The platform-admin +entity `4a1f663c-0621-0483-edd0-f852274f4b6b` is not that caller. + +Demand: State Hub message `08b254af-59d4-4d1a-9544-fed3dcaaf213` from +the-custodian, workplan CUST-WP-0081. + +## Boundaries + +- ops-mason writes the plan, the draft CCR, the role document, and the + verifier. It does not install a policy or a role, and it does not read + `REUSE_SURFACE_TOKEN`. +- railiance-platform owns the OpenBao apply. CCR-2026-0005 stays the + authority for the existing secret and the ESO lane. +- key-cape owns group `reuse-surface-hub-writers` and the member principal. +- ops-warden keeps catalog `reuse-surface-hub-write-token`. It does not + receive a secret value. It names the role only after both verifier modes + pass. + +## Write the caller plan + +```task +id: MASON-WP-0011-T01 +status: done +priority: high +``` + +Record why the admin entity cannot carry the workload policy, reuse the +existing KV path and policy, and publish the draft role, CCR, and verifier. + +Done 2026-10-14. Plan `plans/reuse-surface-hub-writer.md` (status draft). +Bundle `builds/reuse-surface-hub-writer/`. Verifier +`scripts/verify-reuse-surface-hub-writer.py`. No live login and no KV read. + +## Confirm the group and the member entity + +```task +id: MASON-WP-0011-T02 +status: wait +depends_on: [MASON-WP-0011-T01] +blocked_on: message-from:key-cape +blocking_reason: "key-cape confirms group reuse-surface-hub-writers and a member entity that is not 4a1f663c-0621-0483-edd0-f852274f4b6b and has no platform-admin or operator-custody identity policy." +priority: high +``` + +Ask key-cape to create or confirm the group and to name the member entity +id without sending a credential. `bound_claims_confirmed` stays false until +that reply. railiance-platform's two approvers still have to accept the CCR. + +## Apply the role without a KV write + +```task +id: MASON-WP-0011-T03 +status: wait +depends_on: [MASON-WP-0011-T02] +blocked_on: human +blocking_reason: "After T02. Founder applies the CCR through the delegated applier. No bao kv put. Do not attach the policy to the admin entity." +priority: high +``` + +railiance-platform assigns the real CCR id, copies the policy file over the +existing `workload-kv-read-reuse-surface-runtime` policy, and applies the +role. The KV value is already present under CCR-2026-0005. + +## Record the sanitized receipt + +```task +id: MASON-WP-0011-T04 +status: wait +depends_on: [MASON-WP-0011-T03] +blocked_on: human +blocking_reason: "After T03. Founder runs scripts/verify-reuse-surface-hub-writer.py member and non-member. A live receipt does not exist yet." +priority: high +``` + +Both modes must pass. Copy the mode-0600 receipts into `docs/evidence/` +with no token and no field value, reply to message `08b254af`, and tell +ops-warden the role name for `auth_method`. Hub registration itself stays +with the-custodian.