Skip to content

feat(signature): persist a durable per-repo TOFU signing baseline (JEF-263)#136

Merged
thejefflarson merged 1 commit into
mainfrom
thejefflarson/jef-263-persist-a-durable-per-repo-tofu-signing-baseline-decision
Jul 1, 2026
Merged

feat(signature): persist a durable per-repo TOFU signing baseline (JEF-263)#136
thejefflarson merged 1 commit into
mainfrom
thejefflarson/jef-263-persist-a-durable-per-repo-tofu-signing-baseline-decision

Conversation

@thejefflarson

Copy link
Copy Markdown
Owner

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 variant — 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 (PROTECTOR_ENGINE_JOURNAL_PATH), no second store/env var.
  • state::SigningBaselineStore (new module engine/src/engine/state/signing_baseline.rs) — learns per registry/repo from Signed postures only, persists to and boot-replays from the durable journal, bounded with defined eviction. Exposed for JEF-264 to consume.
  • Boot replay — restored in run_watch alongside the admission-log restore, folding SigningBaseline lines last-write-wins (compaction semantics). This is how TOFU survives a restart.
  • Compaction, not rotation-aging — each baseline line is full state; the store re-appends every live repo per pass, so rotation never ages out a live established baseline and silently re-arms cold-start trust. A negative-control test proves a single un-recompacted line does age out.
  • Feeding — wired into the existing running-Pod sweep (signing_sweep::sweep), additive; existing callers pass None + a disabled journal.
  • repo_key — reuses policies/signature's registry-host canonicalization, then strips :tag/@digest (preserving a host:port).

Acceptance criteria

  • Observing a signed image updates the repo baseline (identities/issuers, established) keyed by registry/repoobserving_a_signed_image_creates_a_repo_keyed_baseline, sweep_teaches_the_repo_baseline_from_a_signed_image.
  • A new digest/tag under a repo with an existing baseline does NOT create a new baseline and is not drift — a_new_tag_or_digest_under_a_known_repo_is_not_a_new_baseline.
  • Survives restart (write + boot-replay), round-trip test — baseline_survives_an_engine_restart_round_trip.
  • Fresh vs established distinguishable (established: bool + first_seen) — a_freshly_learned_baseline_is_distinguishable_from_an_established_one.
  • Rotation never drops a live established baseline (compaction test) — compaction_keeps_a_live_established_baseline_across_rotation.
  • Bounded state + defined eviction; never leaves the cluster — the_store_is_bounded_and_evicts_non_established_before_established.

ADR-0020 addenda (decisions recorded)

  1. Eviction. Durability-wise, eviction is compaction-in-journal (rotation-safe re-append). In-memory, the store is bounded by 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.
  2. established = wall-clock age (24h since first_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 the first_seen_ms we already persist. This matches ADR-0020's framing that "a freshly-learned … baseline is surfaced as weaker evidence than an aged … one." Both established + first_seen are 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_REPOS and small in practice; flagged for the architect if a split-journal or change-only-compaction refinement is wanted.

🤖 Generated with Claude Code

…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 thejefflarson merged commit 21287fe into main Jul 1, 2026
2 of 3 checks passed
@thejefflarson thejefflarson deleted the thejefflarson/jef-263-persist-a-durable-per-repo-tofu-signing-baseline-decision branch July 1, 2026 02:42
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>
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.

1 participant