Commit graph

28 commits

Author SHA1 Message Date
cb23dc82fd Prepare compatible audit receiver with verified container lifecycle
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 1s
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
2026-09-11 06:46:15 +02:00
b098fb12ca AUDIT-WP-0009 T04/T06/T07 — heartbeats, reconciliation, and a home for findings
The detection half audit-core argued up to a MUST and then could not support.
Two registered sources were waiting on it.

T04, heartbeats. A heartbeat is an ordinary event — same envelope, same
append-only custody, same chain, no special table. Deliberate: a heartbeat
stored outside the chain would be the one record here that could be back-dated.
Declared per class rather than per source, because a per-source heartbeat from
a mixed-volume emitter is satisfied by its chattiest class and says nothing
about the quiet, security-relevant one, which is the only reason heartbeats
exist. Not the §17 cadence schema T05 waits on: cadence describes expected
rate, this says how often a source promises to say "nothing to report" for a
class that may legitimately be silent.

no_heartbeat_since_registration is its own finding kind rather than a skip —
it is the case most likely to be a broken integration and the one a naive
"compare against last seen" implementation silently drops. Grace widens the
window so one late run does not flap; it never removes a finding.

T06, reconciliation. Counts, never payloads. The awkward part is that every
registered sender holds may_read: false, which taken literally makes the §9.6
reconciliation obligation undischargeable by every source actually registered.
Resolved by observing that a source asking how many of its own events we hold
is not reading the archive — it learns nothing it did not itself emit. So the
surface is scoped to the caller's own sources and tenants and returns no
payloads; anything wider stays behind may_read and full tenant scope. Another
source's counts return 403 rather than an empty count, because a zero would
read as "we hold none of yours" — a false answer to a question about
completeness. No default window, since a count whose bounds the caller did not
choose is not comparable to anything the caller computed.

T07, the findings surface. /v1/stream-findings, following the dead-letter and
secret-finding conventions: may_read plus full tenant scope, since findings
span every sender and carry no tenant key to filter on.

The bound is on every response rather than in a document nobody opens beside
it. A missing heartbeat is not proof of suppression, and agreement on counts
proves neither completeness nor that any event occurred. Both controls cover
loss, outage, drain failure and accident; neither covers a source lying about
itself, and where the emitter is compromised both agree with it. Closing that
needs an observer independent of the emitter, which §16 put outside our scope.

The scope overlay may shorten a heartbeat interval or add a class, never
lengthen or remove one — same asymmetry as evidence_kind, and for the same
reason: a ConfigMap refresh must not widen the window in which a suppressed
class goes unnoticed without anyone deciding to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nb7Q6ZmXppNDkTWytfYqfv

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2069992@bnt-lap001
Assistant-Session: 167dd7f8-2a25-4be1-aa46-3b6f1a5f94c6
2026-09-10 16:43:26 +02:00
de9e3abe5f AUDIT-WP-0009 T02/T10 — schedule attestation, and make the §5 check total
T02. deploy/attest-cronjob.yaml: daily at 03:17 UTC against the 168h window,
its own ServiceAccount, and a Role reaching exactly one named ConfigMap —
get/update/patch, no create, no list. audit_core/attest_publish.py does the
publish in stdlib; the image carries no kubectl, and adding one to an audit
receiver's image to write a single file is the worse trade.

Three refusals, all deliberate:

  The producer is not the receiver. A receiver that could rewrite its own
  attestation could forge it. audit-core-egress is now scoped to
  component: receiver and a separate audit-core-attest-egress carries the 6443
  rule, so the receiver never gains API-server reach. Asserted by test.

  It refuses to publish over a broken chain. A fresh head written over a break
  replaces an honest chain_break with a fresh-looking attestation. Stale
  degrades the claim visibly; false does not.

  Mounted as a directory, not subPath. Found while writing the manifest: a
  subPath ConfigMap mount is resolved once at pod start and never updates, so
  the daily attestation would land in the ConfigMap and never reach the running
  receiver — tamper_evidence would age out to false while the job reported
  success every night, silent in both directions.

The offsite copy stays an operator step. audit-core holds no Nextcloud
credential and should not acquire one to publish a hash, so docs/integrity.md
states the bound plainly: until that copy exists the delivered control defends
against a database owner, not a cluster owner, and no stronger claim may be
made from it.

