feat(signature): persist a durable per-repo TOFU signing baseline (JEF-263)#136
Merged
thejefflarson merged 1 commit intoJul 1, 2026
Conversation
…F-263)
Observation (JEF-261) is a snapshot; it can't catch a *change* in a repo's
signing posture over time — the actual supply-chain attack. This adds the
learned, restart-surviving history ADR-0020 §2 calls for: per `registry/repo`
(never tag/digest), the identity/issuer set observed signing that source, when
it was first seen signed, and whether that history is `established` yet (TOFU).
- New tagged `journal::Decision::SigningBaseline { repo, identities, issuers,
first_seen_ms, established }`, full-state JSON line, `#[serde(default)]` on
every field for forward-compat. SAME journal file, no second store/env var.
- `state::SigningBaselineStore`: learns only from a `Signed` posture (a new
tag/digest under a known repo folds to the same key — not a new baseline,
not drift), persists to and boot-replays from the durable journal, and is
bounded (DEFAULT_MAX_REPOS) with eviction that drops non-established entries
before established ones.
- Compaction, not rotation-aging: each baseline line is full state
(last-write-wins on replay) and the store re-appends every live repo per
pass, so rotation never ages out a live established baseline and re-arms
cold-start trust.
- `established` = wall-clock age (24h since first_seen), not digest-count:
the first observation is the weakest evidence and an attacker can inflate a
digest counter; age can't be gamed and needs no extra durable state. Exposes
`established` + `first_seen` for JEF-262/JEF-264 to render/weigh.
- Reuses `policies/signature`'s registry-host canonicalization via a new
`repo_key` (host-normalize, then strip `:tag`/`@digest`, preserving host:port).
- Wired into the existing running-Pod sweep (additive) and boot-replayed in
run_watch alongside the admission-log restore.
Scope: persistence + in-memory store + boot replay only. Drift findings
(JEF-264), enforcement (JEF-265), the dashboard render (JEF-262), and Rekor
(JEF-266) consume this; they are NOT built here. Degraded mode is honest: a
disabled journal ⇒ in-memory only, resets on restart, re-learns from
observation.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP
thejefflarson
added a commit
that referenced
this pull request
Jul 1, 2026
… established (#138) Records the two implementation decisions the durable per-repo signing baseline (#136) required: (1) eviction = per-pass full-state journal compaction + a bounded in-memory store that evicts non-established entries first; (2) `established` = 24h wall-clock age from first_seen, not a digest count an attacker could inflate. Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes JEF-263
What this ships
Observation (JEF-261) is a snapshot — it can't catch a change in a repo's signing posture over time, which is the actual supply-chain attack. This adds the learned, restart-surviving history ADR-0020 §2 calls for.
journal::Decision::SigningBaseline { repo, identities, issuers, first_seen_ms, established }. Full-state JSON line,#[serde(default)]on every field for forward-compat. SAME journal file (PROTECTOR_ENGINE_JOURNAL_PATH), no second store/env var.state::SigningBaselineStore(new moduleengine/src/engine/state/signing_baseline.rs) — learns perregistry/repofromSignedpostures only, persists to and boot-replays from the durable journal, bounded with defined eviction. Exposed for JEF-264 to consume.run_watchalongside the admission-log restore, foldingSigningBaselinelines last-write-wins (compaction semantics). This is how TOFU survives a restart.signing_sweep::sweep), additive; existing callers passNone+ a disabled journal.repo_key— reusespolicies/signature's registry-host canonicalization, then strips:tag/@digest(preserving ahost:port).Acceptance criteria
registry/repo—observing_a_signed_image_creates_a_repo_keyed_baseline,sweep_teaches_the_repo_baseline_from_a_signed_image.a_new_tag_or_digest_under_a_known_repo_is_not_a_new_baseline.baseline_survives_an_engine_restart_round_trip.established: bool+first_seen) —a_freshly_learned_baseline_is_distinguishable_from_an_established_one.compaction_keeps_a_live_established_baseline_across_rotation.the_store_is_bounded_and_evicts_non_established_before_established.ADR-0020 addenda (decisions recorded)
DEFAULT_MAX_REPOS(4096, a safety cap — real clusters stay far below) and evicts a non-established entry (cheap to re-learn) before an established one, least-recently-updated first, so a matured baseline is never dropped for churn.established= wall-clock age (24h sincefirst_seen), not digest-count. The first observation is the weakest evidence (could be the attacker's first signed push), so trust should mature over time. A digest counter is gameable (burst of digests) and needs extra durable state; age can't be gamed and reuses thefirst_seen_mswe already persist. This matches ADR-0020's framing that "a freshly-learned … baseline is surfaced as weaker evidence than an aged … one." Bothestablished+first_seenare exposed for JEF-262/JEF-264 to render/weigh; a digest-count/distinct-day refinement is a documented future option.Scope
Persistence + in-memory store + boot replay only. Drift findings (JEF-264), enforcement (JEF-265), the dashboard render (JEF-262 — untouched), and Rekor (JEF-266) consume this; not built here. Degraded mode is honest: a disabled journal ⇒ in-memory only, resets on restart, re-learns from observation.
Invariants
Shadow-only (pure learning, never gates); zero-egress (writes the local journal mount, no new outbound path); presentation-is-a-view (the sweep still records
allow); untrusted Fulcio identities/issuers carried verbatim and documented as escape-at-render for the consumer.Testing
cargo fmt·cargo build·cargo clippy --all-targets(zero warnings) ·cargo test— 402 lib tests pass (15 new baseline + 2 new sweep), file-size guard green.Risk note
Per-pass full compaction shares one journal with breach/admission decisions; a very large repo count could shorten their rotation window. Bounded by
DEFAULT_MAX_REPOSand small in practice; flagged for the architect if a split-journal or change-only-compaction refinement is wanted.🤖 Generated with Claude Code