diff --git a/plans/fluid-telegram-operator-credential-lane.md b/plans/fluid-telegram-operator-credential-lane.md new file mode 100644 index 0000000..368fcd6 --- /dev/null +++ b/plans/fluid-telegram-operator-credential-lane.md @@ -0,0 +1,183 @@ +--- +id: fluid-telegram-operator-credential-lane +demand_source: fluid-telegram/workplans/FT-WP-0002 and State Hub message 287cf897-7f47-4bcc-87d6-085f43d5f03e +consumer_repo: fluid-telegram +credential_type: openbao-oidc-kv-read-write +status: draft +approved_by: null +approved_at: null +created: "2026-09-04" +updated: "2026-09-04" +--- + +# Construction plan: fluid-telegram operator credential lane + +## 1. Demand + +`fluid-telegram` has an attended provisioner that creates a Telegram bot and +channels from a declared campaign specification. The provisioner needs an +operator OIDC token that reads four campaign-specific KV entries, writes the +MTProto session and bot token, and creates the redaction salt once. The +MTProto session is a full-account credential, so this is a high-risk operator +lane, never an autonomous agent or adapter lane. ops-mason constructs only the +OpenBao structure and never handles Telegram values. + +The first consumer is campaign `hall-of-helix`. A future adapter has a +different, narrower need and is intentionally excluded from this plan. + +## 2. Existing-structure survey + +### Disk survey completed 2026-09-04 + +- `railiance-platform/docs/workload-kv-access-lanes.md` documents attended + OIDC through `auth/netkingdom`, exact group-bound roles, three callback URIs, + and 15-minute tokens. +- The nearest concrete tenant-shaped path is + `platform/workloads/coulomb/whynot-design/npm-publish`. Its CCR history says + the owner corrected the first segment to the organisation/tenant, not the + State Hub domain. +- Existing policy files contain no `fluid-telegram` lane. +- `ops-warden/registry/routing/catalog.yaml` contains only the generic + `openbao-api-key` template for this need, and its login string omits + `-path=netkingdom`. No concrete Telegram lane exists. +- `ops-mason/src/ops_mason/executor.py` supports AppRole and Kubernetes KV + lanes. It has no OIDC role writer and its generic policy helper emits the + same capabilities for one data and metadata path, which cannot express this + four-path matrix. +- `railiance-platform/schemas/credential-change-request.schema.yaml` permits + only `request_type: workload-kv-read`; its validator also requires the + `workload-kv-read-` policy prefix and `-workload-kv-read` OIDC role suffix. + +### Live survey pending + +No valid scoped OpenBao session was available during intake, so the required +live outputs from `bao secrets list`, `bao auth list`, `bao policy list`, and +the relevant `auth/netkingdom/role/*` reads are not yet attached. Phase 2 must +repeat the survey with `./scripts/bao-session.sh grant MASON-WP-0005-T01`. + +**Existing-lane answer:** none is known from source state. This remains a +draft finding until the live survey confirms it. + +## 3. Proposed changes + +The recommended CLI prefix is +`platform/workloads/coulomb/fluid-telegram/hall-of-helix`. `coulomb` is a +recommendation from the existing owner precedent, not an authorization; T01 +must replace this paragraph with railiance-platform's explicit decision before +review. + +The recommended objects are: + +| # | Action | Object | Rationale | +|---|---|---|---| +| 1 | create | Policy `workload-kv-fluid-telegram-hall-of-helix-operator` | A new exact-path matrix is required; no read-only lane can satisfy the writes. | +| 2 | create | OIDC role `fluid-telegram-hall-of-helix-workload-kv` at `auth/netkingdom` | The caller is an attended operator; AppRole and Kubernetes auth are the wrong identity shapes. | +| 3 | modify | ops-mason executor with an approved OIDC KV matrix spec | The current executor cannot safely express OIDC or per-path capabilities. | +| 4 | modify | railiance-platform CCR schema/validator with an approved read/write request type and naming rules | The platform owner explicitly rejects this shape today; bypassing that gate is forbidden. | +| 5 | modify | railiance-platform agent high-risk boundary with exact data and metadata denies for all four paths | Agent-bound identities must not inherit this lane through another policy. | +| 6 | propose | ops-warden entry `fluid-telegram-operator`, pointer-only, `status: draft`, `resolvable: false` | A concrete selector is useful before activation; no placeholder prefix becomes routable. | + +### Exact policy matrix + +All entries are individual KV-v2 API paths. There is no `*` grant, parent +`list`, metadata mutation, delete, destroy, undelete, or sudo capability. + +| CLI entry | KV-v2 data-path capabilities | Risk and reason | +|---|---|---| +| `.../operator-app` | `read` | High confidentiality: Telegram application id/hash; entered only through sanctioned attended value entry. | +| `.../operator-session` | `create,read,update` | High: full-account MTProto credential. Provisioning only; never adapter-visible. | +| `.../bot-token` | `create,read,update` | High: controls Bot API publication. Provisioner writes immediately after BotFather issues it. | +| `.../redaction-salt` | `create,read` | High integrity and sensitive pseudonymisation material. Overwrite, patch, and deletion are denied. | + +OpenBao KV v2 documents that it honors the ACL distinction between `create` +and `update`; it also documents CAS `0` as create-if-absent. The fluid-telegram +client currently performs a read followed by a normal POST. The ACL therefore +supplies the decisive server-side write-once boundary: the first POST is a +create and a later POST is an update, which this policy denies. The client +should still add CAS `0` to make its intent race-safe and explicit, but this +plan does not depend on client-only behavior. + +This is operational write-once for the provisioner, not immutable storage. +A platform administrator can recover from a corrupt first write through a +separately approved attended action. That action must preserve the old version +until recovery is verified and must never be exposed through this OIDC role. + +### OIDC role shape + +- auth mount: `netkingdom` +- role: `fluid-telegram-hall-of-helix-workload-kv` +- token policies: exactly + `workload-kv-fluid-telegram-hall-of-helix-operator`; set + `token_no_default_policy=true` +- role type: `oidc` +- token TTL: `15m`, maximum TTL `30m` +- user claim: `sub`; groups claim: `groups` +- scopes: `openid,profile,email,groups` +- proposed bound claim: `groups=fluid-telegram-operators` — **unconfirmed** +- callbacks: Bao UI plus localhost and loopback CLI callbacks already accepted + by the platform contract + +The role is campaign-specific because its policy is campaign-specific. A +second campaign receives a second policy and role after its own review; it is +not added below this prefix. + +### Value flow + +- An attended operator enters `operator-app` with the existing sanctioned + paste-once/value-entry mechanism. +- The provisioner creates or updates `operator-session` and `bot-token`. +- The provisioner creates `redaction-salt` once. +- ops-mason records paths, policy/auth metadata, capabilities, versions, + timestamps, and audit references only. It never requests or reads a value. + +## 4. Review notes (phase 2) + +This section is incomplete while T01 and T02 are open; the plan must remain +`draft`. + +- **Reuse:** reuse the existing `netkingdom` auth mount, callback set, group + claim pattern, 15-minute TTL, and KV-v2 mount. Create only the policy and + role that do not exist. +- **Least privilege:** exact entry paths are safer than one campaign-prefix + wildcard and are necessary for the salt's create-only rule. The role has no + access to KV metadata because the provisioner uses data GETs to detect + absence; metadata evidence comes from the separately scoped builder/operator + verification path. +- **Identity binding:** an unconfirmed group is a hard blocker, not a warning. + The role must not be readable by every OIDC user. +- **Write-once semantics:** KV-v2 create/update distinction is documented, but + activation also requires an owner-run non-secret probe proving the installed + engine behaves as expected. The resulting evidence contains no probe value. +- **Agent boundary:** explicit denies must cover all four data paths and their + metadata paths. The operator-session path is the decisive negative check. +- **Adapter separation:** `MASON-IN-0003` tracks a later runtime lane that + reads only `bot-token` and `redaction-salt`; no role or policy in this plan is + reused by the adapter. +- **Reversal:** disable/delete the OIDC role first, then retire the policy. KV + entries remain in operator custody for an explicit retention/recovery + decision; teardown does not silently delete values. +- **Implementation gates:** both ops-mason and railiance-platform must extend + their current read-lane tooling before construction. Direct `bao policy + write`/`bao write auth/netkingdom/role/...` commands outside those approved + surfaces are not an alternative. + +### Verification required before routing activation + +1. Positive capabilities on each exact path match the matrix above. +2. Parent, sibling workload, and sibling campaign paths report `deny`. +3. A non-member OIDC identity cannot obtain the role's policy. +4. Agent-bound identities report `deny` on all four data and metadata paths. +5. An owner-run disposable KV-v2 probe proves create succeeds and a second + write with the same policy is denied; only timestamps, versions, path, and + pass/fail are returned. +6. The value-entry/provisioner flow confirms all required entries exist using + metadata/version evidence only. +7. ops-warden keeps `fluid-telegram-operator` non-resolvable until all checks + pass and the exact built pointers are returned. + +## 5. Executive summary (phase 3) + +Not yet renderable. The owner-approved tenant segment, OIDC bound claim, live +structure survey, CCR/executor extension, and installed-engine create/update +probe are open inputs. When those close, this section must ask for one explicit +approval covering the exact objects and capability matrix above. diff --git a/plans/inbound-library-imap-approle.md b/plans/inbound-library-imap-approle.md index cac171a..c26cd05 100644 --- a/plans/inbound-library-imap-approle.md +++ b/plans/inbound-library-imap-approle.md @@ -3,7 +3,7 @@ id: inbound-library-imap-approle demand_source: inbound-library/workplans/ILIB-WP-0003-T01 consumer_repo: inbound-library credential_type: openbao-approle-kv -status: approved +status: built approved_by: "Bernd Worsch" approved_at: "2026-10-05" approved_spec_sha256: "c62765c8a4505821e77b58c5e0daf0511dec39fb569a05e190ff51f5cdc3e2ba" @@ -207,3 +207,32 @@ binding. **Decision:** approve / reject / send back to phase 1 with what to change. **Decision 2026-10-05: approved** by Bernd Worsch in a Claude Code session ("I approve"), for the exact build_spec above (digest `c62765c8…e2ba`). + +## 6. Build result (phase 4) + +Built 2026-10-05 with `build_approle_kv_lane` under the grant +`token-claude-ILIB-WP-0003-T03` (ops-mason-build). The builder reloaded the plan +and matched approval and digest `c62765c8…e2ba` before any OpenBao command. It +added a metadata-only record to `audit/build-log.jsonl`. + +- **created** policy `workload-kv-read-inbound-library-imap`: `read` on + `platform/data/workloads/inbound-library/imap` and + `platform/metadata/workloads/inbound-library/imap`, and nothing else. + Confirmed with `bao policy read`. +- **created** AppRole `inbound-library`: `token_policies=[workload-kv-read-inbound-library-imap]`, + `token_ttl=900`, `token_max_ttl=1800`, `token_num_uses=4`, `secret_id_ttl=0`, + `bind_secret_id=true`. Confirmed with `bao read`. +- **delivered** `role_id` and `secret_id` to `/home/worsch/.local/inbound-library/approle/` + (directory 0700, files 0600). The values were not printed or read by the agent. + +Capabilities-safe verification: +- **Negative:** a `default`-only token (2 minutes, 2 uses, then revoked) gets + `deny` on the data path. +- **Positive:** proven by the consumer, not by ops-mason, which reads no values. + The first systemd run of `inbound-library.service` (2026-10-05 16:09 CEST) did + the AppRole login, read the entry, revoked its token, and scanned + `library@coulomb.social` read-only. It retained 2 messages. No secret appeared + in the journal. + +The ops-warden catalog entry is proposed under inbound-library ILIB-WP-0003-T05. +No paste-once step remains: the value (version 2) was supplied by the founder before the build.