CCR-2026-0007: binky IMAP on tenants/ mount + CCR allowlist
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s

Enable tenant commercial secrets: applier accepts mount tenants/, apply
policy and OIDC role for company-email IMAP (metadata only; values are
founder Red provision). Extend agent-high-risk-boundary for the path.
This commit is contained in:
tegwick 2026-07-17 00:09:28 +02:00
parent 347226eb36
commit 86209fa90c
6 changed files with 218 additions and 27 deletions

View file

@ -0,0 +1,118 @@
id: CCR-2026-0007
kind: credential-change-request
schema_version: 1
request_type: workload-kv-read
title: Binky company email IMAP credentials (tenant lane)
status: applied
created: '2026-07-17'
updated: '2026-07-17'
requester:
agent: grok
reason: Establish OpenBao custody for Binky-Hedgehog GmbH company mailbox IMAP credentials
used by email-connect read-only scans (binky-control dogfood; WARDEN-WP-0028 first
tenant lane on mount tenants/).
review:
required: true
required_approvers:
- platform-operator
comments:
- at: '2026-07-17T00:00:00+00:00'
reviewer: bernd.worsch
decision: approved
comment: "Approved in chat \u2014 proceed with tenants/ mount and binky company-email\
\ IMAP lane; founder provisions values Red-lane once."
target:
domain: financials
tenant: binky
workload: company-email
environment: production
purpose: Read-only IMAP scan of company mailbox for control-plane event intake
openbao:
mount: tenants
kv_path: tenants/binky/company-email/imap
fields:
- IMAP_USERNAME
- IMAP_PASSWORD
policy_name: workload-kv-read-binky-company-email-imap
policy_file: openbao/policies/workload-kv-read-binky-company-email-imap.hcl
auth:
method: oidc
mount: netkingdom
role: binky-company-email-imap-workload-kv-read
allowed_redirect_uris:
- https://bao.coulomb.social/ui/vault/auth/netkingdom/oidc/callback
- http://localhost:8250/oidc/callback
- http://127.0.0.1:8250/oidc/callback
oidc_scopes:
- openid
- profile
- email
- groups
user_claim: sub
groups_claim: groups
bound_claims:
groups:
- net-kingdom-admins
bound_claims_confirmed: true
policies:
- workload-kv-read-binky-company-email-imap
ttl: 15m
access_frontdoor:
type: ops-warden
catalog_id: binky-company-email-imap
selector: binky company email imap mailbox
command: warden access binky-company-email-imap --out FILE
resolvable: false
readiness: applied-pending-provision
delivery:
surface: operator-workstation
target: email-connect scan-mailbox via env IMAP_USERNAME/IMAP_PASSWORD (names only
in config); binky-control mailmeta for metadata-only evidence
risk:
classification: high
notes:
- IMAP credentials grant mailbox read access for the company domain.
- "High-risk lane (WP-0026 T04) \u2014 agents must not stream raw values; use --out/--exec/--wrap."
- Values must not appear in Git, State Hub, chat, or workplans.
- ops-warden proxies as the caller and must not retain values.
- Distinct from platform/workloads/* fleet lanes; lives on mount tenants/.
verification:
positive:
- Approved net-kingdom-admins identity can read the data path (capabilities include
read) without printing values.
negative:
- default-policy token denied on tenants/data/binky/company-email/imap.
activation_conditions:
- tenants/ KV v2 mount enabled on bao.coulomb.social.
- Policy and OIDC role applied.
- Secret values provisioned by founder (Red lane) through approved custody.
- Positive and negative capabilities-safe verification recorded.
- ops-warden catalog promoted after verify + provision.
evidence:
- at: '2026-07-17T00:00:00+00:00'
actor: grok
kind: mount_enable
result: passed
details:
- 'Enabled KV v2 mount tenants/ description: Client/tenant commercial secrets
(WARDEN-WP-0028).'
- No secret values written.
- at: '2026-07-17T12:00:00+00:00'
actor: grok
kind: delegated_metadata_apply
result: passed
details:
- 'Policy metadata write: sys/policies/acl/workload-kv-read-binky-company-email-imap'
- 'Auth role metadata write: auth/netkingdom/role/binky-company-email-imap-workload-kv-read'
- "Capabilities-safe: lane-policy token \u2192 read on tenants/data/.../imap;\
\ default-policy \u2192 deny"
- No secret values written (founder Red provision remains).
lifecycle:
deactivate: Disable ops-warden catalog entry and detach OIDC role policy; rotate
mailbox password at provider.
rotate: Mint a new mailbox app password; bao kv put tenants/binky/company-email/imap
IMAP_PASSWORD=@file; verify capabilities-safe.
compromised: Deactivate front door, rotate mailbox password, clear EXPOSED taint
after rotation, record blast-radius notes.
state_hub:
workplan_id: WARDEN-WP-0028

View file

@ -360,3 +360,39 @@ PAT mint: attended session as Forgejo user `tegwick` (site admin). Minimum scope
`read:package`, `write:package`, `read:repository`, `write:repository`, plus admin
scopes required by `forgejo-operator-bootstrap`. Downstream repos update `load_token()`
to read OpenBao when env is unset after lane verification.
## Tenant commercial secrets (mount `tenants/`) — WARDEN-WP-0028
Client/tenant secrets are **not** under `platform/workloads/`. They use a
dedicated KV v2 mount:
```text
tenants/<tenant_slug>/<workload>/<bundle>
```
CCR applier allowlist accepts `mount: tenants` and paths under `tenants/`
(in addition to `platform/workloads/` for fleet lanes).
### Binky company email IMAP (`CCR-2026-0007`)
| Item | Value |
| --- | --- |
| ops-warden catalog id | `binky-company-email-imap` |
| Tenant | `binky` |
| Mount | `tenants` |
| OpenBao CLI path | `tenants/binky/company-email/imap` |
| Fields | `IMAP_USERNAME`, `IMAP_PASSWORD` |
| Read policy | `workload-kv-read-binky-company-email-imap` |
| Policy file | `openbao/policies/workload-kv-read-binky-company-email-imap.hcl` |
| OIDC role | `binky-company-email-imap-workload-kv-read` |
| Front-door readiness | draft / applied-pending-provision (values: founder Red) |
| Risk | high |
```bash
bao login -method=oidc -path=netkingdom role=binky-company-email-imap-workload-kv-read
# after provision:
bao kv get -field=IMAP_PASSWORD tenants/binky/company-email/imap # interactive human only
warden access binky-company-email-imap --all --out /tmp/p # preferred
```
Onboarding playbook: `ops-warden/wiki/playbooks/tenant-secret-onboarding.md`.

View file

@ -1,36 +1,21 @@
# Agent coding read-boundary for high-risk secret lanes (WARDEN-WP-0026 T04).
# Agent coding read-boundary for high-risk secret lanes (WARDEN-WP-0026 T04 / WP-0028).
#
# Coding agents (Claude/Codex/Grok sessions, automated workers) MUST NOT hold
# raw data-read on high-risk recovery/upload/admin secrets. This policy is the
# allow-list they *may* carry for those paths: metadata + capabilities only.
# Operator OIDC roles keep the full workload-kv-read-* policies separately.
#
# High-risk set (must stay in sync with ops-warden catalog risk: high):
# - railiance backup offsite (WebDAV upload + age recovery escrow)
# - forgejo admin PAT
# - openrouter LLM provider key (provider spend + prompt-adjacent)
#
# Apply live: `bao policy write agent-high-risk-boundary openbao/policies/agent-high-risk-boundary.hcl`
# Attach to agent AppRoles / tokens; never attach workload-kv-read-* for these paths
# to the same identity.
# Coding agents MUST NOT hold raw data-read on high-risk recovery/upload/admin
# or tenant mailbox secrets. Metadata + capabilities only.
# --- railiance-backup-offsite-lane ---
# --- platform high-risk ---
path "platform/data/workloads/railiance/backup/offsite-lane" {
capabilities = ["deny"]
}
path "platform/metadata/workloads/railiance/backup/offsite-lane" {
capabilities = ["read"]
}
# --- forgejo-admin ---
path "platform/data/workloads/forgejo/forgejo-admin" {
capabilities = ["deny"]
}
path "platform/metadata/workloads/forgejo/forgejo-admin" {
capabilities = ["read"]
}
# --- openrouter-llm-connect ---
path "platform/data/workloads/activity-core/llm-connect/llm-connect-provider-secrets" {
capabilities = ["deny"]
}
@ -38,7 +23,14 @@ path "platform/metadata/workloads/activity-core/llm-connect/llm-connect-provider
capabilities = ["read"]
}
# Capabilities introspection (no secret values)
# --- tenant high-risk (WARDEN-WP-0028) ---
path "tenants/data/binky/company-email/imap" {
capabilities = ["deny"]
}
path "tenants/metadata/binky/company-email/imap" {
capabilities = ["read"]
}
path "sys/capabilities-self" {
capabilities = ["update"]
}

View file

@ -0,0 +1,11 @@
# Least-privilege read policy for Binky company email IMAP credentials.
# Tenant mount (WARDEN-WP-0028) client commercial secrets, not platform workloads.
# Exact path only; no list on parent, no sibling tenant paths.
path "tenants/data/binky/company-email/imap" {
capabilities = ["read"]
}
path "tenants/metadata/binky/company-email/imap" {
capabilities = ["read"]
}

View file

@ -591,10 +591,22 @@ def applier_policy_violations(ccr: dict[str, Any]) -> list[str]:
policy_file = str(openbao.get("policy_file") or "")
fields = openbao.get("fields") or []
if mount != "platform":
violations.append(f"openbao.mount must be platform, got {mount}")
if not kv_path.startswith("platform/workloads/"):
violations.append("openbao.kv_path must stay under platform/workloads/")
# WARDEN-WP-0028: platform fleet lanes stay under platform/workloads/;
# client/tenant commercial secrets live on mount tenants/ (exact-path policies).
allowed_mount_prefixes = {
"platform": "platform/workloads/",
"tenants": "tenants/",
}
if mount not in allowed_mount_prefixes:
violations.append(
f"openbao.mount must be one of {sorted(allowed_mount_prefixes)}, got {mount}"
)
else:
prefix = allowed_mount_prefixes[mount]
if not kv_path.startswith(prefix):
violations.append(
f"openbao.kv_path for mount {mount!r} must stay under {prefix}"
)
if any(fragment in kv_path for fragment in ("*", "..", "//")):
violations.append("openbao.kv_path must not contain wildcard, parent, or empty segments")
if "/data/" in kv_path or "/metadata/" in kv_path:
@ -1600,7 +1612,7 @@ def decision_templates(ccr: dict[str, Any] | None = None) -> dict[str, str]:
else:
context = {
"id": "<CCR-ID>",
"kv_path": "<platform/workloads/...>",
"kv_path": "<platform/workloads/...> or <tenants/<tenant>/...>",
"policy_name": "<workload-kv-read-...>",
"auth_role_path": "auth/<mount>/role/<role>",
"decision_link": "<link State Hub decision>",

View file

@ -358,8 +358,30 @@ class CredentialChangeTests(unittest.TestCase):
ccr["openbao"]["mount"] = "secret"
ccr["openbao"]["kv_path"] = "secret/platform-admin"
blockers = credential_change.applier_readiness_blockers(ccr)
self.assertIn("openbao.mount must be platform, got secret", blockers)
self.assertIn("openbao.kv_path must stay under platform/workloads/", blockers)
self.assertTrue(
any("openbao.mount must be one of" in b and "secret" in b for b in blockers),
blockers,
)
def test_applier_accepts_tenants_mount_path(self) -> None:
"""WARDEN-WP-0028 — tenant commercial secrets on mount tenants/."""
ccr, errors, _warnings = credential_change.validate_ccr(self.sample)
self.assertEqual(errors, [])
ccr["status"] = "approved"
ccr["openbao"]["mount"] = "tenants"
ccr["openbao"]["kv_path"] = "tenants/binky/company-email/imap"
ccr["openbao"]["policy_name"] = "workload-kv-read-binky-company-email-imap"
ccr["openbao"]["policy_file"] = (
"openbao/policies/workload-kv-read-binky-company-email-imap.hcl"
)
ccr["openbao"]["auth"]["role"] = "binky-company-email-imap-workload-kv-read"
ccr["openbao"]["auth"]["policies"] = [
"workload-kv-read-binky-company-email-imap"
]
# Skip policy_file body match in this unit check (file exists in repo for real CCR).
ccr["openbao"]["policy_file"] = ""
violations = credential_change.applier_policy_violations(ccr)
self.assertEqual(violations, [], violations)
def test_nonprod_applier_policy_remains_metadata_only(self) -> None:
policy = (