ops-mason/plans/inbound-library-imap-approle.md
tegwick 4e3fc02f86 Close the inbound-library IMAP AppRole lane: built, verified, routed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 1170398@bnt-lap001
Assistant-Session: 622cd705-cb31-4322-ba08-dd42e44fa583
2026-10-05 16:14:02 +02:00

12 KiB

id demand_source consumer_repo credential_type status approved_by approved_at approved_spec_sha256 created updated build_spec
inbound-library-imap-approle inbound-library/workplans/ILIB-WP-0003-T01 inbound-library openbao-approle-kv built Bernd Worsch 2026-10-05 c62765c8a4505821e77b58c5e0daf0511dec39fb569a05e190ff51f5cdc3e2ba 2026-10-05 2026-10-05
engine policy_name kv_path approle_name token_num_uses kv_capabilities token_ttl token_max_ttl secret_id_ttl delivery_dir bao_bin audit_log_path reuse_policy reuse_policy_sha256
openbao-approle-kv workload-kv-read-inbound-library-imap platform/workloads/inbound-library/imap inbound-library 4
read
15m 30m 0 /home/worsch/.local/inbound-library/approle bao /home/worsch/ops-mason/audit/build-log.jsonl false null

Construction plan: inbound-library IMAP AppRole

1. Demand

inbound-library collects material delivered by email for the rest of the ecosystem. Its first use is the daily and weekly Grok and ChatGPT intelligence briefs for intelligence-radar. A systemd user timer on the workstation reads the dedicated mailbox library@coulomb.social (IONOS, imap.ionos.de:993) read-only every 15 minutes. It must authenticate without an operator, and the founder ruled out caching the password in a file. On every run it should log in to OpenBao, read the IMAP entry once, and revoke its token. The consumer code is already implemented and tested against a fake OpenBao (inbound_library/vault.py, ILIB-WP-0003-T04).

The KV entry already exists. railiance-platform/scripts/inbound_library_imap_entry.py created it password-free, and the founder added IMAP_PASSWORD as version 2 (railiance-platform/docs/inbound-library-imap-custody.md). This plan only creates the read path to it.

2. Existing-structure survey

Generated by scripts/plan-survey.py at 2026-10-05T12:31:21Z against BAO_ADDR=http://127.0.0.1:18200, token display_name token-claude-MASON-WP-0008-T05 (policies: default, ops-mason-build). Metadata only: no kv get and no */data/* read was issued; policy bodies were read with bao policy read for name-matched candidates only.

Demand: KV path platform/workloads/inbound-library/imap, proposed policy workload-kv-read-inbound-library-imap, AppRole inbound-library, Kubernetes role -, consumer inbound-library. Match terms: inbound-library, imap.

bao secrets list (10 mounts, 1 relevant)

platform/                kv         version=2

bao auth list (5 auth mounts, 2 relevant)

approle/                 approle
kubernetes/              kubernetes

bao policy list (76 policies, 1 relevant)

workload-kv-read-binky-company-email-imap                matches: imap

bao list auth/approle/role (approle) (13 roles, 0 relevant)

(no relevant entries)

bao list auth/kubernetes/role (kubernetes) (25 roles, 0 relevant)

(no relevant entries)

bao policy read of name-matched candidates (target platform/data/workloads/inbound-library/imap)

workload-kv-read-binky-company-email-imap: no stanza covers platform/data/workloads/inbound-library/imap

bao kv metadata get / bao kv list (metadata only)

platform/workloads/inbound-library/imap: exists, current_version=2, created=2026-10-05, updated=2026-10-05
platform/workloads/inbound-library/: imap

ops-warden routing catalog /home/worsch/ops-warden/registry/routing/catalog.yaml (40 entries, 2 relevant)

binky-company-email-imap  owner=railiance-platform  status=active  matches: imap
    path_template: tenants/binky/company-email/imap
    auth_method: caller's own OpenBao token (OIDC netkingdom role binky-company-email-imap-workload-kv-read)
agent-harness-binky-mail-approle  owner=railiance-platform  status=active  matches: imap
    path_template: tenants/binky/company-email/imap
    auth_method: AppRole login role=agent-harness-binky-mail (role_id+secret_id on worker host via EXECUTOR_APPROLE_DIR)

Does an existing lane already satisfy this demand?

Draft answer: partial. Review before committing; this is generated, not a decision.

  • KV path platform/workloads/inbound-library/imap already exists (v2)
  • policy grants were checked only for name-matched candidates; a policy with an unrelated name that covers platform/data/workloads/inbound-library/imap is not detected