T10. layer.yaml lists four infrastructure contacts — platform-pg, state-hub,
kube-apiserver, the container registry — each with its role and whether another
layer reads it. tooling_contacts stays [], which is true under §5 as written;
the companion's totality request is met by the uncatalogued list rather than by
inventing a Tooling row. tests/test_layer_conformance.py derives the egress
destinations from the manifests and the registry from the pinned digests, so a
new contact appearing in deploy/ without a row fails the test rather than
waiting for a reviewer to notice.

Applying the manifests remains an operator action; nothing here was applied.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nb7Q6ZmXppNDkTWytfYqfv

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2069992@bnt-lap001
Assistant-Session: 167dd7f8-2a25-4be1-aa46-3b6f1a5f94c6
2026-09-10 16:39:20 +02:00
3c2cdcdf79 AUDIT-WP-0010 T01/T03/T04 — admit tenant-engine, and the envelope does not match
Registration: attributive, with the declared completeness_trade recorded on the
receiver side rather than only in the emitter, per §9.6's requirement that the
trade travel with the trail. tenants ["*"] is justified rather than inherited —
tenant-engine's events carry the affected tenant, the set is every tenant
including ones created later, and an explicit list would fail closed at exactly
the moment a tenant is provisioned, dead-lettering the creation evidence of the
tenant whose creation it is. source stays pinned exactly. Ingress ANDs namespace
and pod label; a weaker evidence class is not a reason for a wider network rule.
All inert until the token exists.

Then the finding T04 existed to find: tenant-engine cannot deliver a single
event today. envelope_for sends five required fields under other names —
event_id, action, resource, observed_at, details — and omits correlation_id
entirely, so normalize() raises invalid_event. Verified by running the real
envelope through the real function, not by reading.

Worse than an ordinary integration bug. The drain treats 400 as terminal, so the
outbox row is marked handled while audit-core holds only a dead letter, which is
not chained and is not custody. Lost on both sides, and since the drain is
non-blocking and attributive, nothing fails loudly — a silent total loss of the
stream presenting as a working integration.

Taking the correction the intake invited rather than accepting a lossy record.
normalize() is NOT relaxed to accept the alternate spellings: a receiver that
guesses which sender key means which stored field has made the mapping its own,
and the record stops being the sender's assertion. correlation_id cannot be
synthesized at all — an invented one ties an event to an operation audit-core
never observed.

Root cause is ours. The accepted envelope was published nowhere a sender could
read it; audit-backend-contract.md describes the stored record, and a sender
reading it would reasonably infer exactly the names tenant-engine used.
schema_version audit-core.event.v1alpha1 selects nothing here and gave a false
impression of a negotiated contract. Published docs/event-envelope.md as the
wire contract, including the point that a 400 means the event is not in the
archive and must be treated as a defect to fix rather than a delivery outcome.

T05 moved to wait: nothing to prove end to end until an event can be accepted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nb7Q6ZmXppNDkTWytfYqfv

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2069992@bnt-lap001
Assistant-Session: 167dd7f8-2a25-4be1-aa46-3b6f1a5f94c6
2026-09-10 16:34:45 +02:00
c4016a70d5 AUDIT-WP-0009-T11 — register informed-decision, and answer GH-DEC-2026-014
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
informed-decision is the browser-facing approver surface; GH-DEC-2026-012
limit 3 makes its evidence copy the one that must reach audit-core
independently of the emitter, because there the actor being audited and the
evidence source are the same component.

Registration accepted on every proposed field — exact source,
["tenant:platform"], write true, read false, load-bearing, secret_policy
redact. Prepared and inert: the scope overlay applies only to a sender the
Secret already carries, asserted by test rather than by reading. Ingress ANDs
namespace and pod label in one peer, following approval-engine rather than
user-engine's older breadth.

Gate House asked whether the record shape can carry a source-held-content
declaration with a retrieval expectation, and asked for a straight answer
rather than a rule the storage cannot meet. Both halves, which must travel
together:

  It CAN carry the declaration. data is stored verbatim into details.data and
  hash-chained, so content_exists and custody need no schema change and become
  as tamper-evident as the commitment they accompany.

  It CANNOT detect non-production. audit-core performs no retrieval and its
  egress permits Postgres and DNS only. Detection happens at retrieval, by the
  reviewer; the stored declaration is what turns a blank into a failure
  attributable to the named custodian.

Residual stated rather than left to be found: a custodian that never held the
content can emit a false content_exists. audit-core validates the declaration's
shape, never its truth — the same class as omission at source, and not closed
by the chain, by attestation, or by T04/T06. A test asserts no egress to the
emitter exists, because that claim silently stops being true if one appears.

Cadence: reconciliation plus heartbeat is right for a mixed-volume source, with
both scoped per class rather than per source — a per-source heartbeat is
satisfied by the high-volume presentation stream and says nothing about a quiet
month of dispositions. Bound: a compromised emitter suppresses the event and
its own count together.

Also recorded: commitment-only satisfies non-alteration and never
reconstructability, in this repo's documents as in theirs; and tenant
provenance under GH-DEC-2026-013 lands in the registration record, not the
envelope, since audit-core checks a value the credential may write rather than
resolving an identity claim.

No secret was created and no production manifest applied.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nb7Q6ZmXppNDkTWytfYqfv

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2069992@bnt-lap001
Assistant-Session: 167dd7f8-2a25-4be1-aa46-3b6f1a5f94c6
2026-09-10 15:15:35 +02:00
bbe8c15494 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
2026-09-08 17:01:41 +02:00
15e54369be AUDIT-WP-0009-T03/T09 — evidence_kind, and approval-engine's registration inputs
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
T03. §9.6 gives load-bearing and attributive sources different obligations, so
the archive must record which one a source declared rather than infer it from
traffic. evidence_kind and completeness_trade now sit on SenderIdentity, the
AUDIT_CORE_SENDERS schema, and the non-secret scope overlay.

Two asymmetries are deliberate. The default is attributive, because the other
default would have audit-core imply a completeness obligation no source ever
accepted. And the overlay may raise the kind but never lower it — the same
principle that stops an ExternalSecret refresh shrinking user-engine's
tenants: a ConfigMap refresh must not drop a source's atomicity and detection
obligations without anyone deciding to. A load-bearing source may not carry a
completeness trade at all, since §9.6 requires atomicity of it, and
evidence_declaration() reports completeness_claimed: false for both kinds.

T09 (in progress). approval-engine's registration inputs are prepared and
recorded in docs/approval-engine-source-registration.md: scope entry declared
load-bearing, and audit-core-approval-engine-ingress with namespace and pod
label ANDed in one `from` peer — narrower than user-engine's namespace-only
rule, which is left unchanged. The scope entry lands ahead of the credential
because the overlay only applies to senders the Secret already carries, so it
admits nothing until the token exists; a test asserts that rather than
trusting the reading.

Two inputs remain approval-engine's: a confirmed tenant scope, since senders.py
requires a missing tenant restriction be justified per sender and audit-core
cannot justify it on another repo's behalf, and an explicit secret_policy
choice. Applying the manifests is an operator action.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0185wifnLzCxjEY2MT1XbK7L

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 713962@bnt-lap001
Assistant-Session: 2718d99d-d3ff-478f-83a2-3a30f01a02fc
2026-09-06 22:31:37 +02:00
2f7f475e85 AUDIT-WP-0009-T01 — derive tamper_evidence from live attestation state
The Postgres backend returned tamper_evidence=True as a constant while
docs/integrity.md permits the claim only when a live external chain-head
attestation exists. The one attestation on record is 2026-08-16 and no job
renews it, so audit-core was telling every sender it had a property whose
precondition was unverified — the §9.6 defect it twice corrected in
gate-house's doctrine, turned inward.

evaluate_tamper_evidence() derives the flag from the chain report and the
mounted attestation, distinguishing seven states. Absence, staleness,
mismatch, an undated or unreadable attestation, a chain break, and an
unwalkable chain all degrade the claim rather than leave it standing.
Unreadable is treated as absent on purpose: a malformed file must not hold
up a claim a missing file would drop.

The freshness window is 168h against an intended daily cadence — seven
cadences, so a handful of missed runs degrade the claim rather than a single
one flapping it. Window and cadence are one contract in docs/integrity.md.

Production /readyz will now report tamper_evidence: false until
AUDIT-WP-0009-T02 schedules attestation. The claim was already false; it now
says so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0185wifnLzCxjEY2MT1XbK7L

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 713962@bnt-lap001
Assistant-Session: 2718d99d-d3ff-478f-83a2-3a30f01a02fc
2026-09-06 20:34:22 +02:00
f9d83a95a4 evidence(AUDIT-WP-0008): establish E2 target pass
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-sol
Assistant-Session: 01a02991-be07-7bb3-8b6d-e9701b5621de
2026-08-23 00:26:32 +02:00
c221921142 Record Whitehat E2 admission abort
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-sol
Assistant-Session: 01a02991-be07-7bb3-8b6d-e9701b5621de
2026-08-22 21:26:38 +02:00
8c8bcf49ae feat(AUDIT-WP-0008): add T02 synthetic load driver
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-sol
Assistant-Session: 01a02991-be07-7bb3-8b6d-e9701b5621de
2026-08-22 16:46:31 +02:00
abd22fa0a6 feat(AUDIT-WP-0008): enforce temporary sender expiry
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-sol
Assistant-Session: 01a025c2-407a-7a32-b40a-f37a52f03f62
2026-08-22 11:59:26 +02:00
5b5196eea7 feat(AUDIT-WP-0008): admit governed E2 probe ingress
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-sol
Assistant-Session: 01a025c2-407a-7a32-b40a-f37a52f03f62
2026-08-22 09:37:30 +02:00
58a106aa4d Implement AUDIT-WP-0008 T03, T06, T07, T08.
Some checks are pending
CI Smoke / host-smoke (push) Waiting to run
CI Smoke / container-smoke (push) Waiting to run
T08 fixes two defects in our own declaration. provider.R.available quoted a
30-day horizon audit-core does not solely control: at P1 the erasure horizon is
the instance maximum across co-residents, so a co-resident declaring longer
extends what a sender's records remain recoverable for, silently. Decision 4.5.4
names this for tiers; it applies to a provider quoting a number too, and the
provider block now says so. And user-engine, the only consumer, was notified
under Decision 6.1 -- what we declared, that E4 and R4 are unreachable here, and
that the retention number is a floor rather than a ceiling.

T03 writes docs/erasure-and-audit.md: the fact/payload split, why shreddability
is not retrofittable onto a chain committing to cleartext, and why the retained
hash is a confirmation oracle over low-entropy audit records. The framework half
was already resolved as Decision 4.5.3, so what remains is our own position. The
legal basis for retaining audit facts is routed to risk-nexus, open and visible.

T06 closes the review loop with net-kingdom: five findings adopted, declaration
validates clean, and the E line will go stale on an upgrade that Decision 6.1
deliberately does not require anyone to announce.

T07 enumerates the seven dependencies on the accept path and specifies five
recovery scenarios with integrity as a pass condition. It settled one thing: V2
is not reachable from P1 as built, since platform-pg runs instances 1 and
Decision 4.6.1 makes V the minimum across synchronous providers. V1 is the
ceiling here, not the next step. The exercise needs a live window.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:24:55 +02:00
4d5d989344 Refine AUDIT-WP-0008 open tasks against draft-8.
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
The framework moved from draft-5 to draft-8 while this workplan ran. No finding
was reversed, but three things changed underneath it: R moved to 2 once
rapp-postgres declared the window, a sixth axis V (availability) appeared, and
the implemented-versus-evidenced distinction became a schema field.

T03 reduces: the confirmation-oracle finding landed as Decision 4.5.3 and
question 11 is marked framework-resolved, so no amendment remains -- only our
own position document. The legal question routes to risk-nexus rather than
the-custodian, per §19.11 and policy-nexus INTENT.

T06 reduces to confirmation: all five findings were adopted and the two stale
status lines it was going to flag are already fixed.

T07 is new. V1 needs critical dependencies enumerated, restart recovery
exercised and recovery time measured. The 2026-08-16 reboot walk observed ~40s
of unreadiness but is not an exercise and does not enumerate the dependency set.

T08 is new and covers two defects in our own declaration. provider.R.available
quotes a 30-day horizon we do not solely control -- at P1 the horizon is the
instance maximum across co-residents. And under Decision 6.1, user-engine was
never told what we declared, which makes the declaration drift rather than a
completed change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:20:58 +02:00
a2d84bdd9b Declare the tenancy posture vector (AUDIT-WP-0008-T01).
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Written against draft-7, which landed after the task was drafted and moved the
target. Decision 5.4 fixes the location at tenancy.yaml in the repo root rather
than docs/, and fixes the schema: current, target, reviewed, gap,
placement_exceptions, service_class, per-path detail, provider block.

Declares I1 A2 E1 P1 R1. E is quoted at 1 although T04 put the E2 mechanism on
both paths, because §13.2 states a passing CI run is not E2 evidence -- the
artifact is adversarial, compares separate tenant contexts and carries a review
date. Our cross-tenant tests are mechanical, so under §13.1 the level is not
claimable until T05. The mechanism is recorded in paths.E and the reason in
gap.E. Claiming E2 off unit tests would be the overclaim §6 prohibits, and
refusing that reasoning is what found the read-path defect.

R stays at 1: R2 needs backupRetentionDays in rapp-postgres's consumer file,
requested in T02 and not ours to declare.

Two additions draft-7 forced. A credentials gap under Decision 9.2 -- our own
finding, adopted as a rule, and it binds us: ingest credentials are static
long-lived bearer tokens, declared as a stated gap rather than a silent
exclusion. And a provides block under Decision 5.5, declaring what a sender can
reach through this service: E2 now, E3 pending ADR-0003, E4 and R4 unreachable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 22:55:31 +02:00
aaa2b4c50b Scope the read path by tenant (AUDIT-WP-0008-T04).
All checks were successful
CI Smoke / host-smoke (push) Successful in 1s
CI Smoke / container-smoke (push) Successful in 2s
_read gated on may_read alone and never called permits_tenant, so any reader
credential could read every tenant through /v1/events, /v1/events/<id>,
/v1/dead-letters and /v1/secret-findings. Deployment bounded the exposure --
the only production sender holds may_read: false -- but the boundary was not in
the code, which is the difference between E2 and E1 on the tenancy posture
enforcement ladder.

Two rules, because the surfaces divide cleanly. Event reads are filtered to the
tenants the credential may act for. Surfaces with no tenant key to filter on --
stats, integrity, dead letters, secret findings -- require full tenant scope and
are refused rather than served instance-wide facts to a scoped reader.

A cross-tenant fetch returns 404 rather than 403. A distinguishable forbidden
would confirm that an event id exists and which tenant holds it, turning the
read surface into an existence oracle. Correlation lookup is filtered rather
than refused, since a correlation id legitimately spans tenants.

