From f059fb466574c8b1846c26c0d61bc2092f65d132 Mon Sep 17 00:00:00 2001 From: Steve Beattie Date: Wed, 2 Sep 2026 17:40:51 -0700 Subject: [PATCH 1/3] docs: state which images CertificateAudit can assess MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rule reads each expected digest from a sidecar the image build writes, so an image with no sidecar has nothing to compare against and fails rather than skipping. That is deliberate — the sidecar-exists criteria exist so a missing one cannot pass vacuously — but it puts a floor on which images the rule can meaningfully assess, and nothing said so. Pin the floor to a version rather than the commit the doc already cited: v1.2.30 is the first apko release carrying writeCABundleChecksums; v1.2.29 does not have it. Every current image is well past that, so this matters when scanning something older — an archived release, a pinned image from before the change, or an image built by other tooling. The part worth writing down is how to tell such a failure from a real one, since the rule verdict is `fail` either way. It is distinguishable from the scan artifact rather than needing the image: with --oval-results (or an ARF), tst:4 false with tst:2 error means no usable sidecar, and tst:4 true with tst:2 false means a sidecar was read and disagreed. The error is the tell — the variable behind the comparison collected no values because there was nothing to read one from. Measured both ways against built images rather than reasoned from the criteria. The same reading applies to tst:6/tst:7 and tst:11/tst:12. That also makes the distinction available retrospectively, on a results file kept as evidence, which an in-image check cannot do. Two things went stale and are corrected while here. CertificateAuditTest.xml is no longer missing — #164 added it, and validate_checks now covers all 8 definitions — so that entry leaves Known gaps. The /kaniko branch a real image takes has changed. kaniko/ssl/certs/ca-certificates.crt was not in apko's caBundlePaths at v1.2.35 but is at v1.2.43, and the published kaniko image now ships its own sidecar where in August it did not. Confirmed by running the guard against the image: the copy matches its own sidecar and that sidecar matches obj:10's pattern, which is the first time that pattern has been checked against anything but a synthetic fixture. So production kaniko is on tst:11/tst:12 and the fallback is now what an older kaniko image would take. Both branches stay fixture-covered; nothing needs changing. Docs only. Co-Authored-By: Claude Opus 5 --- docs/certificate-audit.md | 71 +++++++++++++++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 6 deletions(-) diff --git a/docs/certificate-audit.md b/docs/certificate-audit.md index 31a5cdd..509114b 100644 --- a/docs/certificate-audit.md +++ b/docs/certificate-audit.md @@ -72,6 +72,60 @@ apko's own comment on `writeCABundleChecksums` states the purpose: "so downstream tooling (e.g. OpenSCAP) can verify they were not modified post-build." This rule is that downstream tooling. +### Which images this rule can assess + +Because the expected digest comes from the sidecar, an image that has no sidecar +has nothing to compare against — and the rule **fails** rather than skipping. +That is deliberate: `tst:4`/`tst:6`/`tst:11` exist so a missing sidecar cannot +pass vacuously, and `certificate_audit/fail_missing_stamp` pins it. + +The consequence is a floor on which images the rule can meaningfully assess: + +| image | outcome | +| --- | --- | +| built by apko **v1.2.30 or later** | assessable | +| built by apko **v1.2.29 or earlier** | fails — no sidecars exist | +| not built by apko at all | fails — no sidecars exist | + +v1.2.30 is the first release carrying `writeCABundleChecksums`; v1.2.29 does not +have it. Every current Chainguard image is well past that, so this is not a +concern for scanning what the registry serves today. It matters when scanning +something older: an archived release, a customer's pinned image from before the +change, or an image built by other tooling. + +**A failure caused by the floor is not distinguishable from a real one by the +rule verdict alone** — both are `fail`. It *is* distinguishable from the scan +artifact, in the per-test OVAL results, so no access to the image is needed and +an archived results file can be read after the fact. Scan with `--oval-results` +(or keep the ARF) and compare two tests: + +| | no usable sidecar | trust store actually drifted | +| --- | --- | --- | +| `tst:4` — sidecar exists and parses | `false` | `true` | +| `tst:2` — bundle matches the sidecar digest | `error` | `false` | + +The `error` on `tst:2` is itself the tell: the variable behind the comparison +collected no values, because there was no sidecar to read one from. A `false` +there means a sidecar was read and disagreed. + +So `tst:4 false` says the rule *could not assess* this image — it predates the +mechanism, or the sidecar is malformed — which is a different statement from +"this image's trust stores were modified". `tst:4 true` with `tst:2 false` is +the real finding. The same reading applies to `tst:6`/`tst:7` for the Java +truststore and `tst:11`/`tst:12` for a `/kaniko` copy. + +Measured, not inferred: scanning an image with its sidecar removed and its +bundle intact gives `tst:4 false`, `tst:2 error`; scanning one with the sidecar +intact and the bundle appended to gives `tst:4 true`, `tst:2 false`. Both report +the rule as `fail`. + +In-image, `ls -l /etc/ssl/certs/.ca-certificates.crt.sha256` answers the same +question more directly, where you have a shell and the image to hand. + +Note this is a change in which images are assessable, not only in how. Under the +previous design the expected digest was pinned in the datastream, so an old image +could pass if its bundle happened to match that pin — no sidecar required. + ## Why not a pinned digest The rule previously pinned the CA bundle's SHA-256 in the datastream. That had @@ -128,7 +182,16 @@ Then an `OR` for the Java truststore: to the absent-truststore branch. - **`tst:13` before falling back to `tst:9`.** A sidecar beside the `/kaniko` copy takes precedence, so a divergent copy cannot sidestep its own sidecar by - appealing to the system one. + appealing to the system one. Which branch a real image takes has + changed. `kaniko/ssl/certs/ca-certificates.crt` was **not** in apko's + `caBundlePaths` at v1.2.35 but **is** at v1.2.43, and the published kaniko + image now ships `/kaniko/ssl/certs/.ca-certificates.crt.sha256` where in + August 2026 it did not. So real kaniko images have moved off the fallback and + onto `tst:11`/`tst:12`. Confirmed by running the guard against the image: the + copy matches its own sidecar, and that sidecar matches `obj:10`'s pattern — + the first time that pattern has been checked against anything other than a + synthetic fixture. Both branches remain fixture-covered, so nothing needs + changing; the fallback is now the path an *older* kaniko image would take. - **`tst:5` uses `none_exist`** rather than testing for Java some other way, because a non-Java image must not fail for lacking a truststore. @@ -341,12 +404,8 @@ against is remote; the trade was judged not worth it for now. Revisit if the ## Known gaps - apko also stamps `var/lib/ecs/deps/execute-command/certs/tls-ca-bundle.pem` - (still in `caBundlePaths` as of apko v1.2.35). This rule neither checks that + (still in `caBundlePaths` as of apko v1.2.43). This rule neither checks that bundle nor its sidecar. -- `CertificateAuditTest.xml` is the only OVAL component referenced by the - datastream with no standalone file under - `gpos/xml/scap/ssg/content/ssg-chainguard-xccdf/OvalDefinitions/`, so - `make validate_checks` does not validate this definition. - `1` with `only_one_exists` means a sidecar containing two matching lines silently uses the first. - Deleting `/etc/ssl/certs/java/cacerts` outright satisfies the Java `OR` via From 24981fcd4bccd58c20fc0a1d1a1e8393ab46eb89 Mon Sep 17 00:00:00 2001 From: Steve Beattie Date: Wed, 9 Sep 2026 09:23:03 -0700 Subject: [PATCH 2/3] Correct the triage guidance and reconcile the /kaniko facts Review of #175 found the new triage section generalizes in a way that mis-reads ordinary images, and that the /kaniko finding contradicts a deferral rationale further down the same file. The generalization was the substantive one. "The same reading applies to tst:6/tst:7 and tst:11/tst:12" holds only where the truststore or /kaniko copy exists. On an image with neither -- the common case -- their objects collect nothing, so tst:6/tst:11 are false and tst:7/tst:12 error as a matter of course. That is byte-for-byte the "no usable sidecar" column of the table above it, appearing on an image that is fully assessable and very likely passing, since tst:5 and tst:3 carry those branches. An analyst following the old wording would report an unassessable truststore on an image with no Java in it. Now qualified, with a pointer to read tst:5/tst:3 first. The tst:4-false disjunction also omitted post-build sidecar deletion. Because tst:2's comparator is read from the sidecar (var:1 <- obj:4) rather than from the bundle, deleting it yields exactly the signature a pre-v1.2.30 image gives, so tst:4 false is not an exoneration. The image cannot settle it either: the sidecars belong to no apk package (verified -- apk info -W reports "Could not find owner package", while the bundle beside them is owned by ca-certificates-bundle), so apk audit cannot report one missing. obj:2 collects the bundle's real SHA-256 regardless and is the out-of-band disambiguator; that is now written down. Added a sensitivity note on the --oval-results guidance. Those files carry fully collected content for every definition in the profile, and textfilecontent54 items serialize the matched line verbatim -- 31 of the 91 rules this profile selects read /etc/shadow, so results from an image failing one reproduce its password field, alongside the host name, OS version and architecture. Demonstrated against a synthetic root: the canary hash appears as . The narrower `oscap oval eval --id` invocation is offered for where it will do. Not changed: the review also held that --oval-results is functional only alongside --results. Tested both ways and it is not -- on its own it writes one .result.xml per definition into the working directory, so the guidance was already correct. Said where the files land, since that was the useful part. The /kaniko reconciliation: the deferral rationale named "gains a sidecar of its own" as its own revisit trigger, and this PR's evidence fires it verbatim while concluding nothing needs changing. Both halves are now stated -- the criteria genuinely need no change, but the guard coverage does shift, because production now resolves through obj:10 and obj:10 is the one sidecar tests/stamps/run.sh marks required=no. That setting stays correct (older kaniko images legitimately ship none and fall back to tst:13/tst:9); the consequence is that "present but wrong" is guarded there while "absent" is not. Recorded under Known gaps rather than left implicit. Two stale comments outside docs/, both comment-only. fixtures_test.go named cgr.dev/chainguard/kaniko as shipping no sidecar; that ref does not resolve at all -- the only kaniko image published is cgr.dev/chainguard-private/kaniko, and it does ship one. run.sh's required=no rationale argued from facts that have since changed. The doc said "the published kaniko image" for the same reason and now names the private ref. The apko version claims are about a moving external dependency with nothing in-repo pinning them, as the review noted. All three re-derived against apko: writeCABundleChecksums is absent at v1.2.29 and present at v1.2.30; kaniko/ssl/certs/ca-certificates.crt enters caBundlePaths between v1.2.35 and v1.2.43; the ECS bundle is still there at v1.2.43. Rather than pin a third party's source in CI, the doc now says where to look so a reader can re-check instead of trusting this PR. Co-Authored-By: Claude Opus 5 (1M context) --- docs/certificate-audit.md | 97 +++++++++++++++---- .../internal/scan/fixtures_test.go | 7 +- tests/stamps/run.sh | 15 ++- 3 files changed, 97 insertions(+), 22 deletions(-) diff --git a/docs/certificate-audit.md b/docs/certificate-audit.md index 509114b..5f12f83 100644 --- a/docs/certificate-audit.md +++ b/docs/certificate-audit.md @@ -93,6 +93,11 @@ concern for scanning what the registry serves today. It matters when scanning something older: an archived release, a customer's pinned image from before the change, or an image built by other tooling. +Nothing in this repository pins any apko version, so the claims here and below +are about a moving external dependency. They live in apko's +`pkg/build/certificates.go` — `writeCABundleChecksums` and the `caBundlePaths` +list — and can be re-derived at any tag rather than taken on trust. + **A failure caused by the floor is not distinguishable from a real one by the rule verdict alone** — both are `fail`. It *is* distinguishable from the scan artifact, in the per-test OVAL results, so no access to the image is needed and @@ -104,15 +109,49 @@ an archived results file can be read after the fact. Scan with `--oval-results` | `tst:4` — sidecar exists and parses | `false` | `true` | | `tst:2` — bundle matches the sidecar digest | `error` | `false` | +`--oval-results` needs no companion flag and writes one file per OVAL document +into the working directory, named after it; `CertificateAuditTest.xml.result.xml` +is the one to read. + +**Treat a retained OVAL results file as credential-bearing.** It carries fully +collected content for *every* definition in the profile, not only this one, and +`textfilecontent54` items serialize the matched line verbatim. This profile +co-selects the `/etc/shadow` rules, whose patterns match the password field, so +the results from an image failing one of those reproduce that field — along with +the scanned system's host name, OS version and architecture. Where the narrower +output will do, scope the run to this definition instead: + + oscap oval eval --id oval:org.CABundleHash:def:1 \ + --results cert-audit.xml + The `error` on `tst:2` is itself the tell: the variable behind the comparison collected no values, because there was no sidecar to read one from. A `false` there means a sidecar was read and disagreed. So `tst:4 false` says the rule *could not assess* this image — it predates the -mechanism, or the sidecar is malformed — which is a different statement from -"this image's trust stores were modified". `tst:4 true` with `tst:2 false` is -the real finding. The same reading applies to `tst:6`/`tst:7` for the Java -truststore and `tst:11`/`tst:12` for a `/kaniko` copy. +mechanism, the sidecar is malformed, or the sidecar was **removed after the +build**, whether by an image-slimming step or deliberately. That is a different +statement from "this image's trust stores were modified", but it is not an +exoneration either. Because `tst:2`'s comparator is read from the sidecar +(`var:1` ← `obj:4`) rather than from the bundle, a deleted sidecar produces the +same `tst:4 false` / `tst:2 error` signature as a pre-v1.2.30 image, and the +image itself will not settle which happened: the sidecars belong to no apk +package (`apk info -W` reports `Could not find owner package`, where the bundle +beside them is owned by `ca-certificates-bundle`), so `apk audit` cannot report +one as missing. What does help is `obj:2`, which collects the bundle's real +SHA-256 whatever `tst:2` does — compare it out-of-band against the digest from a +signed build. + +`tst:4 true` with `tst:2 false` is the real finding. + +The same reading applies to `tst:6`/`tst:7` for the Java truststore and +`tst:11`/`tst:12` for a `/kaniko` copy — **but only where that truststore or +copy is actually present.** On an image with neither, `tst:6`/`tst:11` are +`false` and `tst:7`/`tst:12` `error` as a matter of course, because their +objects collect nothing: that is the "no usable sidecar" column above appearing +on a fully assessable image, and on a *passing* one. `tst:5` and `tst:3` are +what carry those branches in that case, so read them first — an absent +truststore or `/kaniko` copy is not a finding. Measured, not inferred: scanning an image with its sidecar removed and its bundle intact gives `tst:4 false`, `tst:2 error`; scanning one with the sidecar @@ -184,14 +223,18 @@ Then an `OR` for the Java truststore: copy takes precedence, so a divergent copy cannot sidestep its own sidecar by appealing to the system one. Which branch a real image takes has changed. `kaniko/ssl/certs/ca-certificates.crt` was **not** in apko's - `caBundlePaths` at v1.2.35 but **is** at v1.2.43, and the published kaniko - image now ships `/kaniko/ssl/certs/.ca-certificates.crt.sha256` where in - August 2026 it did not. So real kaniko images have moved off the fallback and - onto `tst:11`/`tst:12`. Confirmed by running the guard against the image: the - copy matches its own sidecar, and that sidecar matches `obj:10`'s pattern — - the first time that pattern has been checked against anything other than a - synthetic fixture. Both branches remain fixture-covered, so nothing needs - changing; the fallback is now the path an *older* kaniko image would take. + `caBundlePaths` at v1.2.35 but **is** at v1.2.43, and + `cgr.dev/chainguard-private/kaniko` now ships + `/kaniko/ssl/certs/.ca-certificates.crt.sha256` where in August 2026 it did + not. That private ref is the only kaniko image there is; no public + `chainguard/kaniko` exists to confuse it with. So real kaniko images have + moved off the fallback and onto `tst:11`/`tst:12`. Confirmed by running the guard + against it: the copy matches its own sidecar, and that sidecar matches + `obj:10`'s pattern — the first time that pattern has been checked against + anything other than a synthetic fixture. Both branches remain + fixture-covered, so the criteria need no change; the fallback is now the path + an *older* kaniko image would take. What this does change is which branch the + guards cover — see [Known gaps](#known-gaps). - **`tst:5` uses `none_exist`** rather than testing for Java some other way, because a non-Java image must not fail for lacking a truststore. @@ -396,10 +439,21 @@ This is deliberately not automated. Doing so would need two additions to the workflow's trust surface, not one: a credential for the private registry, and a second accepted signer identity, because that image is signed by `chainguard-dev/stereo/.github/workflows/release-containers.yaml` rather than -the `chainguard-images/images/*` identity the workflow requires. The copy is -currently byte-identical to its system bundle, so the drift being guarded -against is remote; the trade was judged not worth it for now. Revisit if the -`/kaniko` copy ever starts diverging, or gains a sidecar of its own. +the `chainguard-images/images/*` identity the workflow requires. + +That trade was originally made on the grounds that the copy was byte-identical +to its system bundle and carried no sidecar of its own, making the drift being +guarded against remote — and it named "gains a sidecar of its own" as the thing +that should prompt a revisit. That has now happened (see +[Why each guard exists](#why-each-guard-exists)), so the original rationale no +longer applies as written. + +The trade still looks right, but for a narrower reason: the two additions to the +trust surface are unchanged, while the copy having its own sidecar means a +divergent copy is now caught by `tst:11`/`tst:12` against that sidecar rather +than needing to be caught by the fallback. What the sidecar's arrival does cost +is guard coverage rather than criteria coverage, which is recorded under +[Known gaps](#known-gaps). ## Known gaps @@ -411,6 +465,15 @@ against is remote; the trade was judged not worth it for now. Revisit if the - Deleting `/etc/ssl/certs/java/cacerts` outright satisfies the Java `OR` via `tst:5`. - No automated run guards the `/kaniko` criteria against a real image; the - daily workflow inspects only public images. It is coverable on demand — see + daily workflow stamps only `cgr.dev/chainguard/jre:latest`, and the `/kaniko` + copy lives solely on a private ref. It is coverable on demand — see [Covering the /kaniko criteria](#covering-the-kaniko-criteria) — and deferred rather than declined. +- That gap now falls on the branch production images actually take. Since the + `/kaniko` copy gained its own sidecar, real images resolve through + `tst:11`/`tst:12` over `obj:10` — and `obj:10` is the one sidecar + `tests/stamps/run.sh` marks `required=no`, so a run that does reach a kaniko + image still will not fail on the sidecar going missing. `required=no` remains + correct, since older kaniko images legitimately ship no sidecar and the + criteria fall back to `tst:13`/`tst:9` for them; the consequence is that + "present but wrong" is guarded there while "absent" is not. diff --git a/tests/oscap-offline/internal/scan/fixtures_test.go b/tests/oscap-offline/internal/scan/fixtures_test.go index b42bcc5..cc4fde5 100644 --- a/tests/oscap-offline/internal/scan/fixtures_test.go +++ b/tests/oscap-offline/internal/scan/fixtures_test.go @@ -339,8 +339,11 @@ const caStampPath = "etc/ssl/certs/.ca-certificates.crt.sha256" // caBundlePath and kanikoCABundlePath are the two locations CertificateAudit // accepts SSL_CERT_FILE pointing at, as tar member paths (rootfs-relative, no // leading slash). kanikoCAStampPath is the stamp file the /kaniko copy is -// checked against where one is shipped beside it; cgr.dev/chainguard/kaniko -// ships none today, so the copy falls back to the stamp at caStampPath. +// checked against where one is shipped beside it. The only kaniko image +// published is cgr.dev/chainguard-private/kaniko (there is no public one), and +// it does ship that sidecar as of apko v1.2.43, so real images now resolve +// through tst:11/tst:12; the fallback to caStampPath is what an older kaniko +// image takes. Both branches are covered by fixtures below. const ( caBundlePath = "etc/ssl/certs/ca-certificates.crt" kanikoCABundlePath = "kaniko/ssl/certs/ca-certificates.crt" diff --git a/tests/stamps/run.sh b/tests/stamps/run.sh index 34e3647..f5883c0 100755 --- a/tests/stamps/run.sh +++ b/tests/stamps/run.sh @@ -52,9 +52,18 @@ DEFAULT_IMAGES="${STAMP_IMAGES:-cgr.dev/chainguard/jre:latest}" # sidecar (tst:13 + tst:9), so only a present-but-wrong sidecar # is a failure. # -# /kaniko is carried only by cgr.dev/chainguard-private/kaniko, so it is not -# reached by the default public image. Pass that ref explicitly to cover it -# wherever credentials for it exist; a run reports what it did not reach. +# obj:10 stays required=no because older kaniko images legitimately ship no +# sidecar beside the /kaniko copy and the criteria fall back for them. Note this +# is no longer the branch a current image takes: the copy entered apko's +# caBundlePaths by v1.2.43 and now carries its own sidecar, so production +# resolves through tst:11 + tst:12. The consequence of required=no is therefore +# that a sidecar going *missing* from an image that should have one will not +# fail this guard, only one that is present and wrong. +# +# /kaniko is carried only by cgr.dev/chainguard-private/kaniko — there is no +# public kaniko image — so it is not reached by the default public image. Pass +# that ref explicitly to cover it wherever credentials for it exist; a run +# reports what it did not reach. SIDECARS=( "oval:org.CABundleHash:obj:4|etc/ssl/certs|.ca-certificates.crt.sha256|ca-certificates.crt|yes" "oval:org.CABundleHash:obj:6|etc/ssl/certs/java|.cacerts.sha256|cacerts|yes" From 91c782258f610b696495d174ad5483a1fa4da4db Mon Sep 17 00:00:00 2001 From: Steve Beattie Date: Wed, 9 Sep 2026 20:57:14 -0700 Subject: [PATCH 3/3] Verify the /kaniko claims against the image, and fix the auth diagnostic The /kaniko facts in this PR rested on a guard run that could not be reproduced during review without credentials for the private registry. Re-authed and checked them directly against cgr.dev/chainguard-private/kaniko@sha256:f57d9900: kaniko/ssl/certs/.ca-certificates.crt.sha256 shipped, 86 bytes copy vs its own sidecar matches sidecar vs obj:10's pattern matches, exactly one line copy vs the system bundle still byte-identical tests/stamps/run.sh against jre plus that ref exits 0 and reports the obj:10 pattern satisfied, which is the first time that pattern has been checked against a real image rather than a fixture. The useful part is the fourth line. The deferral rationale rested on two grounds -- byte-identical copy, no sidecar of its own -- and only the second has changed, so the revisit trigger has fired while the divergence the guard exists to catch still has not occurred. The previous wording left the byte-identity half ambiguous by reasoning about it rather than checking; it is now a table of measurements. Also corrects the auth diagnostic. `chainctl auth status` without --audience reports on the console-API token, which is valid whether or not the cgr.dev login has happened -- so it reads as success before the re-auth and, misleadingly, still shows the console-API audience after one. `chainctl auth status --audience cgr.dev` is the check that answers the question, with the caveat that it is not a pure read: if that token has expired, asking for its status re-requests one. Co-Authored-By: Claude Opus 5 (1M context) --- docs/certificate-audit.md | 44 +++++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/docs/certificate-audit.md b/docs/certificate-audit.md index 5f12f83..a2e3b32 100644 --- a/docs/certificate-audit.md +++ b/docs/certificate-audit.md @@ -426,6 +426,16 @@ still fails with `No matching credentials were found for "cgr.dev"`. The credential helper itself is usually already wired up (`chainctl auth configure-docker` will say so); the audience is the part that goes missing. +To check the audience you actually need, ask for it by name: + + chainctl auth status --audience cgr.dev + +Without `--audience` the command reports on the console-API token, which is the +one that is valid whether or not the `cgr.dev` login has happened — so a bare +`chainctl auth status` is the reason this failure is easy to misdiagnose in both +directions. Note the flag is not a pure read: if the `cgr.dev` token has +expired, asking for its status re-requests one. + tests/stamps/run.sh cgr.dev/chainguard/jre:latest \ cgr.dev/chainguard-private/kaniko:latest @@ -441,19 +451,27 @@ second accepted signer identity, because that image is signed by `chainguard-dev/stereo/.github/workflows/release-containers.yaml` rather than the `chainguard-images/images/*` identity the workflow requires. -That trade was originally made on the grounds that the copy was byte-identical -to its system bundle and carried no sidecar of its own, making the drift being -guarded against remote — and it named "gains a sidecar of its own" as the thing -that should prompt a revisit. That has now happened (see -[Why each guard exists](#why-each-guard-exists)), so the original rationale no -longer applies as written. - -The trade still looks right, but for a narrower reason: the two additions to the -trust surface are unchanged, while the copy having its own sidecar means a -divergent copy is now caught by `tst:11`/`tst:12` against that sidecar rather -than needing to be caught by the fallback. What the sidecar's arrival does cost -is guard coverage rather than criteria coverage, which is recorded under -[Known gaps](#known-gaps). +That trade was originally made on two grounds — the copy was byte-identical to +its system bundle, and it carried no sidecar of its own — and it named "gains a +sidecar of its own" as the thing that should prompt a revisit. Exactly one of +those has changed. Checked against the image directly: + +| | | +|---|---| +| `kaniko/ssl/certs/.ca-certificates.crt.sha256` | now shipped (86 bytes) | +| `kaniko/ssl/certs/ca-certificates.crt` vs the system bundle | still byte-identical (`b8d83784…`) | +| the copy vs its own sidecar | matches | +| that sidecar vs `obj:10`'s pattern | matches, exactly one line | + +So the revisit trigger has fired, but the divergence the guard exists to catch +still has not occurred. + +The trade still looks right, for a narrower reason than before: the two +additions to the trust surface are unchanged, and the copy having its own +sidecar means a divergent copy would now be caught by `tst:11`/`tst:12` against +that sidecar rather than having to be caught by the fallback. What the sidecar's +arrival does cost is guard coverage rather than criteria coverage, which is +recorded under [Known gaps](#known-gaps). ## Known gaps