fix(admission): restrict approval audit sender to the platform tenant
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-5.6-luna
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
This commit is contained in:
tegwick 2026-09-08 17:01:41 +02:00
parent 633f68b81b
commit bbe8c15494
5 changed files with 46 additions and 22 deletions

View file

@ -16,7 +16,7 @@
"approval-engine" "approval-engine"
], ],
"tenants": [ "tenants": [
"*" "tenant:platform"
], ],
"may_write": true, "may_write": true,
"may_read": false, "may_read": false,

View file

@ -31,7 +31,7 @@ data:
{ {
"name": "approval-engine", "name": "approval-engine",
"sources": ["approval-engine"], "sources": ["approval-engine"],
"tenants": ["*"], "tenants": ["tenant:platform"],
"may_write": true, "may_write": true,
"may_read": false, "may_read": false,
"evidence_kind": "load-bearing" "evidence_kind": "load-bearing"

View file

@ -20,7 +20,7 @@ token appears here, in Git, in State Hub, or in a workplan.
| `may_write` | true | | `may_write` | true |
| `may_read` | false — a source does not gain a read surface by emitting | | `may_read` | false — a source does not gain a read surface by emitting |
| `secret_policy` | `redact` (estate default; see below) | | `secret_policy` | `redact` (estate default; see below) |
| `tenants` | `["*"]` — **proposed, needs approval-engine's confirmation** | | `tenants` | `["tenant:platform"]` — exact platform store tenant |
| Retention | no expiry set; recoverable history is the platform `data.backup` window, 30 days, `measured` | | Retention | no expiry set; recoverable history is the platform `data.backup` window, 30 days, `measured` |
| Custody class | `operational` — never `archive`, never WORM | | Custody class | `operational` — never `archive`, never WORM |
@ -39,25 +39,21 @@ Audit Core stores all four and derives nothing from them. It renders no verdict
on whether an approval is still valid (§9.4), asserted by test in on whether an approval is still valid (§9.4), asserted by test in
`tests/test_approval_validity_prohibition.py`. `tests/test_approval_validity_prohibition.py`.
### The `tenants` value is the one input still owed ### Source return — 2026-09-08
`["*"]` is proposed because approval issuance is not obviously tenant- `approval-engine/docs/audit-source-registration.md` now records the exact
partitioned, and the alternative — an explicit tenant list — cannot be invented `["tenant:platform"]` scope and explicit `secret_policy: redact`. The tenant
here. `audit_core/senders.py` states that omitting a tenant restriction "should choice follows the already accepted platform tenant decision and the Engine,
be justified per sender, not adopted by default", so this needs production CLI and deployment defaults; no cross-tenant scope is needed.
`approval-engine` to either confirm the wildcard with its reasoning or supply Both scope manifests now use that exact value. Tests prove that the overlay
the list. Tenant identifiers are opaque to Audit Core; their shape is owned by removes a stale wildcard tenant/source/read grant and refuses near-miss tenant
the IAM Profile and `tenant-engine`. spellings while retaining the accepted platform tenant.
### `secret_policy` `redact` retains a legitimate event with a secret-shaped field removed rather
than turning it into a delivery failure. This does not permit credentials in
`redact` accepts the event and redacts a secret-shaped field, so a legitimate producer payloads. The scope overlay deliberately does not override
event is not lost over one field. For a load-bearing source that trade is worth `secret_policy`: the protected sender registry entry must set it explicitly
re-examining: `reject` refuses the event instead, which for approvals converts a when platform provisioning occurs, and live admission must verify that value.
redactable field into a delivery failure the source must retry. `redact` is the
recommendation — losing a revocation record is worse than storing it with one
field redacted — but it is `approval-engine`'s call to make explicitly rather
than inherit.
## Applied in this repository ## Applied in this repository
@ -80,8 +76,7 @@ older rule's breadth. `user-engine`'s sender and its policy are unchanged.
| Input | Owner | Note | | Input | Owner | Note |
| --- | --- | --- | | --- | --- | --- |
| Token at `approval-engine/approval-engine-audit`, key `audit-token` | OpenBao (`railiance-platform`), routed via `warden route` | Audit Core never holds or transports it | | Token at `approval-engine/approval-engine-audit`, key `audit-token` | OpenBao (`railiance-platform`), routed via `warden route` | Audit Core never holds or transports it |
| `tenants` confirmation | `approval-engine` | see above | | Receiver registry custody and explicit `secret_policy: redact` | `railiance-platform` | Link protected registry and sender credential in the first-provision record; no token generated here |
| `secret_policy` confirmation | `approval-engine` | see above |
| Applying the manifests to `railiance01` | operator | Audit Core does not apply cluster changes unprompted | | Applying the manifests to `railiance01` | operator | Audit Core does not apply cluster changes unprompted |
## What Audit Core will and will not claim about this stream ## What Audit Core will and will not claim about this stream

View file

@ -314,6 +314,26 @@ def test_a_scope_entry_without_a_token_admits_nothing():
assert registry.authenticate("Bearer live").name == "user-engine" assert registry.authenticate("Bearer live").name == "user-engine"
@pytest.mark.parametrize("tenant", ["platform", "tenant:coulomb", "tenant:Platform", "tenant:platform ", "tenant:customer"])
def test_approval_scope_restricts_a_stale_wildcard_registration(tenant):
registry = SenderRegistry.from_env({
"AUDIT_CORE_SENDERS": json.dumps([{
"name": "approval-engine", "tokens": ["fixture-only"],
"sources": ["*"], "tenants": ["*"],
"may_read": True, "secret_policy": "redact",
}]),
"AUDIT_CORE_SENDERS_SCOPE_PATH": str(SCOPE_FILE),
})
identity = registry.authenticate("Bearer fixture-only")
assert identity.permits_tenant("tenant:platform")
assert not identity.permits_tenant(tenant)
assert identity.permits_source("approval-engine")
assert not identity.permits_source("user-engine")
assert identity.may_write and not identity.may_read
assert identity.secret_policy == "redact"
assert identity.is_load_bearing
def test_user_engine_evidence_kind_is_not_asserted_on_its_behalf(): def test_user_engine_evidence_kind_is_not_asserted_on_its_behalf():
"""Undeclared means attributive by default, not a claim audit-core made.""" """Undeclared means attributive by default, not a claim audit-core made."""
scope = json.loads(SCOPE_FILE.read_text()) scope = json.loads(SCOPE_FILE.read_text())

View file

@ -264,6 +264,15 @@ approvals is omission, which tamper evidence does not address at all. T04 does.
A consumer waiting on T02 before trusting approval evidence would be waiting on A consumer waiting on T02 before trusting approval evidence would be waiting on
the wrong thing. the wrong thing.
**2026-09-08 source return:** approval-engine now records the exact existing
store scope `["tenant:platform"]` and explicitly selects `redact` in
`docs/audit-source-registration.md`. This supersedes the two input waits above.
The JSON and ConfigMap overlays are narrowed together. The overlay preserves
secret_policy from the protected registry, so provisioning must set it there.
T09 remains progress for the platform custody record, credential, admitted
manifest application and positive/negative live ingestion evidence. No secret
was created and no production manifest was applied by this source change.
```task ```task
id: AUDIT-WP-0009-T10 id: AUDIT-WP-0009-T10
status: todo status: todo