Skip to content

1.0 readiness: integrated batch of 12 reviewed PRs#292

Merged
jeremi merged 47 commits into
mainfrom
integration/1.0-readiness
Jul 7, 2026
Merged

1.0 readiness: integrated batch of 12 reviewed PRs#292
jeremi merged 47 commits into
mainfrom
integration/1.0-readiness

Conversation

@jeremi

@jeremi jeremi commented Jul 7, 2026

Copy link
Copy Markdown
Member

Integrates 12 reviewed PRs into one branch, merged in dependency order with --no-ff --signoff merge commits (one per PR), cross-PR conflicts resolved once, and the full verification gate run on the final tip.

Integrated PRs

PR Title Branch SHA Summary
#277 fix(notary): repair the exposure-check security gate dfb4c6a0 Repairs just exposure-check (dead since the monorepo migration: manifest refs resolved only against the product tree) and wires it into root CI so it cannot go dark again. Merged first to unbreak the gate for everything after.
#268 fix(platform): raise default audit rotation retention to 50 files e42f3ae9 Raises the platform-audit default rotation retention from 5 to 50 files and hardens retained-suffix bootstrap.
#269 fix(relay): make waived deployment gates loud at boot 50d5ae45 Waiver-suppressed gate findings, expired waivers, and an undeclared profile now warn-log at boot, plus a deployment.gate_waived operational audit record per waived gate.
#273 feat(relay): add local auth-failure throttle d0ddd69f Opt-in in-process auth-failure throttle (auth.failure_throttle, stable 429 auth.rate_limited + Retry-After), keyed on trust-proxy-resolved client address, with a validation warning when enabled without trust_proxy.
#282 feat(relay): gate local-only audit retention behind off-host shipping attestation b43ddae2 New relay.audit.retention_local_only deployment gate + deployment.evidence.audit_offhost_shipping attestation; local rotating file audit sink without the attestation raises a posture finding (warn/production, error/evidence_grade).
#271 feat(notary): surface source bindings without a matching policy bf14bee1 doctor warning + notary.source_binding.no_matching_policy deployment gate for claim source bindings that declare no matching policy; resolution behavior unchanged, visibility only.
#283 feat(notary): gate local-only audit retention behind off-host shipping attestation 83ff4e3b Notary counterpart of #282: notary.audit.retention_local_only gate + deployment.evidence.audit_offhost_shipping attestation for file/jsonl audit sinks.
#276 feat(notary): add per-principal machine evaluation quota 47f608ac evidence.machine_quota: per-principal, subject-counted fixed-window quota over machine evaluate/batch_evaluate, 429 evaluation.quota_exceeded + Retry-After; disabled by default; self-attestation principals exempt.
#266 docs(manifest): mark datasets as optional in the manifest reference 3b949360 Manifest reference/overview now mark datasets as optional.
#270 docs(notary): document instance-global admin scope model 806074aa Documents that registry_notary:admin scope is instance-global (not per credential profile) and adds a pinning integration test.
#274 docs: note holder binding is the default for issuance f4978d67 Evidence-issuance explanation page now states holder binding is the issuance default.
#280 docs(release): record exercised upgrade and roll-back run v0.8.3 to v0.8.4 86f1d883 Records the exercised upgrade + roll-back run (v0.8.3 → v0.8.4) as a release exercise, with the lab compose topology used.

Review history

Each of the 12 PRs was reviewed by the Codex bot with all findings fixed and answered on-thread, then re-reviewed by expert reviewers (relay, notary, and docs panels). The expert-review findings — #268 retained-suffix hardening, #273 trust_proxy warning, #271 doctor double-report, #274 link cap, #280 exercise-record linking — were all fixed on the source branches before integration; the branch SHAs in the table above are the post-review heads, verified against the expected SHAs at integration time.

Conflict resolutions (done once, here)

Security review notes (consolidated)

