Plan the inbound-library IMAP AppRole lane (reviewed, awaiting approval).

Demand: inbound-library ILIB-WP-0003-T01. First live plan-survey run
(MASON-WP-0008-T05) included verbatim in section 2.

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
This commit is contained in:
tegwick 2026-10-05 14:32:29 +02:00
parent b9c9f082cf
commit f7d5624f56

View file

@ -0,0 +1,207 @@
---
id: inbound-library-imap-approle
demand_source: inbound-library/workplans/ILIB-WP-0003-T01
consumer_repo: inbound-library
credential_type: openbao-approle-kv
status: reviewed
approved_by: null
approved_at: null
approved_spec_sha256: null
created: "2026-10-05"
updated: "2026-10-05"
build_spec:
engine: openbao-approle-kv
policy_name: workload-kv-read-inbound-library-imap
kv_path: platform/workloads/inbound-library/imap
approle_name: inbound-library
token_num_uses: 4
kv_capabilities:
- read
token_ttl: 15m
token_max_ttl: 30m
secret_id_ttl: '0'
delivery_dir: /home/worsch/.local/inbound-library/approle
bao_bin: bao
audit_log_path: /home/worsch/ops-mason/audit/build-log.jsonl
reuse_policy: false
reuse_policy_sha256: 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)
```text
platform/ kv version=2
```
### `bao auth list` (5 auth mounts, 2 relevant)
```text
approle/ approle
kubernetes/ kubernetes
```
### `bao policy list` (76 policies, 1 relevant)
```text
workload-kv-read-binky-company-email-imap matches: imap
```
### `bao list auth/approle/role` (approle) (13 roles, 0 relevant)
```text
(no relevant entries)
```
### `bao list auth/kubernetes/role` (kubernetes) (25 roles, 0 relevant)
```text
(no relevant entries)
```
### `bao policy read` of name-matched candidates (target `platform/data/workloads/inbound-library/imap`)
```text
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)
```text
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)
```text
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.