From f059fb466574c8b1846c26c0d61bc2092f65d132 Mon Sep 17 00:00:00 2001 From: Steve Beattie Date: Wed, 2 Sep 2026 17:40:51 -0700 Subject: [PATCH] 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