On this page
Verification policy
Every container image and Helm chart in the CommonLayer registry was submitted by its project's CI as a digest, fetched from the source registry, and checked against the verification policy before it was copied and promoted. This page lists the checks, how a check can be waived, how projects can tighten the policy, and what the result does and does not say.
The policy and each check's result are public: GET /api/v1/verification-policy returns
the platform default, and GET /api/v1/digests/{digest} returns the checks recorded for a
digest. The implementation is crates/commonlayer-artifacts/src/domain/policy.rs; every
check has a passing and a failing test (policy_tests.rs).
Results
Each check yields pass, fail, waived (by a recorded exception) or
not_applicable (for example chart checks on an image). Promotion requires that no
check fails. A rejected submission keeps its digest record in state rejected with the
failed checks, so the public digest page explains why. For a failed scan gate the public
page states only how many findings blocked it, never their ids, because a finding may be
embargoed (security review MEDIUM-7).
Resuming a failed submission (re-submitting the same digest) runs the scan, the evaluation, mirroring and promotion again; an old evaluation is never reused (MEDIUM-2).
Checks
| Check | Requirement | Waivable |
|---|---|---|
source.matches_manifest | The source registry and repository equal registry.source in the project's commonlayer.yaml | never |
digest.verified | The fetched content hashes to the submitted digest | never |
version.declared | The version belongs to a supported line in the manifest, and the revision is new | never |
platforms.complete | Every required platform is present (default linux/amd64 and linux/arm64) | by platform waiver |
image.nonroot | The image config user is a non-zero UID | by manifest deviation non-root or platform waiver |
image.labels | The required OCI and org.commonlayer.* labels are present and consistent with the submission | by manifest deviation labels or platform waiver |
signature.valid | At least one cosign signature verifies | never |
signature.identity | The signer's identity and issuer match the identity registered in the manifest (keyless) or a registered public key | never |
sbom.present | A signed SBOM attestation in an accepted format exists for every platform | by platform waiver |
sbom.parsable | The SBOM parses and lists components | by platform waiver |
provenance.present | A signed SLSA provenance attestation exists | by platform waiver |
provenance.subject | The provenance subject digest equals the artifact digest | by platform waiver |
provenance.source | The provenance source repository is a verified repository of the project, and the commit matches the submission | by platform waiver |
scan.fresh | A successful scan by at least one scanner completed for this digest | by platform waiver |
scan.gate | No finding at or above the gate severity (default critical) that has a fix available, unless covered by an approved exception or a not_affected statement | exceptions and VEX come from the security domain |
chart.images_pinned | A chart's default images are digests of promoted CommonLayer images | by platform waiver |
chart.schema | A chart ships values.schema.json | by platform waiver |
Charts are not image-scanned: scan.fresh and scan.gate are not_applicable for a chart,
whose images must already be promoted, scanned CommonLayer digests (chart.images_pinned).
What has been exercised. All 17 checks have unit tests with a passing and a failing
case. The publish drill (WP-70g, locally, with real cosign, Trivy, Grype and a real
registry) produced 15 passing image checks and 10 passing chart checks for correctly built
artifacts signed with a key, and a rejection listing 8 failed checks for an unsigned image.
signature.identity for keyless signatures, and provenance.* for provenance made by
GitHub's attestation action, have not been exercised outside unit tests.
Required labels by default: org.opencontainers.image.{title, description, version, revision, created, source, url, documentation, licenses, vendor, base.name, base.digest}
and org.commonlayer.{project, variant, upstream.version, revision, standard}
(DEFAULT_REQUIRED_LABELS).
Waivers
A failing check becomes waived only through one of two recorded exceptions
(ADR-0028):
- A manifest deviation for the image standards
non-root(checkimage.nonroot) andlabels(checkimage.labels). Deviations are declared incommonlayer.yamlwith a reason and shown on the artifact page. - A platform waiver in the platform policy, recorded by a holder of
platform.settings.managethroughPUT /api/v1/verification-policy. A waiver names the check, the project, optionally the artifact, a reason of at least 20 characters, who recorded it and when, and an optional expiry. Publishing a new platform policy version is the only way to record a waiver; it is audited.
The checks that establish what an artifact is and who built it (source.*,
digest.verified, version.declared, signature.*) can never be waived.
Project policies
A project admin (project.settings.manage) can set a project policy
(PUT /api/v1/projects/{slug}/verification-policy) that only tightens the platform
default: a stricter scan gate, more required platforms or labels, fewer accepted SBOM
formats. A project policy cannot carry waivers and cannot switch provenance off.
What the policy does not claim
- A valid keyless signature proves which workflow, repository and ref produced the digest. It says nothing by itself about whether the content is secure.
- A passing scan means the configured scanners found nothing at or above the gate with a
fix available, at the time of the scan, with their databases of that day. Scanners only
see what the SBOM lists. Projects declare the software they build from source (nginx,
Valkey, PostgreSQL, darkhttpd) in the SBOM with CPEs; Grype matches those
declarations, Trivy does not, so for that software the result rests on one scanner
(ADR-0039).
CommonLayer's own helpers (
cl-probe,cl-kube-label) are listed but have no vulnerability feed, and libraries vendored into a binary surface only as that binary's vulnerabilities. Scans are repeated daily for supported digests and SBOMs are re-matched against OSV every six hours, so findings can appear after promotion. - A key-signed artifact's signature proves that the holder of the registered key signed it, not which build produced it; the provenance attestation is the evidence for that.
- CommonLayer does not claim a SLSA level for any artifact and does not call an image "hardened" because a scan passed. It states what it verified.
Snapshot and instructions
Each verification stores the effective policy (platform plus project) and the signing
identity in force. GET /api/v1/digests/{digest}/verify-instructions builds the exact
commands a consumer can run from that snapshot; see
verifying artifacts.
Configuration that affects verification
| Setting | Effect |
|---|---|
registry.cosign_path | Path of the cosign binary. Without it, submissions stop with artifacts.verifier_not_configured; verification is never skipped |
registry.cosign_require_transparency_log | Require a Rekor entry (must be true in production; false only in development and test, as in the publish drill) |
registry.allowed_source_registries | Registries a submission may name (default ghcr.io and the local source registry) |
security.trivy_path, security.grype_path, security.osv_enabled | Which scanners run; at least one must succeed for scan.fresh |
security.gate | Default gate severity for the scan gate in the platform's security domain |
security.max_scan_age_hours (runtime setting, default 48) | How old a successful scan may be and still count as fresh |
registry.scan_timeout_secs | How long the pipeline waits for a scan (default 2 h); a timeout ends the submission as failed (resumable), not rejected |