Minimize sensitive content in evidence bundles while preserving the verifiable signal
Summary
An evidence bundle today ships the full backing payloads it attests to — the
validate-time snapshot (snapshot.yaml), the resolved recipe
(recipe.yaml), and per-phase CTRF reports (ctrf/<phase>.json) including
captured pod stdout. As snapshot collectors grow (more node/OS/topology detail)
and CTRF accumulates more log lines, the bundle increasingly carries
operational detail that is sensitive to publish — node names, cloud provider
instance IDs, the full node label/taint set, kernel/sysctl tuning, and raw
container stdout that can leak internal endpoints, hostnames, and identifiers.
The goal of this issue is to reduce what the bundle physically ships to the
minimum needed for a strong, verifiable conformance signal — without weakening
the cryptographic binding that makes the bundle trustworthy.
Key observation: the verifiable signal is already digest-based
The trust signal does not come from the raw payloads — it comes from the
signed in-toto predicate, which commits to every artifact by hash
(pkg/evidence/attestation/types.go):
Predicate.Recipe.Digest = sha256(canonicalize(recipe.yaml))
Predicate.Phases[phase].CTRFDigest = sha256(ctrf/<phase>.json)
Predicate.BOM.Digest, Predicate.BOM.ImageCount
Predicate.Manifest.Digest → manifest.json, which carries a per-file
sha256 for every file in the bundle, transitively binding all of them to
the signature
- Per-phase
Passed/Failed/Skipped counts, Fingerprint, CriteriaMatch,
validator image digests, catalog version
So a verifier can confirm "recipe X, validated against a cluster matching
criteria Y, passed/failed N tests per phase, signed by identity Z, logged in
Rekor" entirely from the predicate — the bundled snapshot.yaml and CTRF
stdout are the backing content a digest points at, not the signal itself.
That gives us room to shrink or sanitize the backing content while keeping the
binding intact.
This is consistent with the direction already taken in ADR-007
(docs/design/007-recipe-evidence.md), which split large/sensitive logs
into a separate opt-in artifact precisely because they are "large,
contributor-controlled ... and frequently sensitive (cluster identifiers,
internal endpoints)." This issue extends that same reasoning to the snapshot
and CTRF payloads that still ship in the summary bundle by default.
What's currently bundled and where the sensitivity lives
Snapshot (snapshot.yaml) — emitted in full. Sensitive fields include:
pkg/collector/k8s/node.go: source-node (node name) and provider-id
(e.g. aws:///us-west-2a/i-0123456789abcdef0)
pkg/collector/topology: the complete node label and taint set across the
cluster — custom labels routinely encode account IDs, cost centers,
environment/tenant names, regions
pkg/collector/os: GRUB/kernel boot args, full sysctl tree, loaded modules —
reveals hardening/tuning posture
pkg/collector/gpu, systemd: driver/CUDA versions, service config
CTRF reports (ctrf/<phase>.json) — pkg/validator/ctrf/types.go:
TestResult.Stdout (captured pod stdout, capped by line count/length) and
TestResult.Message (termination-log text) — free-form log text that can
contain IPs, DNS names, cert/secret names, internal URLs
Recipe (recipe.yaml) — generally the least sensitive (criteria, component
refs, version pins, constraints), but worth confirming nothing operator-specific
leaks via overlay resolution / matchedRules.
Goal
Define and implement a "minimal evidence" bundle that:
- Preserves the full cryptographic verification story (predicate digests,
manifest binding, DSSE signature, Rekor).
- Ships only the data an external auditor needs to trust the conformance claim.
- Keeps potentially sensitive operational detail out of the published bundle by
default, with an explicit opt-in for operators who want to publish more.
Possible approaches (to be discussed — not yet decided)
These are mutually compatible building blocks, not exclusive options:
- A. Field-level snapshot redaction/allowlist. Emit a sanitized snapshot
that drops/obfuscates known-sensitive fields (node names, provider IDs, the
raw label/taint set) while retaining what constraints actually evaluated
against. Risk: must keep the snapshot digest meaningful — decide whether the
predicate attests to the sanitized snapshot or to the raw one (and keep the
raw one local). An allowlist (fail-closed) is safer than a denylist as
collectors grow.
- B. CTRF stdout/message scrubbing or omission by default. Strip or redact
Stdout/Message from published CTRF, or replace full logs with a digest +
summary, keeping raw logs in the existing opt-in local/logs path. The
pass/fail counts and per-test status — the actual signal — stay.
- C. Digest-only backing content. Replace bundled payloads with their
digests in the predicate and ship the raw payloads only when the operator
opts in (mirrors the ADR-007 logs split). Verifiers still get the binding;
publication of the bytes becomes a choice.
- D. Built-in redaction transforms with a documented allowlist. A small,
audited set of regex/structured redactors (cf. the existing ELB-hostname
redaction in pkg/evidence/cncf/scripts/collect-evidence.sh:2263) applied
consistently, with the redaction policy itself recorded in the predicate so
verifiers know what was scrubbed.
Open questions
- Should the predicate attest to the sanitized artifact (so the published
bundle self-verifies) or to the raw artifact (so the local full bundle
remains the canonical signed thing)? This is the central design decision.
- Do downstream consumers (CNCF conformance submission, recipe-health) depend on
any field we'd want to redact? Inventory those consumers first.
- Default posture: minimal-by-default with
--include-snapshot/--include-logs
opt-in, vs. full-by-default with --minimal/--redact?
- How do we make redaction auditable — record the redaction policy version in
the predicate so a verifier can tell what was removed?
Acceptance criteria (draft)
References
pkg/evidence/attestation/types.go — Predicate, *Ref digest fields,
bundle file constants
pkg/evidence/attestation/manifest.go — per-file sha256 integrity inventory
pkg/collector/{k8s,os,gpu,systemd,topology} — snapshot data sources
pkg/validator/ctrf/types.go, pkg/validator/job/result.go — CTRF + stdout
capture
docs/design/007-recipe-evidence.md — summary/logs split rationale (logs
already treated as sensitive/opt-in)
Minimize sensitive content in evidence bundles while preserving the verifiable signal
Summary
An evidence bundle today ships the full backing payloads it attests to — the
validate-time snapshot (
snapshot.yaml), the resolved recipe(
recipe.yaml), and per-phase CTRF reports (ctrf/<phase>.json) includingcaptured pod stdout. As snapshot collectors grow (more node/OS/topology detail)
and CTRF accumulates more log lines, the bundle increasingly carries
operational detail that is sensitive to publish — node names, cloud provider
instance IDs, the full node label/taint set, kernel/sysctl tuning, and raw
container stdout that can leak internal endpoints, hostnames, and identifiers.
The goal of this issue is to reduce what the bundle physically ships to the
minimum needed for a strong, verifiable conformance signal — without weakening
the cryptographic binding that makes the bundle trustworthy.
Key observation: the verifiable signal is already digest-based
The trust signal does not come from the raw payloads — it comes from the
signed in-toto predicate, which commits to every artifact by hash
(
pkg/evidence/attestation/types.go):Predicate.Recipe.Digest=sha256(canonicalize(recipe.yaml))Predicate.Phases[phase].CTRFDigest=sha256(ctrf/<phase>.json)Predicate.BOM.Digest,Predicate.BOM.ImageCountPredicate.Manifest.Digest→manifest.json, which carries a per-filesha256for every file in the bundle, transitively binding all of them tothe signature
Passed/Failed/Skippedcounts,Fingerprint,CriteriaMatch,validator image digests, catalog version
So a verifier can confirm "recipe X, validated against a cluster matching
criteria Y, passed/failed N tests per phase, signed by identity Z, logged in
Rekor" entirely from the predicate — the bundled
snapshot.yamland CTRFstdout are the backing content a digest points at, not the signal itself.
That gives us room to shrink or sanitize the backing content while keeping the
binding intact.
This is consistent with the direction already taken in ADR-007
(
docs/design/007-recipe-evidence.md), which split large/sensitive logsinto a separate opt-in artifact precisely because they are "large,
contributor-controlled ... and frequently sensitive (cluster identifiers,
internal endpoints)." This issue extends that same reasoning to the snapshot
and CTRF payloads that still ship in the summary bundle by default.
What's currently bundled and where the sensitivity lives
Snapshot (
snapshot.yaml) — emitted in full. Sensitive fields include:pkg/collector/k8s/node.go:source-node(node name) andprovider-id(e.g.
aws:///us-west-2a/i-0123456789abcdef0)pkg/collector/topology: the complete node label and taint set across thecluster — custom labels routinely encode account IDs, cost centers,
environment/tenant names, regions
pkg/collector/os: GRUB/kernel boot args, full sysctl tree, loaded modules —reveals hardening/tuning posture
pkg/collector/gpu,systemd: driver/CUDA versions, service configCTRF reports (
ctrf/<phase>.json) —pkg/validator/ctrf/types.go:TestResult.Stdout(captured pod stdout, capped by line count/length) andTestResult.Message(termination-log text) — free-form log text that cancontain IPs, DNS names, cert/secret names, internal URLs
Recipe (
recipe.yaml) — generally the least sensitive (criteria, componentrefs, version pins, constraints), but worth confirming nothing operator-specific
leaks via overlay resolution /
matchedRules.Goal
Define and implement a "minimal evidence" bundle that:
manifest binding, DSSE signature, Rekor).
default, with an explicit opt-in for operators who want to publish more.
Possible approaches (to be discussed — not yet decided)
These are mutually compatible building blocks, not exclusive options:
that drops/obfuscates known-sensitive fields (node names, provider IDs, the
raw label/taint set) while retaining what constraints actually evaluated
against. Risk: must keep the snapshot digest meaningful — decide whether the
predicate attests to the sanitized snapshot or to the raw one (and keep the
raw one local). An allowlist (fail-closed) is safer than a denylist as
collectors grow.
Stdout/Messagefrom published CTRF, or replace full logs with a digest +summary, keeping raw logs in the existing opt-in local/logs path. The
pass/fail counts and per-test status — the actual signal — stay.
digests in the predicate and ship the raw payloads only when the operator
opts in (mirrors the ADR-007 logs split). Verifiers still get the binding;
publication of the bytes becomes a choice.
audited set of regex/structured redactors (cf. the existing ELB-hostname
redaction in
pkg/evidence/cncf/scripts/collect-evidence.sh:2263) appliedconsistently, with the redaction policy itself recorded in the predicate so
verifiers know what was scrubbed.
Open questions
bundle self-verifies) or to the raw artifact (so the local full bundle
remains the canonical signed thing)? This is the central design decision.
any field we'd want to redact? Inventory those consumers first.
--include-snapshot/--include-logsopt-in, vs. full-by-default with
--minimal/--redact?the predicate so a verifier can tell what was removed?
Acceptance criteria (draft)
the published bundle by default (or behind an explicit minimal mode).
aicr evidence verifystill passes on the minimal bundle: predicatedigests, manifest binding, and signature all validate.
so verification stays reproducible.
docs/design/007-recipe-evidence.md, CLI/evidence docs) andADR amended for the new default posture.
References
pkg/evidence/attestation/types.go—Predicate,*Refdigest fields,bundle file constants
pkg/evidence/attestation/manifest.go— per-file sha256 integrity inventorypkg/collector/{k8s,os,gpu,systemd,topology}— snapshot data sourcespkg/validator/ctrf/types.go,pkg/validator/job/result.go— CTRF + stdoutcapture
docs/design/007-recipe-evidence.md— summary/logs split rationale (logsalready treated as sensitive/opt-in)