Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 278674@bnt-lap001 Assistant-Session: 64827407-e196-4ce3-b70c-76e7b537493f
255 lines
14 KiB
Markdown
255 lines
14 KiB
Markdown
---
|
||
id: mas-tem-relay-smtp
|
||
demand_source: mail-address-semantics/workplans/MAS-WP-0003-T03
|
||
consumer_repo: mail-address-semantics
|
||
credential_type: openbao-kv-founder-provision
|
||
status: reviewed
|
||
approved_by: null
|
||
approved_at: null
|
||
created: "2026-10-18"
|
||
updated: "2026-10-18"
|
||
---
|
||
|
||
# Construction plan: MAS live-test TEM SMTP relay credential
|
||
|
||
## 1. Demand
|
||
|
||
`MAS-WP-0003-T03` runs a temporary live-test MX (`mas/mas-mx`) on
|
||
railiance01. It sends service replies through Scaleway Transactional Email
|
||
at `[smtp.tem.scaleway.com]:587`, because outbound port 25 is blocked on the
|
||
node. The MX needs one SMTP credential: `username` (the Scaleway project ID,
|
||
which is not secret) and `password` (a Scaleway API secret key). The founder
|
||
types the value into the OpenBao UI. ops-mason builds an empty home and a
|
||
read path into namespace `mas`, delivered as Secret `mas-relay`, which the MX
|
||
mounts optionally at `/etc/mas-relay`. The lane is temporary: it is removed
|
||
with the `mas` namespace when MAS-WP-0003 closes.
|
||
|
||
Requested by hub message `14bdd217` (2026-10-18). The request suggested
|
||
`kv/mas/tem-relay`; it is placed at `platform/suppliers/scaleway/mas-tem-relay`
|
||
instead (supplier convention). It also asked for a ClusterSecretStore `openbao-mas` and an ESO token.
|
||
This plan keeps the shape but adapts it to the current conventions (§3).
|
||
|
||
## 2. Existing-structure survey
|
||
|
||
Generated by `scripts/plan-survey.py` at 2026-10-18T10:13:55Z against `BAO_ADDR=http://127.0.0.1:18200`, token display_name `token-claude-MASON-WP-0014-T02` (policies: default, ops-mason-survey). 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/suppliers/scaleway/mas-tem-relay`, proposed policy `workload-kv-read-mas-tem-relay`, AppRole `-`, Kubernetes role `external-secrets-mas-tem-relay`, consumer `mail-address-semantics`. Match terms: `mail-address-semantics`, `external-secrets-mas-tem-relay`, `tem`, `smtp`, `mas`, `suppliers`, `scaleway`, `mas-tem-relay`.
|
||
|
||
### `bao secrets list` (10 mounts, 1 relevant)
|
||
|
||
```text
|
||
platform/ kv version=2
|
||
```
|
||
|
||
### `bao auth list` (6 auth mounts, 2 relevant)
|
||
|
||
```text
|
||
approle/ approle
|
||
kubernetes/ kubernetes
|
||
```
|
||
|
||
### `bao policy list` (94 policies, 5 relevant)
|
||
|
||
```text
|
||
authelia-smtp-eso matches: smtp
|
||
operator-kv-scaleway-bootstrap matches: scaleway
|
||
ops-mason-construct matches: mas
|
||
ops-mason-survey matches: mas
|
||
telemetry-smtp-eso matches: smtp
|
||
```
|
||
|
||
### `bao list auth/approle/role` (approle) (14 roles, 0 relevant)
|
||
|
||
```text
|
||
(no relevant entries)
|
||
```
|
||
|
||
### `bao list auth/kubernetes/role` (kubernetes) (35 roles, 2 relevant)
|
||
|
||
```text
|
||
authelia-smtp-eso matches: smtp
|
||
token_policies=['authelia-smtp-eso']
|
||
token_ttl=900
|
||
token_max_ttl=900
|
||
token_num_uses=0
|
||
bound_service_account_names=['authelia-smtp-eso']
|
||
bound_service_account_namespaces=['sso']
|
||
audience=openbao
|
||
telemetry-smtp-eso matches: smtp
|
||
token_policies=['telemetry-smtp-eso']
|
||
token_ttl=900
|
||
token_max_ttl=900
|
||
token_num_uses=0
|
||
bound_service_account_names=['telemetry-smtp-eso']
|
||
bound_service_account_namespaces=['telemetry']
|
||
audience=openbao
|
||
```
|
||
|
||
### `bao policy read` of name-matched candidates (target `platform/data/suppliers/scaleway/mas-tem-relay`)
|
||
|
||
```text
|
||
authelia-smtp-eso: no stanza covers platform/data/suppliers/scaleway/mas-tem-relay
|
||
operator-kv-scaleway-bootstrap: no stanza covers platform/data/suppliers/scaleway/mas-tem-relay
|
||
ops-mason-construct: path "platform/data/suppliers/*" capabilities=['create']
|
||
ops-mason-construct: path "platform/metadata/suppliers/*" capabilities=['create', 'read', 'update', 'delete', 'list']
|
||
ops-mason-construct: path "platform/metadata/*" capabilities=['read', 'list']
|
||
ops-mason-construct: path "platform/data/*" capabilities=['deny']
|
||
ops-mason-survey: not permitted by ops-mason-survey
|
||
telemetry-smtp-eso: no stanza covers platform/data/suppliers/scaleway/mas-tem-relay
|
||
```
|
||
|
||
### `bao kv metadata get` / `bao kv list` (metadata only)
|
||
|
||
```text
|
||
platform/suppliers/scaleway/mas-tem-relay: exists, current_version=2, created=2026-10-18, updated=2026-10-18
|
||
owner: railiance-platform
|
||
platform/suppliers/scaleway/: backup, bootstrap, mas-tem-relay
|
||
```
|
||
|
||
### ops-warden routing catalog `/home/worsch/ops-warden/registry/routing/catalog.yaml` (48 entries, 10 relevant)
|
||
|
||
```text
|
||
state-hub-forge-derivation-read owner=ops-mason status=active matches: mas
|
||
path_template: platform/workloads/state-hub/forge-derivation (field FORGE_READ_TOKEN)
|
||
auth_method: Kubernetes auth (role state-hub-forge-derivation, ServiceAccount state-hub/state-hub) from the in-cluster state-hub workload
|
||
object-storage-sts owner=net-kingdom status=draft matches: tem
|
||
rein-openweights-openrouter-approle owner=ops-mason status=active matches: mas
|
||
path_template: reins/rein-openweights/openrouter
|
||
auth_method: AppRole login role=rein-openweights (role_id+secret_id via REIN_OPENWEIGHTS_APPROLE_DIR)
|
||
inbound-library-imap-approle owner=ops-mason status=active matches: mas
|
||
path_template: platform/workloads/inbound-library/imap
|
||
auth_method: AppRole login role=inbound-library (role_id+secret_id in ~/.local/inbound-library/approle, config/library.json credential.approle_dir)
|
||
audit-core-senders owner=ops-mason status=active matches: mas
|
||
path_template: platform/workloads/audit-core/senders
|
||
policy_ref: external-secrets-audit-core
|
||
auth_method: OpenBao KV v2 custody; audit-core reads via ESO (policy external-secrets-audit-core, token auth). Operators and agents never read the registry; a sender gets its own exact-path projection.
|
||
email-connect-transactional owner=railiance-platform status=active matches: smtp
|
||
path_template: platform/workloads/email-connect/transactional
|
||
policy_ref: flex-auth check secret.read:email-connect
|
||
auth_method: caller's own OpenBao token (operator OIDC, or ESO child token openbao-email-connect-eso-token)
|
||
scaleway-bootstrap owner=railiance-platform status=draft matches: mas, scaleway
|
||
path_template: platform/workloads/railiance/scaleway/bootstrap
|
||
auth_method: caller's own OpenBao token (founder / operator workstation)
|
||
openbao-structure-survey owner=ops-mason status=active matches: mas
|
||
auth_method: founder-minted scoped grant: ops-mason scripts/bao-session.sh grant <task> (policy ops-mason-survey, 45m, display_name claude-<task>)
|
||
platform-supplier-credentials owner=railiance-platform status=active matches: suppliers
|
||
path_template: platform/suppliers/<supplier>/<account>
|
||
auth_method: railiance-platform delegated credential change (CCR); founder-attended provision from a mode-0600 file
|
||
fluid-telegram-operator owner=ops-mason status=draft matches: mas
|
||
path_template: platform/workloads/coulomb/fluid-telegram/hall-of-helix/{operator-app,operator-session,bot-token,redaction-salt}
|
||
auth_method: bao login -method=oidc -path=netkingdom role=fluid-telegram-hall-of-helix-workload-kv (members of fluid-telegram-operators, aal2, 15m token)
|
||
```
|
||
|
||
|
||
### Does an existing lane already satisfy this demand?
|
||
|
||
**Decision (review 2026-10-18): no.** Edited from the generated "partial".
|
||
|
||
- The KV path exists because ops-mason created the placeholder (v1,
|
||
10:03Z), and the founder wrote the TEM value as v2 (10:11Z). That is the
|
||
home this plan builds, not an existing lane.
|
||
- No policy or role reads `platform/data/suppliers/scaleway/mas-tem-relay`.
|
||
The only name-matched policy that touches the path is `ops-mason-construct`,
|
||
which has create-only access and cannot read.
|
||
- The two SMTP ESO lanes, `authelia-smtp-eso` and `telemetry-smtp-eso`, both
|
||
read `platform/data/workloads/railiance-telemetry/smtp`, which is the IONOS
|
||
mailbox `platform@coulomb.social` (CCR-2026-0035). That is a different
|
||
supplier and belongs to production senders, so it is not reused.
|
||
- Repo sources (checked earlier the same day): no catalog entry or CCR for
|
||
`mas` or TEM. `openbao-email-connect` is a separate production sender.
|
||
- Not detected by design: a policy with an unrelated name covering the path.
|
||
|
||
## 3. Proposed changes
|
||
|
||
SEC-DSM §37:
|
||
|
||
- **Q1** Can the operation use identity directly? No. TEM SMTP
|
||
authenticates only with username/password.
|
||
- **Q2** Can the target accept federated identity? No. TEM SMTP has no
|
||
federation.
|
||
- **Q3** Can the target issue an ephemeral credential? Not viably. Scaleway
|
||
API keys can carry an expiry, but adding an issuer for a temporary test
|
||
lane would be issuance for show (`DSM-TRANS-003`). Instead the founder sets
|
||
the key's own Scaleway expiry to the end of MAS-WP-0003.
|
||
- **Q4** Can an existing static credential be rotated automatically? No
|
||
automated rotation for a lane this short-lived.
|
||
- **Q5** Must a persistent static credential exist? Yes, for the life of
|
||
MAS-WP-0003 only. This is §5 level "vaulted static secret".
|
||
|
||
**Credential facts:**
|
||
|
||
| Field | Value |
|
||
|---|---|
|
||
| Owner | founder (Scaleway IAM) |
|
||
| Consumer | `mas/mas-mx` |
|
||
| Authority | Scaleway IAM API key on a dedicated application with only the Transactional Email permission set, on the TEM project |
|
||
| Delivery | ESO → Secret `mas/mas-relay` |
|
||
| Copies | OpenBao KV, plus one Kubernetes Secret (a delivery copy, not the authority) |
|
||
| Rotation | Replace the KV version; ESO refreshes |
|
||
| Revocation | Delete the API key in Scaleway, then tear down (rows 6–7 below) |
|
||
| Detection | TEM sending logs and the OpenBao audit log for reads of the path |
|
||
| What would eliminate it | Removing the MX, or an MTA that relays through `email-connect`'s API instead of SMTP |
|
||
|
||
| # | Action | Object | Reuse-vs-new rationale |
|
||
|---|---|---|---|
|
||
| 1 | create | KV v2 `platform/suppliers/scaleway/mas-tem-relay`, fields `username`, `password` (placeholder `xxx`) | No existing home (§2). Supplier account path `platform/suppliers/<supplier>/<account>` (founder decision 2026-10-08, ops-warden `platform-supplier-credentials`); `credential-change.py` accepts it |
|
||
| 2 | create | policy `workload-kv-read-mas-tem-relay`: `read` on `platform/data/suppliers/scaleway/mas-tem-relay`, plus token lookup-self and revoke-self (CCR-2026-0051 policy file, used verbatim) | Single-path ESO reader; no metadata read needed |
|
||
| 3 | create | Kubernetes auth role `external-secrets-mas-tem-relay`: SA `mas-tem-relay-eso`, ns `mas`, audience `openbao`, ttl/max 15m, `token_no_default_policy` | Current pattern (CCR-2026-0050), with no static ESO token to deliver or expire |
|
||
| 4 | create (consumer repo) | ServiceAccount `mas-tem-relay-eso`, **namespaced** SecretStore `openbao-mas-tem-relay`, ExternalSecret `mas-relay` (keys `username`, `password`), all in ns `mas` | Namespaced instead of a ClusterSecretStore, so the store is deleted with the namespace |
|
||
| 5 | reuse (railiance-platform) | **CCR-2026-0051** (`railiance-platform@4398f89`), with policy file `openbao/policies/workload-kv-read-mas-tem-relay.hcl` | Platform's record of the lane. ops-mason applies rows 2–3 under `ops-mason-construct` after approval and records evidence against the CCR |
|
||
| 6 | tear-down (at MAS-WP-0003 close) | auth role, policy, KV path (`metadata delete`) | Cluster-scoped OpenBao objects do not go away with the namespace |
|
||
| 7 | tear-down (founder) | Scaleway API key | Revokes the authority itself |
|
||
|
||
No catalog entry: the lane is temporary and has a single consumer.
|
||
|
||
## 4. Review notes
|
||
|
||
- **Naming:** `mas-tem-relay` is used for the supplier account segment,
|
||
policy, role, SA and store, so one grep finds every object to retire. Secret `mas-relay` keeps
|
||
the name the consumer asked for.
|
||
- **TTL/scoping:** 15m role TTL matches CCR-2026-0050. The policy covers one
|
||
data path and no metadata. Binding is to exact SA and namespace claims.
|
||
- **Redundancy:** does not reuse the `email-connect` sender key (§2).
|
||
- **Deviation from the request:** it asked for a ClusterSecretStore
|
||
`openbao-mas` and an ESO token. Both are replaced, by Kubernetes auth
|
||
(DSM-K8S-001, projected identity before a broker token) and by a
|
||
namespaced store (easier to retire). The consumer applies rows 4 itself.
|
||
- **Secret values:** ops-mason neither reads nor writes them. The policy
|
||
grants `read` only to the ESO role.
|
||
- **Naming vs existing SMTP lanes:** `authelia-smtp-eso` and
|
||
`telemetry-smtp-eso` use one name for role, policy and SA. This lane keeps
|
||
the `external-secrets-*` / `workload-kv-read-*` shape, which is what
|
||
`ops-mason-construct` may build (MASON-IN-0005).
|
||
- **Build path:** after approval, ops-mason applies rows 2–3 itself under
|
||
`bao-session.sh build`. Row 1 is done: the placeholder came first and the
|
||
founder wrote the value on 2026-10-18.
|
||
- **CCR:** CCR-2026-0051 (approvers: founder, railiance-platform-owner,
|
||
mail-address-semantics-owner). The post-build check compares the written
|
||
policy with its policy file.
|
||
- **Open:** founder approval; `bound_claims_confirmed` after the SA exists.
|
||
|
||
## 5. Executive summary
|
||
|
||
**One-line ask:** let the temporary MAS test mail server read its TEM SMTP
|
||
login from OpenBao, and nothing else, until MAS-WP-0003 ends.
|
||
|
||
**Who gets access:** the External Secrets reader in namespace `mas`, and
|
||
only when it signs in as ServiceAccount `mas-tem-relay-eso`. No person, no
|
||
agent, no other namespace.
|
||
|
||
**To what:** read on `platform/suppliers/scaleway/mas-tem-relay`
|
||
(`username`, `password`), delivered as Secret `mas/mas-relay` to the `mas-mx`
|
||
container at `/etc/mas-relay`.
|
||
|
||
**For how long:** 15-minute OpenBao sessions, renewed by ESO, until teardown
|
||
at MAS-WP-0003 close. The Scaleway key's own expiry is the hard stop.
|
||
|
||
**Blast radius if the credential leaks:** someone can send mail as your TEM
|
||
project, which risks reputation, quota and cost, until you revoke the key in
|
||
the Scaleway console. If the key is TEM-only it gives no other Scaleway
|
||
access.
|
||
|
||
**Cost to reverse:** low. Delete the role, the policy and the KV path, then
|
||
revoke the key. Nothing else depends on it.
|
||
|
||
**Decision:** pending.
|