Back

CICD-SEC-9: Improper Artifact Integrity Validation - Code Examples

Each pair below shows an insecure configuration and the secure version for the same hand-off. The examples focus on the controls that matter most for artifact integrity: signing and verifying, build provenance, digest pinning, dependency integrity, and deploy-time admission.

1. Sign and Verify a Container Image (GitHub Actions + cosign)

Insecure

name: build
on: { push: { branches: [main] } }
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build and push (mutable tag, no signature)
        run: |
          docker build -t registry.example.com/api:prod .
          docker push registry.example.com/api:prod
          # No signature is produced. Anyone who can push to :prod
          # can substitute the image and nothing will notice.

Secure

name: build
on: { push: { branches: [main] } }
permissions:
  id-token: write        # OIDC identity for keyless signing
  packages: write
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build and capture the digest
        id: build
        run: |
          docker build -t registry.example.com/api:build .
          docker push registry.example.com/api:build
          DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' \
                   registry.example.com/api:build | cut -d@ -f2)
          echo "digest=$DIGEST" >> "$GITHUB_OUTPUT"
      - uses: sigstore/cosign-installer@v3
      - name: Sign the DIGEST keylessly (no long-lived key)
        run: |
          cosign sign --yes \
            registry.example.com/api@${{ steps.build.outputs.digest }}
# Consumer / deploy step -- verification is the control that matters
cosign verify \
  --certificate-identity-regexp '^https://github.com/acme/api/.github/workflows/build.yml@.+' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  registry.example.com/api@sha256:<digest>  || exit 1   # fail closed

2. Generate and Verify SLSA Build Provenance

Insecure

# The pipeline pushes an artifact with NO record of how it was built.
# A signature (if any) proves the bytes, not the process -- a build-time
# backdoor (SolarWinds-class) would be signed and shipped identically.
docker push registry.example.com/api:prod

Secure

# Emit an in-toto SLSA provenance attestation for the exact digest...
cosign attest --yes \
  --predicate provenance.slsa.json \
  --type slsaprovenance \
  registry.example.com/api@sha256:<digest>

# ...and REQUIRE it (source + builder) before deploying.
cosign verify-attestation \
  --type slsaprovenance \
  --certificate-identity-regexp '^https://github.com/acme/api/.+' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  registry.example.com/api@sha256:<digest>  || exit 1

Provenance answers "which source and which builder produced this?" A bare signature cannot. Requiring provenance is what defends against build-system compromise.

3. Reference by Immutable Digest, Not a Mutable Tag

Insecure

# Dockerfile -- base image can change under you
FROM python:3.12-slim

# Kubernetes -- ":prod" is a moving pointer
spec:
  containers:
    - name: api
      image: registry.example.com/api:prod

Secure

# Dockerfile -- pinned to exact bytes
FROM python:3.12-slim@sha256:0d1f3c9e<...full-digest...>

# Kubernetes -- deploy the exact artifact that was signed + attested
spec:
  containers:
    - name: api
      image: registry.example.com/api@sha256:9a7b2f1c<...full-digest...>

4. Deploy-Time Admission: Only Signed + Attested Images Run

Insecure

# No policy -- the cluster runs whatever is referenced, signed or not.
kubectl apply -f deploy.yaml

Secure

# Sigstore policy-controller: enforce signature + provenance at admission
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
  name: require-signed-attested
spec:
  images:
    - glob: "registry.example.com/**"
  authorities:
    - keyless:
        identities:
          - issuer: https://token.actions.githubusercontent.com
            subjectRegExp: '^https://github.com/acme/.+/.github/workflows/.+'
      attestations:
        - name: slsa-provenance
          predicateType: slsaprovenance
# Unsigned or unattested images are DENIED admission -- fail closed.

The same policy can be written with Kyverno (verifyImages) or OPA/Gatekeeper. Whatever the engine, admission is the last, non-negotiable gate.

5. Enforce Dependency Integrity in the Build

Insecure

# Python -- floating range, no hashes: the index decides what you get
pip install requests

# Node -- non-reproducible install, integrity not enforced
npm install

Secure

# Python -- pinned versions with pinned hashes, verified on install
# requirements.lock:
#   requests==2.32.3 --hash=sha256:<hash>
pip install --require-hashes -r requirements.lock

# Node -- install strictly from the integrity-checked lockfile
npm ci        # aborts if package-lock.json integrity does not match

# Go -- verify module checksums against the checksum database
go mod verify

6. Verify a Signed Build Blob (Non-Container Artifact)

Insecure

# Download a release artifact and use it directly -- no integrity check
curl -O https://artifacts.example.com/app-1.4.2.tar.gz
tar xzf app-1.4.2.tar.gz && ./install.sh    # trusts whatever was served

Secure

# Verify the detached signature against the expected signer BEFORE use
cosign verify-blob \
  --certificate app-1.4.2.tar.gz.pem \
  --signature  app-1.4.2.tar.gz.sig \
  --certificate-identity-regexp '^https://github.com/acme/app/.+' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  app-1.4.2.tar.gz  || { echo "integrity check failed"; exit 1; }
tar xzf app-1.4.2.tar.gz && ./install.sh

7. Validate IaC Before Applying

Insecure

# Applies whatever the plan resolves to, from possibly-tampered modules
terraform apply -auto-approve

Secure

terraform init -lockfile=readonly     # enforce provider checksums (.terraform.lock.hcl)
terraform plan -out tfplan            # produce the exact, reviewable plan
terraform show -json tfplan > tfplan.json
conftest test tfplan.json             # policy-as-code gate (OPA/Rego)
terraform apply tfplan                # apply ONLY the reviewed, gated plan

What Changed, and Why

Hand-offInsecureSecure
Image publishPush to mutable tag, unsignedSign the digest keylessly; verify against signer identity
Build originNo record of how it was builtSLSA provenance generated and required
ReferenceMutable tag (:prod, :latest)Immutable @sha256: digest
DeployCluster runs anythingAdmission policy: signed + attested only
DependenciesFloating versions, no hashes--require-hashes / npm ci / go mod verify
IaCapply -auto-approveLocked plan + policy gate, apply reviewed plan

Next Steps