These PRs touch security-sensitive areas (deployment posture gates, audit integrity, authentication throttling, evaluation quota) and carried explicit per-PR notes; condensed here:

  • fix(notary): repair the exposure-check security gate #277 (security-assurance tooling): re-arms a dormant gate; after merge, exposure-check failures block CI for the first time since the migration. The check itself is unweakened (path fallback applies only when the product-relative path does not exist).
  • fix(platform): raise default audit rotation retention to 50 files #268 (audit provenance / deployment defaults): strictly conservative — only increases retention (more history kept, same rotation mechanism and HMAC chain verification path); no change to chain integrity, redaction, or write semantics. Risk limited to added local disk usage (~500 MiB vs ~50 MiB) for deployments on the crate default.
  • fix(relay): make waived deployment gates loud at boot #269 (audit emission + gate handling): visibility-only. No gate severities or waiver semantics changed; startup_fail gates remain unwaivable; expiry comparison unchanged. Adds logging and an audit record for outcomes that already existed.
  • feat(relay): add local auth-failure throttle #273 (authentication path + audit): new deny surface 429 auth.rate_limited added to the error taxonomy with exhaustive variant/status tests. Throttle keys on the same trust-proxy-resolved client address audit already uses, so untrusted X-Forwarded-For cannot frame another address or evade the throttle. Throttled short-circuits are themselves audited. Data-plane and admin apps use independent throttle instances (flagged for reviewers as intentional).
  • feat(relay): gate local-only audit retention behind off-host shipping attestation #282 (relay deployment posture): posture-only change, no auth/authz/signing behavior; finding_error is a posture-only tier (no startup/readiness change); the gate is waivable with the standard mandatory-reason, mandatory-expiry waiver mechanism, like relay.audit.best_effort.
  • feat(notary): surface source bindings without a matching policy #271 (data-minimization visibility): no authorization, authentication, or claim-resolution behavior changed; the doctor check and gate are pure read-only projections over existing config and cannot fail a previously-succeeding resolution; both bound tiers are waivable by design.
  • feat(notary): gate local-only audit retention behind off-host shipping attestation #283 (notary deployment posture): deployment posture change only; defaults unchanged for local/hosted_lab; audit_offhost_shipping defaults to false, so an operator who does nothing keeps identical runtime behavior, just with an added posture finding where applicable.
  • feat(notary): add per-principal machine evaluation quota #276 (new denial surface on the evaluation path): self-attestation principals never reach the limiter (dedicated test with a colliding sub); quota keys on principal_id, which under auth.mode: oidc is caller-supplied (JWT sub/principal_claim) — the same identity already used for audit, now also a rate-limit key; fails closed (oversized key treated as over-quota); no change to authentication, credential issuance, signing, or audit redaction.
  • docs(notary): document instance-global admin scope model #270 (authorization documentation): documents existing enforcement only; adds a pinning test so a future change partitioning admin scope by profile fails loudly.

Verification (run on the final tip)

