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
This commit is contained in:
parent
6434877a44
commit
fd2c5a5904
12 changed files with 841 additions and 17 deletions
|
|
@ -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 -- <silent helper> 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.
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"]
|
||||
}
|
||||
30
docs/reuse-surface-hub-writer-attended.md
Normal file
30
docs/reuse-surface-hub-writer-attended.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
166
plans/reuse-surface-hub-writer.md
Normal file
166
plans/reuse-surface-hub-writer.md
Normal file
|
|
@ -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 -- <silent helper>
|
||||
```
|
||||
|
||||
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.
|
||||
212
scripts/verify-reuse-surface-hub-writer.py
Normal file
212
scripts/verify-reuse-surface-hub-writer.py
Normal file
|
|
@ -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))
|
||||
89
src/ops_mason/reuse_surface_hub_writer.py
Normal file
89
src/ops_mason/reuse_surface_hub_writer.py
Normal file
|
|
@ -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 ['<missing>']}, want ['read']")
|
||||
for path in MUST_DENY:
|
||||
caps = _caps(report, path)
|
||||
if caps != ["deny"]:
|
||||
failures.append(f"{path} capabilities {caps or ['<missing>']}, want ['deny']")
|
||||
if report.get("revoked") is not True:
|
||||
failures.append("token was not revoked")
|
||||
return failures
|
||||
67
tests/test_reuse_surface_hub_writer.py
Normal file
67
tests/test_reuse_surface_hub_writer.py
Normal file
|
|
@ -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"})
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
98
workplans/MASON-WP-0011-reuse-surface-hub-writer.md
Normal file
98
workplans/MASON-WP-0011-reuse-surface-hub-writer.md
Normal file
|
|
@ -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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue