Skip to content

fix(signature): keep the TUF/tough atomic temp writes off /tmp (JEF-377)#196

Merged
thejefflarson merged 2 commits into
mainfrom
thejefflarson/jef-377-reduce-tuf-write-churn-keep-sigstoretough-atomic-writes-off
Jul 7, 2026
Merged

fix(signature): keep the TUF/tough atomic temp writes off /tmp (JEF-377)#196
thejefflarson merged 2 commits into
mainfrom
thejefflarson/jef-377-reduce-tuf-write-churn-keep-sigstoretough-atomic-writes-off

Conversation

@thejefflarson

Copy link
Copy Markdown
Owner

What & why

protector's own observed behavior intermittently showed wrote file /tmp/.tmp<rand>/latest_known_time.json — the tough TUF client (a sigstore-rs dep) writing trust-root freshness metadata. It churned the operator's prompt AND masqueraded as a /tmp drop-and-execute IOC.

Where the write comes from

sigstore-rs 0.14 builds its tough RepositoryLoader without calling .datastore(...), so tough falls back to Datastore::new(None)TempDir::new() = $TMPDIR/.tmp<rand>/. On every trust-root load it writes latest_known_time.json + refreshed root/timestamp/snapshot metadata there. The cache_dir we pass to SigstoreTrustRoot::new only backs the final trusted_root.json checkout — not the datastore — and sigstore-rs 0.14 exposes no API to redirect the datastore. So $TMPDIR is the only lever.

Fix (source-side — no /tmp observation is filtered)

  • Code: pin $TMPDIR to protector's TUF cache dir at single-threaded startup, before the tokio runtime spawns any worker thread (set_var is only sound single-threaded in edition 2024). An explicit operator $TMPDIR still wins. New engine/src/policies/signature/tuf_tmpdir.rs with a pure, unit-tested resolver.
  • Chart: move the TUF cache off /tmp onto a dedicated protector-owned tuf-cache emptyDir at /var/lib/protector/tuf. With the startup pin, both the checkout AND tough's datastore now land there — a stable, attributable path, never /tmp/.tmp<rand>/.
  • OnceCell resilience (§2): documented get_or_try_init semantics — it caches only a successful init, so a transient TUF blip retries on the next call but a success sticks for the process lifetime. There is no per-verify or interval TUF refresh, so steady state does zero TUF writes.

NON-GOAL respected

/tmp write observation is untouched anywhere — /tmp writes remain IOCs the agent must keep flagging. This only stops protector's OWN benign TUF plumbing from writing to /tmp.

Design note

Used a dedicated tuf-cache emptyDir (/var/lib/protector/tuf) rather than the journal PVC subdir the ticket named (/var/lib/protector/journal/sigstore) to avoid coupling the TUF cache to engine.journal.enabled; tough deletes its datastore after each load and the checkout is cheap to re-fetch, so persistence isn't needed for the fix. Repoint via values to the PVC if cross-restart TUF persistence is wanted.

Tests / checks

  • New unit tests for the $TMPDIR resolver (unset / empty / explicit-override).
  • cargo fmt, cargo clippy --all-targets (clean), cargo nextest run -p protector → 729 passed (incl. the 1,000-line file-size guard).
  • helm lint + helm template render the new volume/mount/env cleanly.

Closes JEF-377

🤖 Generated with Claude Code

https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP

sigstore-rs 0.14 builds its tough TUF client without a datastore path, so tough
falls back to TempDir::new() — a /tmp/.tmp<rand>/ dir — and writes
latest_known_time.json plus refreshed root/timestamp/snapshot metadata there on
every trust-root load. That churns the operator's prompt AND masquerades as a
drop-and-execute IOC. The cache_dir we pass to SigstoreTrustRoot::new only backs
the trusted_root.json checkout, not the datastore, and sigstore-rs exposes no API
to redirect it — so $TMPDIR is the only lever.

- Pin $TMPDIR to protector's TUF cache dir at single-threaded startup (before the
  tokio runtime spawns any worker thread; set_var is only sound single-threaded in
  edition 2024). An explicit operator $TMPDIR still wins. New tuf_tmpdir module
  with a pure, unit-tested resolver.
- Move the chart's TUF cache off /tmp onto a dedicated protector-owned tuf-cache
  emptyDir at /var/lib/protector/tuf, so both the checkout AND (via the pin)
  tough's datastore land in a stable, attributable path.
- Document the OnceCell fetch-once-and-stick semantics: get_or_try_init caches
  only success, so a transient TUF blip retries but steady state does no TUF
  writes — no per-verify or interval refresh.

NON-GOAL respected: /tmp write observation is untouched — /tmp writes remain IOCs
the agent keeps flagging; this only stops protector's OWN benign TUF plumbing.

Closes JEF-377

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP
@thejefflarson

Copy link
Copy Markdown
Owner Author

Architect review (INTEGRATE) — TUF-persistence reconciliation with JEF-326

The $TMPDIR pin is sound (pre-runtime, single-threaded set_var; best-effort no-op on create failure; /tmp write observation untouched, non-goal respected). Merging the code.

Reconciliation decision (recorded): pin_from_env derives $TMPDIR from PROTECTOR_TUF_CACHE, so it honors whatever that env var is set to. The DEPLOYED cluster-repo chart (JEF-326) keeps PROTECTOR_TUF_CACHE on the journal PVC; that chart is separate and is NOT changed here. In the deployed cluster the pin therefore routes tough's atomic temp writes onto the PVC dir too — off /tmp (JEF-377) AND persistent across restarts (JEF-326 preserved). No regression.

This source chart's dedicated tuf-cache emptyDir is a documented default that decouples the TUF cache from engine.journal.enabled and is overridden by the cluster chart's values, so it does not govern deployed behavior. Kept as-is.

Nit (not a blocker, future refinement): binding the ephemeral tough scratch dir to the persistent checkout dir ($TMPDIR == PROTECTOR_TUF_CACHE) complects two concerns; the cleaner shape is a dedicated scratch subdir independent of the persistent cache. Recorded for follow-up.

Required human follow-up (cluster repo — not touched here): ratify that the deployed cluster-repo chart keeps PROTECTOR_TUF_CACHE on the journal PVC and does NOT adopt this emptyDir, so JEF-326 persistence holds after this change syncs.

@thejefflarson
thejefflarson merged commit 62ff2a6 into main Jul 7, 2026
6 checks passed
@thejefflarson
thejefflarson deleted the thejefflarson/jef-377-reduce-tuf-write-churn-keep-sigstoretough-atomic-writes-off branch July 7, 2026 04:50
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