fix(signature): keep the TUF/tough atomic temp writes off /tmp (JEF-377)#196
Conversation
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
|
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. |
…-keep-sigstoretough-atomic-writes-off
What & why
protector's own observed behavior intermittently showed
wrote file /tmp/.tmp<rand>/latest_known_time.json— thetoughTUF client (a sigstore-rs dep) writing trust-root freshness metadata. It churned the operator's prompt AND masqueraded as a/tmpdrop-and-execute IOC.Where the write comes from
sigstore-rs 0.14 builds its tough
RepositoryLoaderwithout calling.datastore(...), so tough falls back toDatastore::new(None)→TempDir::new()=$TMPDIR/.tmp<rand>/. On every trust-root load it writeslatest_known_time.json+ refreshed root/timestamp/snapshot metadata there. Thecache_dirwe pass toSigstoreTrustRoot::newonly backs the finaltrusted_root.jsoncheckout — not the datastore — and sigstore-rs 0.14 exposes no API to redirect the datastore. So$TMPDIRis the only lever.Fix (source-side — no /tmp observation is filtered)
$TMPDIRto protector's TUF cache dir at single-threaded startup, before the tokio runtime spawns any worker thread (set_varis only sound single-threaded in edition 2024). An explicit operator$TMPDIRstill wins. Newengine/src/policies/signature/tuf_tmpdir.rswith a pure, unit-tested resolver./tmponto a dedicated protector-ownedtuf-cacheemptyDir 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>/.get_or_try_initsemantics — 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
/tmpwrite observation is untouched anywhere —/tmpwrites 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-cacheemptyDir (/var/lib/protector/tuf) rather than the journal PVC subdir the ticket named (/var/lib/protector/journal/sigstore) to avoid coupling the TUF cache toengine.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
$TMPDIRresolver (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 templaterender the new volume/mount/env cleanly.Closes JEF-377
🤖 Generated with Claude Code
https://claude.ai/code/session_01VtjoJttCvBY4dzCoE4f9vP