Command Result
cargo fmt --check pass
cargo check --locked --workspace --all-targets pass
cargo clippy --workspace --all-targets -- -D warnings pass
cargo test --locked --workspace pass (184/184 suites, 0 failures)
just openapi-check (products/notary) pass
just openapi-contract (crates/registry-relay) pass (OPENAPI_CONTRACT_BASE_REF unset, breaking-change diff skipped as usual locally)
just exposure-check (products/notary) pass (the #277 gate, now green)
npm test (docs/site) pass (44/44)
npm run check (docs/site) all sub-steps pass except build:archives, which failed only on a sandbox-blocked network clone (ssh to github.com timed out while fetching an external repo pin); check:seo:built (244 latest + 1192 archived HTML files) and check:links:built (254,850 internal links/assets) then run individually — pass
python3 -m unittest release/scripts/test_registry_release.py pass (29 tests)
REGISTRY_LAB_RELEASE_SOURCE_MODE=monorepo lab/scripts/check-release-source-model.sh pass

Merging this PR

Per-item revert granularity is preserved via the 12 per-PR merge commits on this branch. Merge this PR with a merge commit, not squash, so each integrated PR remains individually revertable.

Closes #207
Closes #78
Closes #171
Closes #28
Closes #58

jeremi added 30 commits July 7, 2026 01:29
The validator and REQ-DM-MANIFEST-003 both treat `datasets` as optional;
only `schema_version` and `catalog` are required. `DatasetManifest`
deserializes with `#[serde(default)]` (crates/registry-manifest-core/src/lib.rs:437),
`validate_manifest()` applies only an upper-bound count check to datasets,
and the spec's MUST at rs-dm-manifest.mdx scopes to `catalog` only.

Closes #28

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
JsonlFileSink::new used a 10 MiB x 5 file default rotation cap, retaining
only ~50 MiB of audit history before silently discarding the oldest
rotated file. Raise DEFAULT_MAX_FILES to 50 (~500 MiB) so the crate
default is a reasonable retention window on its own.

Registry Relay and Registry Notary both override rotation via
JsonlFileSink::with_rotation (100 MB x 14 files) and are unaffected.
In-tree, the only caller relying on the crate default is the
registry-platform-sts bridge binary, which was silently losing audit
history after ~50 MiB.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Waivers and an undeclared deployment profile were previously honored
silently at boot: visible only on the posture surface and in doctor,
with nothing in the boot log or the audit trail. Now every config load
warns per suppressed finding (deployment.gate_waived, with the finding
id, reason, and expiry), per expired waiver (deployment.waiver_expired),
and per undeclared profile (deployment.profile_undeclared). The serve
path additionally writes one operational audit record
(deployment.gate_waived) per waived gate once the audit pipeline exists,
so a boot that runs with reduced posture leaves durable evidence.

Part of #207.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
registry_notary:admin is intentionally global per Registry Notary
instance: enforcement is scope-string-only (EvidencePrincipal has no
issuer/tenant field), so one admin-scoped credential authorizes admin
operations for every credential profile the instance hosts. Partitioned
admin authority within a single instance is out of scope before 1.0.
The supported multi-tenant isolation model is one Registry Notary
instance per issuing authority.

Document this in the deployment hardening runbook and disambiguate it
from deployment.multi_instance, which declares horizontal scale-out of
a single issuing authority's workload (shared Redis-backed replay/audit
state) and is not a tenancy boundary.

Add admin_scope_is_instance_global_across_credential_profiles, a
standalone_http integration test that configures two credential
profiles (two issuers, distinct signing keys) on one instance and
shows a single registry_notary:admin credential is authorized for the
admin credential-status route against both, while a non-admin key is
denied for both. This pins the documented behavior so a future change
that tries to partition admin scope by profile fails loudly.

Closes #58

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
A claim source binding with no matching policy (no policy_id and no
context constraints) silently falls back to unrestricted,
identifier-only resolution per spec RS-DM-CLAIM. Nothing warned the
operator.

This adds visibility only; claim resolution behavior is unchanged:

- registry-notary doctor now emits notary.source_binding.no_matching_policy,
  naming every claim/binding pair that lacks a matching policy.
- A matching deployment gate surfaces the same fact as a finding_warn
  under production and a finding_error under evidence_grade; local and
  hosted_lab stay quiet since the fallback is spec-conformant and only
  becomes an operational concern at higher assurance tiers. Both bound
  tiers are waivable, so a waiver is the sanctioned way to accept the
  fallback deliberately.

Closes #171

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Add a disabled-by-default, in-process, coarse throttle on repeated
authentication failures from one client address (auth.failure_throttle).
When enabled, an address that reaches max_failures failed auth attempts
within window_seconds gets a stable 429 (auth.rate_limited, with
Retry-After) before the auth provider runs, for every request from that
address until the window rolls over; successful auth neither counts nor
resets. Ingress rate limiting remains the primary control against
high-volume abuse; this is a local backstop for deployments without a
gateway in front of them, or defense in depth behind one. It does not
throttle other expensive routes for authenticated callers, which is
deliberately deferred.

The throttle keys on the same trust-proxy-aware client address the audit
record's remote_addr reports, extracted from audit/mod.rs into a shared
crate::net::resolve_remote_addr so both subsystems agree on which address
a request came from and untrusted X-Forwarded-For values cannot be used
to evade the throttle or split it across fake keys.

The map is bounded (capped entries, oldest-window eviction) so it cannot
grow without bound under a flood of spoofed source addresses.

Disabled by default: when auth.failure_throttle.enabled is false (the
default), no throttle is constructed and the auth middleware's behavior
is unchanged from before this feature.

Part of #78

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Registry Notary had per-request size caps and source limits, but no budget
on sustained evaluate/batch_evaluate volume from a single machine principal.
Add evidence.machine_quota: a fixed one-minute window, counted in subjects
(a single evaluate costs 1, a batch costs items.len()), keyed by
principal_id, and enforced only for principals that are not classified as
self-attestation. A request whose cost would exceed the remaining budget is
rejected whole, so a denied batch never partially consumes the window.
Exhaustion returns 429 with the stable code evaluation.quota_exceeded and a
Retry-After header.

The quota is disabled by default (subjects_per_minute: 6000 when enabled),
so existing deployments are unaffected until an operator opts in.

Part of #78 (carries former #60 scope).

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
check_security_assurance.py resolved every manifest ref (enforcement
test files, route-source files) as products/notary-relative, but the
refs in exposure-manifest.json and route-inventory.json point at
crates/... paths that only exist at the monorepo root. This has made
`just exposure-check` fail unconditionally since the monorepo
migration (d2d2cf2); the gate is not wired into root CI, so the
breakage was invisible.

Fall back to the monorepo root when a ref does not resolve under the
product tree, and scope the route-source crate scan to registry-notary*
crates so it does not pick up unrelated shared crates (e.g. the
registry-platform-testing mock server) that also live under crates/.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The exposure-manifest/route-inventory security gate has never run in
root CI, so its breakage during the monorepo migration went unnoticed.
Run `just exposure-check` alongside the existing OpenAPI checks now
that its path resolution is fixed.

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

Exercise the draft operate/upgrade-and-rollback how-to against the lab
topology in an isolated compose project: v0.8.3 baseline, doctor-gated
upgrade to v0.8.4, verified roll back to v0.8.3. Evidence and the full
findings list (config schema break on fingerprint.commitment, stdout
audit sink vs 'audit log directory' backup, lazily created anti-rollback
state file, and more) live in release/exercises/, which this commit
introduces. Ticks the READINESS gate with a link to the evidence.

Part of #203.

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

A local rotating file audit sink caps retention, and an attacker with host
access can destroy the audit trail. Add the
deployment.evidence.audit_offhost_shipping attestation and the
relay.audit.retention_local_only gate, which fires only for a local file
sink without the attestation. Stdout and syslog sinks are exempt: stdout
retention is the orchestrator's log pipeline's concern, and syslog
forwarding is the syslog daemon's own surface. Bound at finding_warn under
production and finding_error under evidence_grade; unbound under
local/hosted_lab; waivable like the other audit posture gates.

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

A local file or jsonl audit sink caps retention to whatever the host
disk holds, and an attacker with host access can destroy the evidence.
Add deployment.evidence.audit_offhost_shipping (default false) so
operators can attest that audit events are shipped off-host, and bind
a new notary.audit.retention_local_only gate that fires when the sink
is file/jsonl and the attestation is absent. stdout and syslog sinks
are exempt: their retention is owned by the orchestrator log pipeline
or the syslog daemon's own forwarding surface.

Severity bindings: unbound under local and hosted_lab, finding_warn
under production, finding_error under evidence_grade. Waivable like
other finding-tier gates; no startup_fail or readiness_fail. The
existing audit_sink_is_durable predicate and notary.audit.sink_missing
gate are untouched.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The machine evaluation quota limiter keeps its counters in an in-memory
map and each RegistryNotaryApiState builds its own limiter, so the quota
is enforced per Notary process, not cluster-wide. Document that N
replicas give a caller up to N x subjects_per_minute, and point
operators at replica-count sizing or a shared limiter.

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

Add the missing infra port env exports and -p registry-upgrade-ex
project flag to every recorded compose command so the exercise is
actually reproducible alongside the default lab, and record that the
doctor gate never checked openfn-civil-notary.yaml (v0.8.4's breaking
source-adapter connector rename) as an explicit gap instead of leaving
it implied as validated.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The retained-suffix compatibility path trusted the first parsed
envelope's prev_hash whenever the oldest file on disk was rotated. In a
partial rotation set where the oldest rotated file was truncated to
empty, the first envelope came from a newer file, so bootstrap anchored
on it and silently accepted a chain missing its leading audit records.

Grant the compatibility path only when the oldest rotated file actually
supplies the anchor record; an empty oldest file now falls back to
genesis-strict verification and fails, surfacing the missing records.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
matching_policy_diagnostics always emitted an explicit
notary.source_binding.no_matching_policy diagnostic, even when the active
profile already binds that finding through the deployment gate catalog
(production: finding_warn, evidence_grade: finding_error). Report assembly
never dedups by code, so doctor --profile production/evidence_grade showed
the same code twice with divergent severities.

Add gate_severity_for_profile to look up whether a gate binds under the
active profile, and suppress the explicit diagnostic when it does. local,
hosted_lab, and undeclared profiles leave the gate unbound, so they keep the
explicit diagnostic's visibility.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Behind a proxy or load balancer with server.trust_proxy left disabled
(the default), the throttle keys on the proxy's own socket address, so
every client shares one bucket and combined failures from anyone can
429 all clients until the window rolls. Emit a config.validation_warning
finding at boot so operators catch the misconfiguration instead of
discovering it as a self-inflicted outage.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Clarify that the boot-time deployment.gate_waived audit write is
subject to audit.write_policy: fail_closed aborts startup on write
failure, availability_first logs and continues, making the durable
record best-effort and the per-gate boot log warnings the real floor.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…to 50 files

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
jeremi added 7 commits July 7, 2026 11:41
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>

# Conflicts:
#	products/notary/CHANGELOG.md
…est reference

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
…run v0.8.3 to v0.8.4

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Both fix/relay-boot-gate-visibility (#269) and feat/relay-auth-failure-throttle
(#273) added an identical SharedLog/SharedLogWriter tracing capture helper to
config/validate.rs tests. The textual auto-merge kept both copies, producing
E0119/E0428 duplicate-definition errors. Keep one copy of the shared types and
both distinct capture functions (run_with_captured_logs,
captured_throttle_validation_logs).

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

PR #276's machine_quota_http_test used assert_eq!(x.is_string(), true), which
trips clippy::bool_assert_comparison under -D warnings on rust 1.95. Pre-existing
on the source branch; surfaces in the integration workspace clippy gate. Replace
with assert!(x.is_string()) per the lint's own suggestion.

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9ac087f58a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/registry-relay/src/config/validate.rs Outdated
Comment thread crates/registry-platform-audit/src/lib.rs
Comment thread docs/site/src/content/docs/reference/errors.mdx Outdated
jeremi added 5 commits July 7, 2026 15:00
The auth-failure-throttle self-DoS warning only fired when
server.trust_proxy.enabled was false. With trust_proxy enabled but an
empty trusted_proxies list, resolve_remote_addr matches no peer and still
ignores X-Forwarded-For, collapsing every forwarded request onto the
proxy's socket address: the same shared-bucket 429-everyone condition,
warning suppressed. Extend the warning to fire on enabled+empty-list,
update the docs paragraph, and add a fourth tracing-capture test.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Codex flagged that deleting the true oldest rotated file leaves an
on-disk state byte-identical to a legitimate aged-out rotation: the
remaining oldest record has a non-None prev_hash, the retained-suffix
compat path anchors on it, and a restart silently accepts the loss of
the leading records. Confirmed real but undecidable from local disk
alone (middle-of-set gaps are still caught by boundary PrevHashMismatch
during concatenated verification; only oldest-file deletion is
indistinguishable). Verification behavior is unchanged by design; the
compat path now emits a tracing warning so operators can correlate, with
off-host audit shipping / external anchoring as the structural
mitigation. First platform crate to take a tracing dependency.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
The auth.rate_limited code is emitted by Relay's AuthError::RateLimited
(the auth.failure_throttle backstop), not by Notary. Notary throttling
surfaces self_attestation.rate_limited and evaluation.quota_exceeded
instead. Move the row out of the Registry Notary table into Registry
Relay > Authentication, matching that table's terser column style.

Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
Signed-off-by: Jeremi Joslin <jeremi@joslin.fr>
@jeremi jeremi enabled auto-merge (rebase) July 7, 2026 08:50
auto-merge was automatically disabled July 7, 2026 08:53

Rebase failed

@jeremi jeremi merged commit 0541f24 into main Jul 7, 2026
10 checks passed
@jeremi jeremi deleted the integration/1.0-readiness branch July 7, 2026 08:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment