152 lines
7.3 KiB
Markdown
152 lines
7.3 KiB
Markdown
|
|
# tenant-engine source registration
|
||
|
|
|
||
|
|
`AUDIT-WP-0010` · intake `AUDIT-IN-0002` · statute §9.6
|
||
|
|
|
||
|
|
Registration inputs for `tenant-engine` as an attributive audit source.
|
||
|
|
Owner-side record, entirely non-secret. No sender token appears here, in Git,
|
||
|
|
in State Hub, or in a workplan.
|
||
|
|
|
||
|
|
## Declaration
|
||
|
|
|
||
|
|
| Field | Value |
|
||
|
|
| --- | --- |
|
||
|
|
| Sender name | `tenant-engine` |
|
||
|
|
| Permitted `source` | `tenant-engine` (exact; no wildcard) |
|
||
|
|
| `evidence_kind` | **`attributive`** — declared by the emitter, and the declaration is the emitter's to make |
|
||
|
|
| `completeness_trade` | **declared**: the outbox drains after commit rather than inside the mutation transaction (`tenant-engine/docs/evidence-emission.md`) |
|
||
|
|
| `may_write` | true |
|
||
|
|
| `may_read` | false |
|
||
|
|
| `secret_policy` | `redact` |
|
||
|
|
| `tenants` | `["*"]` — justified below, not inherited |
|
||
|
|
| Retention | no expiry set; recoverable history is the platform `data.backup` window, 30 days, `measured` |
|
||
|
|
| Custody class | `operational` |
|
||
|
|
|
||
|
|
### Why the tenant scope is a wildcard, and why that is not a shrug
|
||
|
|
|
||
|
|
`senders.py` requires a missing tenant restriction to be justified per sender,
|
||
|
|
so: `tenant-engine` is the tenant lifecycle engine. Its events carry the
|
||
|
|
*affected* `tenant_id`, and the set of affected tenants is every tenant that
|
||
|
|
exists, including ones created after this registration was written. An explicit
|
||
|
|
list would have to be edited on every tenant creation, and would fail closed at
|
||
|
|
exactly the moment a new tenant is provisioned — dead-lettering the creation
|
||
|
|
evidence of the tenant whose creation it is. That is a worse failure than the
|
||
|
|
breadth it buys.
|
||
|
|
|
||
|
|
This is the same justification `user-engine` carries and a different one from
|
||
|
|
`approval-engine` and `informed-decision`, which are single-tenant by
|
||
|
|
construction and are therefore scoped exactly.
|
||
|
|
|
||
|
|
The exposure the wildcard leaves is real and worth naming: a compromised
|
||
|
|
`tenant-engine` credential can attribute an event to any tenant. `source` is
|
||
|
|
still pinned exactly, so it cannot impersonate another engine.
|
||
|
|
|
||
|
|
### The attributive trade, recorded here and not only in the emitter
|
||
|
|
|
||
|
|
§9.6 permits attributive evidence to trade away emission atomicity **provided
|
||
|
|
the trade is declared and completeness is never claimed**. `tenant-engine`
|
||
|
|
declared it properly and in advance. Recorded on this side per the statute's
|
||
|
|
requirement that the trade travel with the trail:
|
||
|
|
|
||
|
|
| Step | Atomic with the mutation? |
|
||
|
|
| --- | --- |
|
||
|
|
| local `events` row | yes |
|
||
|
|
| local `audit_outbox` row | yes |
|
||
|
|
| drain to audit-core | **no** — after commit |
|
||
|
|
|
||
|
|
So a mutation can exist here without its event, and audit-core will never know.
|
||
|
|
Nothing in this archive detects that, and no reading of the chain changes it.
|
||
|
|
`completeness_claimed` is `false` for this source, as it is for every source.
|
||
|
|
|
||
|
|
If a control ever starts branching on these events, the class must be raised to
|
||
|
|
load-bearing **before** that ships — and the scope overlay is deliberately
|
||
|
|
built so it can raise the kind but never lower it.
|
||
|
|
|
||
|
|
## Blocking finding: the envelope does not match — events would be rejected
|
||
|
|
|
||
|
|
**`tenant-engine` cannot deliver a single event today, and admitting the sender
|
||
|
|
would not change that.** This is the correction `AUDIT-IN-0002` invited rather
|
||
|
|
than a refusal.
|
||
|
|
|
||
|
|
`tenant_engine.audit_core.envelope_for` emits a shape the receiver does not
|
||
|
|
accept. `normalize()` requires eight fields; the envelope supplies three of them
|
||
|
|
under the required names and omits one entirely.
|
||
|
|
|
||
|
|
| Receiver requires | `tenant-engine` sends | Result |
|
||
|
|
| --- | --- | --- |
|
||
|
|
| `id` | `event_id` | missing |
|
||
|
|
| `type` | `action` | missing |
|
||
|
|
| `subject` | `resource` | missing |
|
||
|
|
| `occurred_at` | `observed_at` | missing |
|
||
|
|
| `data` | `details` | missing |
|
||
|
|
| `correlation_id` | — | **absent entirely** |
|
||
|
|
| `source` | `source` | ok |
|
||
|
|
| `tenant` | `tenant` | ok |
|
||
|
|
|
||
|
|
Verified by running the real envelope through the real `normalize()`, not by
|
||
|
|
reading: it raises `invalid_event`. Every event would return **400** and be
|
||
|
|
dead-lettered.
|
||
|
|
|
||
|
|
**Why this is worse than a normal integration bug.** `tenant-engine`'s drain
|
||
|
|
treats 400 as terminal (`400/409 dead-letter`, `docs/evidence-emission.md`).
|
||
|
|
So the outbox row is marked handled while audit-core holds only a dead letter —
|
||
|
|
which is not chained and is not custody. The event is lost on both sides, and
|
||
|
|
because the drain is non-blocking and attributive, nothing fails loudly. A
|
||
|
|
silent total loss of the stream, presenting as a working integration.
|
||
|
|
|
||
|
|
`schema_version: audit-core.event.v1alpha1` selects nothing at this receiver.
|
||
|
|
There is no version negotiation; the name gave a false impression of a contract.
|
||
|
|
|
||
|
|
**Whose fault this is: ours, mostly.** The accepted envelope was documented
|
||
|
|
nowhere a sender could read it. `docs/audit-backend-contract.md` describes the
|
||
|
|
stored record, and a sender reading it would reasonably infer the field names
|
||
|
|
that `tenant-engine` used. The contract is now published at
|
||
|
|
`docs/event-envelope.md`, and that gap is the actual root cause.
|
||
|
|
|
||
|
|
**What is needed:** `tenant-engine` maps `envelope_for` onto the eight required
|
||
|
|
fields and supplies a `correlation_id` (it has a natural one — the request or
|
||
|
|
operation that caused the mutation). audit-core is **not** relaxing
|
||
|
|
`normalize()` to accept the alternate names: a receiver that guesses which key
|
||
|
|
means what would be inventing the mapping, and a record whose field meanings the
|
||
|
|
archive chose is not the sender's assertion any more. `correlation_id` in
|
||
|
|
particular cannot be synthesized — a made-up one is worse than none.
|
||
|
|
|
||
|
|
A regression test (`tests/test_tenant_engine_envelope.py`) pins the mismatch, so
|
||
|
|
it becomes visible the moment the emitter is corrected rather than being
|
||
|
|
rediscovered in production.
|
||
|
|
|
||
|
|
## Applied in this repository
|
||
|
|
|
||
|
|
| Input | Where |
|
||
|
|
| --- | --- |
|
||
|
|
| Non-secret scope | `deploy/senders-scope.json` + `deploy/senders-scope.yaml` (lockstep) |
|
||
|
|
| Receiver ingress | `deploy/networkpolicies.yaml`, `audit-core-tenant-engine-ingress` |
|
||
|
|
| Declared trade | `SenderIdentity.completeness_trade`, carried through the overlay |
|
||
|
|
| Wire contract | `docs/event-envelope.md` |
|
||
|
|
|
||
|
|
Inert until the token exists: the overlay applies only to a sender the Secret
|
||
|
|
already carries.
|
||
|
|
|
||
|
|
The ingress rule ANDs namespace and pod label in one peer. Attributive rather
|
||
|
|
than load-bearing changes what may be claimed of the stream, not how narrow its
|
||
|
|
reachability should be — a weaker evidence class is not a reason for a wider
|
||
|
|
network rule. `user-engine`'s policy is unchanged.
|
||
|
|
|
||
|
|
## Still owed
|
||
|
|
|
||
|
|
| Input | Owner | Note |
|
||
|
|
| --- | --- | --- |
|
||
|
|
| Envelope correction | **`tenant-engine`** | Blocking. Nothing lands until this is fixed |
|
||
|
|
| Sender token, custody via `warden route` | OpenBao (`railiance-platform`) | audit-core never holds or transports it |
|
||
|
|
| Protected registry entry with explicit `secret_policy` | `railiance-platform` | The overlay does not override it |
|
||
|
|
| Applying the manifests | operator | audit-core does not apply cluster changes unprompted |
|
||
|
|
| Live positive-path evidence under `docs/evidence/` | audit-core, after the above | `AUDIT-WP-0010-T05` |
|
||
|
|
|
||
|
|
## What Audit Core will and will not claim about this stream
|
||
|
|
|
||
|
|
**Will:** every event it accepted is stored append-only, and the hash chain
|
||
|
|
detects alteration or truncation of what it received.
|
||
|
|
|
||
|
|
**Will not:** that the archive proves a mutation occurred, or that absence
|
||
|
|
proves it did not — and here that bound is sharper than usual, because the
|
||
|
|
declared trade means a committed mutation can legitimately have no event at all.
|