Skip to content

fix(audit): require off-host completeness evidence#332

Merged
jeremi merged 15 commits into
mainfrom
agent/audit-offhost-completeness-315
Jul 10, 2026
Merged

fix(audit): require off-host completeness evidence#332
jeremi merged 15 commits into
mainfrom
agent/audit-offhost-completeness-315

Conversation

@jeremi

@jeremi jeremi commented Jul 9, 2026

Copy link
Copy Markdown
Member

Closes #315.

Summary

  • Make local file/jsonl audit retention without off-host shipping a startup failure under evidence-grade Relay and Notary deployments.
  • Add nested posture.audit shipping-state fields and update the shared posture schema, fixtures, docs, changelogs, and Notary OpenAPI artifact.
  • Reframe registry-platform-audit verification as retained-set tamper evidence, remove local trusted-anchor helper APIs, and stop writing .anchor.json recovery sidecars.

Security invariants

  • Local hash chains now explicitly prove retained-set integrity only. They do not claim completeness, leading-record retention, or full-rewrite detection.
  • Evidence-grade completeness requires off-host shipping. stdout and syslog are treated as externally collected sinks; local file/jsonl requires deployment.evidence.audit_offhost_shipping.
  • startup_fail and readiness_fail gates are not waiver-suppressed in Relay or Notary; a waiver naming one is rejected at config load. Production warning waivers still work where the gate remains a warning.

Verification

  • cargo fmt --all -- --check
  • cargo test --locked -p registry-platform-audit
  • cargo test --locked -p registry-platform-testing --features test-utils
  • cargo test --locked -p registry-platform-ops --test posture_contract
  • cargo test --locked -p registry-relay --test deployment_profile_gates
  • cargo test --locked -p registry-relay audit_retention_local_only
  • cargo test --locked -p registry-relay posture_audit_summary_reports_shipping_state
  • cargo test --locked -p registry-relay audit_quarantine
  • cargo test --locked -p registry-notary-core audit_retention_local_only
  • cargo test --locked -p registry-notary-server --test deployment_gates_test
  • cargo clippy --locked -p registry-platform-audit -p registry-platform-testing -p registry-platform-ops -p registry-relay -p registry-notary-core -p registry-notary-server --all-targets --features registry-platform-testing/test-utils -- -D warnings
  • cargo check --locked -p registry-platform-audit -p registry-platform-testing -p registry-platform-ops -p registry-relay -p registry-notary-core -p registry-notary-server --all-targets --features registry-platform-testing/test-utils
  • just openapi-contract in products/notary and crates/registry-relay (OPENAPI_CONTRACT_BASE_REF unset, so breaking-change diff skipped)
  • npm run check in docs/site (passes; pre-existing Vale/Redocly/archive warnings only)

Review follow-ups

Two rounds of fixes pushed after review.

