CCR-2026-0007: binky IMAP on tenants/ mount + CCR allowlist
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:
parent
347226eb36
commit
86209fa90c
6 changed files with 218 additions and 27 deletions
|
|
@ -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
|
||||||
|
|
@ -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
|
`read:package`, `write:package`, `read:repository`, `write:repository`, plus admin
|
||||||
scopes required by `forgejo-operator-bootstrap`. Downstream repos update `load_token()`
|
scopes required by `forgejo-operator-bootstrap`. Downstream repos update `load_token()`
|
||||||
to read OpenBao when env is unset after lane verification.
|
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`.
|
||||||
|
|
|
||||||
|
|
@ -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
|
# Coding agents MUST NOT hold raw data-read on high-risk recovery/upload/admin
|
||||||
# raw data-read on high-risk recovery/upload/admin secrets. This policy is the
|
# or tenant mailbox secrets. Metadata + capabilities only.
|
||||||
# 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.
|
|
||||||
|
|
||||||
# --- railiance-backup-offsite-lane ---
|
# --- platform high-risk ---
|
||||||
path "platform/data/workloads/railiance/backup/offsite-lane" {
|
path "platform/data/workloads/railiance/backup/offsite-lane" {
|
||||||
capabilities = ["deny"]
|
capabilities = ["deny"]
|
||||||
}
|
}
|
||||||
path "platform/metadata/workloads/railiance/backup/offsite-lane" {
|
path "platform/metadata/workloads/railiance/backup/offsite-lane" {
|
||||||
capabilities = ["read"]
|
capabilities = ["read"]
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- forgejo-admin ---
|
|
||||||
path "platform/data/workloads/forgejo/forgejo-admin" {
|
path "platform/data/workloads/forgejo/forgejo-admin" {
|
||||||
capabilities = ["deny"]
|
capabilities = ["deny"]
|
||||||
}
|
}
|
||||||
path "platform/metadata/workloads/forgejo/forgejo-admin" {
|
path "platform/metadata/workloads/forgejo/forgejo-admin" {
|
||||||
capabilities = ["read"]
|
capabilities = ["read"]
|
||||||
}
|
}
|
||||||
|
|
||||||
# --- openrouter-llm-connect ---
|
|
||||||
path "platform/data/workloads/activity-core/llm-connect/llm-connect-provider-secrets" {
|
path "platform/data/workloads/activity-core/llm-connect/llm-connect-provider-secrets" {
|
||||||
capabilities = ["deny"]
|
capabilities = ["deny"]
|
||||||
}
|
}
|
||||||
|
|
@ -38,7 +23,14 @@ path "platform/metadata/workloads/activity-core/llm-connect/llm-connect-provider
|
||||||
capabilities = ["read"]
|
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" {
|
path "sys/capabilities-self" {
|
||||||
capabilities = ["update"]
|
capabilities = ["update"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
|
}
|
||||||
|
|
@ -591,10 +591,22 @@ def applier_policy_violations(ccr: dict[str, Any]) -> list[str]:
|
||||||
policy_file = str(openbao.get("policy_file") or "")
|
policy_file = str(openbao.get("policy_file") or "")
|
||||||
fields = openbao.get("fields") or []
|
fields = openbao.get("fields") or []
|
||||||
|
|
||||||
if mount != "platform":
|
# WARDEN-WP-0028: platform fleet lanes stay under platform/workloads/;
|
||||||
violations.append(f"openbao.mount must be platform, got {mount}")
|
# client/tenant commercial secrets live on mount tenants/ (exact-path policies).
|
||||||
if not kv_path.startswith("platform/workloads/"):
|
allowed_mount_prefixes = {
|
||||||
violations.append("openbao.kv_path must stay under platform/workloads/")
|
"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 ("*", "..", "//")):
|
if any(fragment in kv_path for fragment in ("*", "..", "//")):
|
||||||
violations.append("openbao.kv_path must not contain wildcard, parent, or empty segments")
|
violations.append("openbao.kv_path must not contain wildcard, parent, or empty segments")
|
||||||
if "/data/" in kv_path or "/metadata/" in kv_path:
|
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:
|
else:
|
||||||
context = {
|
context = {
|
||||||
"id": "<CCR-ID>",
|
"id": "<CCR-ID>",
|
||||||
"kv_path": "<platform/workloads/...>",
|
"kv_path": "<platform/workloads/...> or <tenants/<tenant>/...>",
|
||||||
"policy_name": "<workload-kv-read-...>",
|
"policy_name": "<workload-kv-read-...>",
|
||||||
"auth_role_path": "auth/<mount>/role/<role>",
|
"auth_role_path": "auth/<mount>/role/<role>",
|
||||||
"decision_link": "<link State Hub decision>",
|
"decision_link": "<link State Hub decision>",
|
||||||
|
|
|
||||||
|
|
@ -358,8 +358,30 @@ class CredentialChangeTests(unittest.TestCase):
|
||||||
ccr["openbao"]["mount"] = "secret"
|
ccr["openbao"]["mount"] = "secret"
|
||||||
ccr["openbao"]["kv_path"] = "secret/platform-admin"
|
ccr["openbao"]["kv_path"] = "secret/platform-admin"
|
||||||
blockers = credential_change.applier_readiness_blockers(ccr)
|
blockers = credential_change.applier_readiness_blockers(ccr)
|
||||||
self.assertIn("openbao.mount must be platform, got secret", blockers)
|
self.assertTrue(
|
||||||
self.assertIn("openbao.kv_path must stay under platform/workloads/", blockers)
|
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:
|
def test_nonprod_applier_policy_remains_metadata_only(self) -> None:
|
||||||
policy = (
|
policy = (
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue