Commit graph

16 commits

Author SHA1 Message Date
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