Sign and verify a container image
Cilock and cosign are complementary tools for container provenance:
- Cosign signs the image itself, proves "this image manifest was signed by this identity."
- Cilock signs evidence about what built the image, proves "this image came from this commit, this build command, with these inputs and this SBOM."
This tutorial wires both together so a verifier can answer both "is this image authentic?" (cosign) and "is this image trustworthy?" (cilock).
What you'll buildโ
Two signing tools, two pieces of evidence, both verifiable independently.
Step 1: Build with attestation evidenceโ
The build step uses cilock-action with the docker and oci attestors so the resulting attestation includes both the buildx metadata and the OCI image layer digests:
- name: build container with attestation
uses: aflock-ai/cilock-action@v1.0.1
with:
step: docker-build
command: |
bash -c "
docker buildx build \
--metadata-file docker-metadata.json \
-t ghcr.io/${{ github.repository }}:${{ github.sha }} \
-o type=docker,dest=image.tar . &&
syft image.tar -o cyclonedx-json=image-bom.cdx.json
"
attestations: environment git github docker oci sbom
cilock-args: --attestor-product-include-glob "{docker-metadata.json,image-bom.cdx.json,image.tar}"
The buildx output is type=docker (a docker save-format tarball with manifest.json at the root), not type=oci (an OCI image-layout tarball with index.json); cilock's OCI attestor parses the former.
Why each attestor:
dockerparses the buildx metadata file (image digest, build duration, image config)ociunpacks the saved image tarball and digests every layer + manifestsbomparses the CycloneDX SBOM that syft generated against the image
The --attestor-product-include-glob filter ensures cilock captures all three artifact files in the product attestation.
Step 2: Push the image and cosign-sign itโ
After the cilock-attested build, push the image and have cosign sign the manifest itself:
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Push image
run: |
docker buildx build \
-t ghcr.io/${{ github.repository }}:${{ github.sha }} \
--push .
- name: Install cosign
uses: sigstore/cosign-installer@v3
- name: cosign sign image (keyless)
run: |
cosign sign --yes \
ghcr.io/${{ github.repository }}:${{ github.sha }}
This is the same cosign keyless-signing pattern the rookery release pipeline uses for its own container image (verifiable from rookery/.github/workflows/release.yml).
The result: one image in GHCR with two parallel pieces of evidence, a cosign signature on the manifest and a cilock attestation in Archivista about how the image was built.
Step 3: Verify both at promotion timeโ
A verifier (release gate, admission controller, or any downstream system) checks both:
# Authenticity: who signed the image manifest?
cosign verify \
--certificate-identity-regexp 'https://github.com/example/myapp/.+' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
ghcr.io/example/myapp:abc123
# Trustworthiness: what built the image, with what evidence?
cilock verify \
--policy ./policy-signed.json \
--publickey ./policy-pubkey.pem \
--attestations "$(archivista_fetch_for_image ghcr.io/example/myapp@sha256:... | paste -sd,)" \
--subjects sha256:...
If either fails, the image doesn't promote. The two checks defend against different attacks:
| Failure mode | Caught by |
|---|---|
| Image manifest replaced in the registry | cosign verify, manifest digest changed |
| Image was built but missing required SBOM | cilock verify, sbom attestation missing from collection |
| Image was built from the wrong branch | cilock verify, Rego rule on the github attestation rejects |
| Image was built but with unsigned/unpinned actions | cilock verify, Layer 1 prevention policy on actionref/refpinned |
Either by itself is a meaningful improvement; together they cover both authenticity and trustworthiness.
Subject digest correlationโ
The trick that makes this work: both cosign and cilock end up with digests that identify the same image, just via different paths. Cosign signs the registry OCI manifest digest (what docker buildx imagetools inspect returns). Cilock's oci attestor reads the tarball and records three subjects: the digest of the in-tar manifest.json (Docker-save format), the full tar digest, and the image-config digest (which is also docker images' "IMAGE ID"). To cross-reference, line up imageid from the cilock side with the image-config digest reachable from the registry-stored manifest:
package image.subject_match
import rego.v1
deny contains msg if {
not input.image_digest == input.cosign_subject
msg := sprintf(
"image digest mismatch: cilock has %s, cosign signed %s",
[input.image_digest, input.cosign_subject]
)
}
(Wire this rule into the oci attestation's regopolicies block.)
Patterns to considerโ
| Pattern | Use case |
|---|---|
| Sign with cosign, attest with cilock | The most common. Image authenticity for any consumer; build provenance for your own gate. |
| Cosign attest with in-toto predicate | Cosign supports attaching attestations directly to images via cosign attest. You can use this instead of Archivista for storing cilock attestations as OCI referrers. |
| Both | Push the cilock attestation as an OCI referrer (via cosign attest --predicate=cilock-attestation.json --type=https://aflock.ai/attestations/...) and push to Archivista. Image consumers see the attestation directly; your gate uses Archivista for cross-build queries. |
Honest caveatsโ
- Image SBOMs vs source SBOMs are different. The SBOM generated from
image.tarenumerates everything in the image (OS packages, included libraries). The SBOM frombin/myapponly enumerates the app's compile-time dependencies. Pick which question you're answering and document it. - Cosign and cilock have different identity models. Cosign verifies a single signature against an identity claim. Cilock verifies attestation collections against a policy with multiple functionaries per step. Both are valid; they answer different questions.
- Verifying in production typically means baking the policy public key (and the Fulcio/Sigstore trust roots) into the verifier, at admission-controller startup, at gate-job startup, etc. Plan key distribution.
See alsoโ
- Release promotion gate, wraps this in a build โ verify โ promote workflow
- SBOM and SARIF evidence, patterns for the SBOM step here
- Cosign documentation, for the cosign side of the workflow