Reviewed answer: no existing lane satisfies this demand. The survey's "partial" refers only to the KV entry, which already exists and is not touched by this plan. The two IMAP lanes it found (binky-company-email-imap, agent-harness-binky-mail-approle) serve a different mailbox, on mount tenants/, for a different tenant. Binding inbound-library to their policy would grant it the Binky company mailbox, and adding this path to their policy would grant the Binky consumers the library mailbox. Both would widen scope. No policy name in the 76 listed names covers the path, and no AppRole or Kubernetes role is named for the consumer. The survey only inspects policies whose names match, so a policy with an unrelated name covering platform/data/workloads/inbound-library/imap would be missed. Any such grant would be a pre-existing over-grant, and this plan does not depend on it.

Live precedent settings, read with the same grant (bao read auth/approle/role/<name>): agent-harness-binky-mail and rein-openweights both have token_ttl=900, token_max_ttl=1800, token_num_uses=8, secret_id_ttl=0, secret_id_num_uses=0 and bind_secret_id=true, with no CIDR binding.

3. Proposed changes

  • create policy workload-kv-read-inbound-library-imap: read on platform/data/workloads/inbound-library/imap and platform/metadata/workloads/inbound-library/imap, nothing else. It is new because, per section 2, no existing policy is scoped to this entry, and reusing a Binky policy would widen scope in both directions.
  • create AppRole inbound-library with token_policies = [workload-kv-read-inbound-library-imap], token_ttl=15m, token_max_ttl=30m, token_num_uses=4, secret_id_ttl=0. It is new because no role exists for the consumer. Its shape follows the two workstation precedents.
  • create the delivery files /home/worsch/.local/inbound-library/approle/role_id and secret_id (private, exclusive), at the location config/library.json credential.approle_dir already points to.
  • unchanged: the KV entry itself, every other policy, role and mount, and the ops-warden catalog. A pointer-only entry is proposed after the build, in ILIB-WP-0003-T05.

4. Review notes (phase 2)

  • Naming: workload-kv-read-<consumer>-<purpose> matches the workload-kv-read-binky-company-email-imap and workload-kv-read-rein-openweights-openrouter convention. The AppRole is named after the consumer, as rein-openweights is.
  • TTL: 15m/30m matches both precedents. One run takes seconds.
  • token_num_uses=4 instead of the precedents' 8: the consumer uses exactly two operations per run, the KV read and auth/token/revoke-self. 4 allows one retry and is tighter than the precedent. Below 2 would break revoke-self.
  • secret_id_ttl=0 (no expiry) matches the current build-phase precedent, and the builder requires it as an explicit, reviewable choice. Alternative: a bounded secret-id TTL with a rotation job. That is not proposed now because the consumer has no rotation path yet. It is recorded as the residual for a fleet-wide rotation decision, not one made for this lane alone.
  • CIDR binding: not used. The precedents do not use it, and the workstation reaches OpenBao through the openbao-ui-railiance01 bridge forward (127.0.0.1:18200), so the source address OpenBao sees is the tunnel endpoint. A binding there would add little.
  • Redundancy and compaction: nothing to compact. The survey found no near duplicate on platform/.
  • Value handling: ops-mason never reads the IMAP value. Proof follows the binky-company-email-imap CCR pattern (capabilities-safe): the lane token is checked for read on the data path, and a default-policy token for deny, using sys/capabilities. The consumer's first live run proves the value works (ILIB-WP-0003-T06).
  • Spec digest: build_spec_digest(build_spec) = c62765c8a4505821e77b58c5e0daf0511dec39fb569a05e190ff51f5cdc3e2ba. This is a candidate only. Record it as approved_spec_sha256 only on approval.

5. Executive summary (phase 3)

  1. One-line ask. Let the inbound-library timer on the workstation read the password for the library@coulomb.social mailbox from OpenBao by itself, so no copy of it is kept on disk.
  2. Who/what gets access. One new machine identity, the AppRole inbound-library, whose login files live only on the workstation. Nothing else gains anything.
  3. To what. Read-only access to exactly one entry: the library mailbox credential. No other entry, mailbox or tenant.
  4. For how long. Each login gives a token valid for 15 minutes (30 at most), usable for 4 operations, and the library revokes it after each run. The login files do not expire. That is the same build-phase choice as the existing workstation lanes, and their rotation is a separate, fleet-wide decision.
  5. Blast radius if the credential leaks. Whoever holds the two login files can read the library mailbox password, and with it read, and through IMAP change or delete, mail in library@coulomb.social. That mailbox only receives automation briefs and similar inbound material. It is not a personal or company mailbox, and it gives no access to other OpenBao entries.
  6. Cost to reverse. Fully isolated. Deleting one role and one policy, and removing the two login files, touches no other consumer. The mailbox password can be rotated independently at IONOS.

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.