_readable_by fails closed: a record with no tenant is readable only at full
scope. Three existing tests read instance-wide surfaces with a scoped
credential, which this makes a 403; bound_app now carries an unrestricted
operator identity and those reads use it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 22:05:52 +02:00
61214bc24a Close AUDIT-WP-0007 after live chain attestation.
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Migration 0006 applied on railiance01. Verify-against the dated
chain-head evidence is intact for 30 events.
2026-08-16 01:23:54 +02:00
5fd04e2095 Implement AUDIT-WP-0007 hash-chain integrity.
Accept now extends a single-schema chain. Verify walks it; a rewritten
payload_hash is a break. Tamper evidence is that detector plus an
external chain-head attestation, not WORM.
2026-08-16 01:18:30 +02:00
5d46723d4e Cut over AUDIT-WP-0006 image and add AUDIT-WP-0007.
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 0s
Live receiver now reports custody_class=operational with a cited
30-day recoverable window (sha256:05fe1c06). Next workplan is
integrity verification so tamper_evidence can become honest.
2026-08-16 00:57:59 +02:00
ded432a63f Implement AUDIT-WP-0006 honest operational custody.
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
Postgres now reports custody_class=operational with a cited 30-day
recoverable window. Join ITC-CAP operations.audit at D4, publish the
interface card, and overlay user-engine tenants [*] from Git so an
ExternalSecret refresh cannot shrink it.
2026-08-16 00:24:33 +02:00
c2af842c55 Record audit sender OpenBao authority
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
2026-08-14 20:12:33 +02:00
c4dbb92a8a Finish AUDIT-WP-0005: walk restore and close T06
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Logical dump of live audit_core restored locally in 4.1s; counts and
sample triples match the rapp-postgres drill. Retention is platform
backup, not an audit-core deletion window. Workplan finished.
2026-08-13 14:14:22 +02:00
74575c4f32 Stand up railiance01 receiver without founder bao kv put
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Database leases come from ClusterSecretStore openbao-audit-core-database
(one extract, one lease). Senders are minted in-cluster. Secret mounts
use fsGroup 10001. Schema 0001-0004 applied after dropping the isolation
stub events table. /readyz reports custody_class=archive.
2026-08-13 10:27:13 +02:00
3a7d63e18f Prepare railiance01 delivery: dynamic leases, migrate Job, operator runbook
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
VaultDynamicSecret pulls database/creds/* so a rotating lease is not frozen
into KV. Runtime sets AUDIT_CORE_AUTO_MIGRATE=0; schema is a Job with the
migration lease. Image base is digest-pinned. Namespace and NetworkPolicies
are on the cluster; Deployment waits for the attended OpenBao ESO token.
2026-08-13 00:58:49 +02:00
bd274f6269 Add the PostgreSQL audit backend and a shared conformance suite
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
AUDIT-WP-0005-T01, built and verified against PostgreSQL 16 locally in
Docker; the Railiance cluster was not needed.

tests/test_backend_conformance.py is one suite run against every backend, so
"the Postgres backend is done" means it satisfies the same contract SQLite
already does rather than having its own green tests. It skips cleanly with no
server reachable; make pg-test-up and make test-pg run it. Suite 50 -> 71.

RetentionPolicy declares immutable=True and earns it: migration 0002 installs
a trigger rejecting UPDATE and DELETE on the events table, so a leaked runtime
credential can append but cannot rewrite or erase the trail. That materially
narrows the residual risk ADR-0001 section 5 called out. tamper_evidence stays
False because nothing here would prove a database owner had dropped the
trigger - hash-chaining or external anchoring would be needed and is not
implemented.

Idempotency is one statement (INSERT ... ON CONFLICT DO NOTHING RETURNING),
verified to behave identically to the SQLite backend under 12 concurrent
submissions of the same event. Migrations are ordered, recorded and
idempotent. Replay reconciles rather than duplicating - the piece deferred out
of WP-0004-T05 - and is tested to leave exactly one custody record.

Backend selection is by AUDIT_CORE_DATABASE_URL; the SQLite fallback logs a
warning so a deployment that lost its URL is visible rather than quietly
running on the wrong store.

Also fixed: ingestion had no __main__ guard, so python -m audit_core.ingestion
silently did nothing. Found during end-to-end smoke.

Counting semantics documented: occurrences counts transmissions, not stored
events, so a retry of a secret-shaped field increments it again. That is the
sender behaviour being optimized away.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 17:09:46 +02:00
576caa2665 Redact secret-shaped fields by default, countable per field path
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
AUDIT-WP-0004-T04, closing the workplan.

Decision (Bernd): default to redaction, allow rejection per sender. Losing an
audit record over one field is worse than storing it masked, but a
higher-assurance channel must be able to refuse rather than mask. secret_policy
is set per sender identity in AUDIT_CORE_SENDERS and defaults to redact.

Detection now covers the whole payload at any depth, including lists, rather
than only the top level of data. Under redaction the value is masked and the
key is preserved: dropping the key would hide that the sender transmitted the
field at all, which is exactly what an operator needs in order to stop it. The
stored record carries details.redaction with policy and affected paths, so a
reader never has to infer whether what they see is what was sent.

Idempotency is unaffected - the payload hash is taken over the original request
body, so redaction is deterministic and a resubmission still reconciles as a
duplicate.

Both outcomes are counted durably by sender, source, action and field path,
exposed at GET /v1/secret-findings. Per-path aggregation is the point: the
actionable unit is "stop emitting data.auth.token on membership.added", not
"there were 47 redactions". Counters survive restart because the fix they drive
lives in another service.

Contract doc updated to match. Tests 46 -> 50. WP-0004 is finished.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 16:02:22 +02:00
07f6046c4c Moved audit forward somewhat 2026-07-04 00:39:03 +02:00