Round 1 (2ade7666..354594d1): quarantine legacy .anchor.json sidecars during chain recovery; reject waivers naming hard deployment gates at Relay config load (parity with Notary's HardGateNotWaivable); shared audit sink classification in registry-platform-ops; remove the always-null posture shipping-health fields (BREAKING for strict posture validators); report declared shipping state in both doctor outputs; correct chain-guarantee docs to interior-only detection; changelog migration notes.

Round 2 (354594d1..a00c385b): observed shipping health.

  • New contract registry.audit.ack_cursor.v1 (registry-platform-ops): a local state file written by whatever ships audit events off-host (acked_at, last_acked_hash, optional writer). evaluate_ack_health maps it to ok/stale/missing/invalid/unverified; 900s default freshness window, 300s future-skew tolerance, fail closed on unreadable or malformed cursors.
  • New config (both products): deployment.evidence.audit_ack_cursor_path and deployment.evidence.audit_ack_max_age_secs, with load-time validation (max-age without a cursor rejected; a cursor on a local file sink without audit_offhost_shipping rejected; stdout/syslog may carry a cursor without the attestation).
  • New gates (both products): *.audit.shipping_unverified (off-host shipping declared for a local sink without a cursor: finding_warn under production and evidence_grade) and *.audit.shipping_stale (cursor stale, missing, or invalid: finding_error under production, readiness_fail under evidence_grade, non-waivable there).
  • BREAKING (Relay): readiness_fail gates are no longer waivable, matching Notary. Waivers naming them fail config load with per-gate remediation. Affected combinations: relay.admin.public_exposure and relay.audit.sink_missing under production; relay.oidc.client_allowlist_empty, relay.audit.best_effort, and relay.audit.shipping_stale under evidence_grade.
  • BREAKING (posture v1): posture_audit adds required nullable shipping_health and shipping_observed_at; strict validators must update together with this release.
  • Doctor output, the admin audit summary, and registryctl's typed doctor aggregation carry the shipping section end to end; the Notary OpenAPI document and the docs-site copies were regenerated via their documented generators only.

Security review notes (round 2)

  • The ack cursor is a liveness and freshness signal for the shipping path, not proof of complete delivery; the PR does not invent delivery-health values from unobserved sources. last_acked_hash binds acks to the chain so a later change can add backlog-depth checks without a contract break.
  • The cursor file is read when gate input, doctor, or posture output is built, never inside a gate condition; parse failures surface as invalid (fail closed) with the reason in the doctor detail. No secrets are read or logged.
  • The waivability tightening is deliberate fail-closed hardening. The operator escape hatch for a stale-cursor readiness failure is reconfiguration (widen audit_ack_max_age_secs or remove audit_ack_cursor_path), not a waiver.

Verification (round 2, at a00c385b)

  • cargo fmt --check; cargo check --locked --workspace --all-targets; cargo clippy --workspace --all-targets -- -D warnings
  • cargo test --locked --workspace: 3017 passed, 0 failed, 8 ignored
  • just openapi-check (products/notary); just openapi-contract (crates/registry-relay)
  • docs/site: npm test (46/46) and npm run check (all stages pass; one transient GitHub disconnect during build:archives passed on isolated re-run)

Merge with main (049945d0): origin/main moved during the follow-up work (signer-custody gate, audit denial coverage, registryctl template profiles, docs). Resolved as a union of both notary gate-catalog additions; the notary OpenAPI document was regenerated via its generator rather than trusting the textual auto-merge. Full gate re-run at 049945d0: 3038 workspace tests passed, 0 failed, clippy clean, both OpenAPI checks and all 15 docs-site check stages green.

Related: #335 fixes the OpenAPI base-comparison path bug found during review (separate PR; does not block this one).

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@jeremi jeremi force-pushed the agent/audit-offhost-completeness-315 branch from c713ace to bee77aa Compare July 9, 2026 15:57
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
@jeremi jeremi force-pushed the agent/audit-offhost-completeness-315 branch from bee77aa to 2ade766 Compare July 9, 2026 16:11
jeremi added 9 commits July 10, 2026 04:27
A release that still wrote `<path>.anchor.json` completeness anchors
can leave one behind after an upgrade; recovery moved the data files
aside but let the stale sidecar keep describing the quarantined chain
as current. Rename it with the same `.corrupt-<timestamp>` suffix as
the data files. Also drop the unused `_max_size_bytes` parameter from
`tail_hash_from_files` and pin the retained-set semantics with explicit
trailing-truncation and interior-deletion verification tests.

Security review: audit integrity. Recovery only renames an existing
sidecar; verification semantics are unchanged and now test-documented.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
A waiver targeting a gate that is startup-fail under the active profile
was silently ignored at evaluation, letting an operator believe a hard
gate was waived. Notary already rejects these at load
(HardGateNotWaivable); Relay now does the same, sharing one waivability
definition with gate evaluation and logging the gate id plus concrete
remediation. The retention gate description also spells out what a
local-only sink cannot detect.

Security review: deployment gates. Fail-closed only: no severity or
waivability changed; previously-ignored waivers now fail load, which is
a breaking config change recorded in the changelog.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
HardGateNotWaivable named the gate but not what to do about it. Spell
out the levers: remove the waiver and fix the condition it reports; for
audit retention, ship audit events off-host and declare
deployment.evidence.audit_offhost_shipping, or use a non-local sink.
The retention gate description now states the truncation rationale.

Security review: message text only; no gate logic changed.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…k classification

posture.audit shipped last_successful_ship_at and backlog_depth as
required fields hardcoded null in both products: there is no observed
shipping-health contract yet (source, freshness, and backlog thresholds
are an open maintainer decision), so the contract must not carry dead
required fields. Remove them from the schema, emitters, fixtures, and
the regenerated Notary OpenAPI baseline. The remaining
shipping_target_configured/shipping_target pair is declared,
config-derived state; its classification now lives in one
registry-platform-ops helper (AuditSinkKind, audit_shipping_target) so
Relay and Notary cannot drift, with the unknown-sink case an explicit
variant instead of a silent wildcard.

Security review: audit evidence surface. Emitted values are unchanged
apart from the removed always-null keys; strict posture validators are
affected as recorded in the changelog.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The machine-readable doctor reports could not distinguish a deployment
with declared off-host shipping from one without. Both products now
emit an audit_shipping object (sink_type, shipping_target_configured,
shipping_target) derived from the shared classification helper, added
to the strict diagnostic-report schema as an optional section that
degrades like `hashes` when config is unavailable. Notary's
"local-chain-only" warning now fires only when off-host shipping is
not declared, matching the deployment gate it echoes. registryctl
passes the optional field through as raw JSON; teaching its typed
aggregation to propagate it is a follow-up.

Security review: declared, config-derived state only; the schema
rejects invented delivery-health fields until that contract is defined.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The audit README claimed deletions "after the first retained envelope"
are detected, which is false for trailing truncation; the notary-server
README still walked through the deleted anchored head/tail workflow;
SECURITY_PRINCIPLES still pointed at the removed anchored verification
helpers. State the real guarantee everywhere: interior edits,
insertions, reordering, and deletions only; leading/trailing truncation
and self-consistent rewrites need off-host completeness evidence.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Name the removed anchor APIs with migration guidance, the legacy
sidecar quarantine, the posture.audit contract change for strict
validators, the hard-gate waiver load failure with migration steps,
and the doctor audit_shipping section.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…health

Add registry.audit.ack_cursor.v1: a local state file written by whatever
ships audit events off-host (acked_at, last_acked_hash, writer), plus a
shared evaluate_ack_health helper (ok/stale/missing/invalid/unverified,
900s default freshness window, 300s future-skew tolerance). Posture v1
gains required nullable shipping_health/shipping_observed_at fields and
the diagnostic report's audit_shipping section gains them optionally, so
declared shipping evidence can now carry an observed freshness signal.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Align relay with notary's is_waivable: readiness gates exist to stop
traffic when evidence guarantees fail, so a waiver must not silently
un-fail them. Waivers naming readiness-fail gates are now rejected at
config load with per-gate remediation guidance. BREAKING for operators
waiving readiness gates (e.g. relay.audit.best_effort under
evidence_grade).

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
jeremi added 5 commits July 10, 2026 08:38
Read the registry.audit.ack_cursor.v1 file named by
deployment.evidence.audit_ack_cursor_path when building gate facts.
relay.audit.shipping_unverified warns when off-host shipping is declared
without a cursor; relay.audit.shipping_stale (finding_error under
production, readiness_fail under evidence_grade, non-waivable) fires
when the cursor is stale, missing, or invalid. Posture and doctor now
report shipping_health and shipping_observed_at.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Read the registry.audit.ack_cursor.v1 file named by
deployment.evidence.audit_ack_cursor_path when building gate input.
notary.audit.shipping_unverified warns when off-host shipping is
declared without a cursor; notary.audit.shipping_stale (finding_error
under production, readiness_fail under evidence_grade, non-waivable)
fires when the cursor is stale, missing, or invalid. Posture and doctor
report shipping_health and shipping_observed_at; OpenAPI regenerated.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Model the optional audit_shipping section as a typed field on
ConfigDiagnosticReport so registryctl's typed aggregation carries it
through instead of silently dropping it on re-serialization.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…iver rules

Record the registry.audit.ack_cursor.v1 contract, the shipping_unverified
and shipping_stale gates, the new evidence config fields, the posture and
doctor shipping-health fields, and the breaking relay readiness-waiver
rejection in the changelogs and operator references. Frame the cursor as
a liveness signal for the shipping path, not delivery-completeness proof.
Correct notary's waiver reference: readiness_fail gates are also rejected
at config load.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…ompleteness-315

Union-resolved the notary gate catalog, gate tests, and operator config
reference so main's signer-custody gate coexists with the branch's audit
shipping gates. Regenerated products/notary/openapi/registry-notary.openapi.json
with the documented generator instead of trusting the textual auto-merge.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
@jeremi jeremi merged commit c481673 into main Jul 10, 2026
11 checks passed
@jeremi jeremi deleted the agent/audit-offhost-completeness-315 branch July 10, 2026 07:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

audit: make off-host shipping the enforced completeness guarantee; demote the local chain to tamper-evidence

1 participant