Artifact Security and Supply Chain Protection
Securing build artifacts with signing, SLSA framework, and provenance verification
Artifact Security and Supply Chain Protection
Build artifacts are the output of your CI/CD pipeline - container images, binaries, packages. Securing them ensures what you deploy is what you built.
The Supply Chain Threat
Recent attacks have shown the importance of artifact security:
- SolarWinds (2020) - Malicious code injected into build process
- Codecov (2021) - Compromised bash uploader exfiltrated secrets
- npm packages - Typosquatting and dependency confusion attacks
SLSA Framework
SLSA (Supply-chain Levels for Software Artifacts) provides a maturity model for supply chain security.
SLSA Levels:
Level 0: No guarantees
|
Level 1: Documentation of build process
| - Build script exists
| - Provenance exists
|
Level 2: Tamper resistance of build service
| - Hosted build platform
| - Signed provenance
|
Level 3: Hardened build platform
| - Isolated builds
| - Non-falsifiable provenance
|
Level 4: High assurance
- Two-person review
- Hermetic builds
Container Image Signing with Cosign
Cosign (from Sigstore) enables keyless signing of container images.
Keyless Signing with GitHub Actions
name: Build and Sign Container
on:
push:
branches: [main]
permissions:
contents: read
packages: write
id-token: write # Required for keyless signing
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and Push
id: build
uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
- name: Install Cosign
uses: sigstore/cosign-installer@v3
- name: Sign Image
run: |
cosign sign --yes \
ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
env:
COSIGN_EXPERIMENTAL: 1
Verifying Signed Images
# Verify image signature
cosign verify \
--certificate-identity-regexp "https://github.com/myorg/.*" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
ghcr.io/myorg/myapp:latest
Kubernetes Admission Control
# kyverno-policy.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signatures
spec:
validationFailureAction: Enforce
background: true
rules:
- name: verify-cosign-signature
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "ghcr.io/myorg/*"
attestors:
- entries:
- keyless:
issuer: "https://token.actions.githubusercontent.com"
subject: "https://github.com/myorg/*"
Provenance and Attestations
Provenance documents how an artifact was built - source commit, build commands, builder identity.
Generating SLSA Provenance
name: Build with SLSA Provenance
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v4
- name: Build Image
id: build
uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
provenance:
needs: build
uses: slsa-framework/slsa-github-generator/.github/workflows/[email protected]
with:
image: ghcr.io/${{ github.repository }}
digest: ${{ needs.build.outputs.digest }}
permissions:
id-token: write
packages: write
Verifying Provenance
# Install slsa-verifier
go install github.com/slsa-framework/slsa-verifier/v2/cli/slsa-verifier@latest
# Verify container provenance
slsa-verifier verify-image \
ghcr.io/myorg/myapp@sha256:abc123... \
--source-uri github.com/myorg/myapp \
--source-branch main
Software Bill of Materials (SBOM)
SBOMs list all components in your software for vulnerability tracking.
Generating SBOM with Syft
name: Build with SBOM
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Image
run: docker build -t myapp:latest .
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
image: myapp:latest
format: spdx-json
output-file: sbom.spdx.json
- name: Attach SBOM to Image
run: |
cosign attach sbom \
--sbom sbom.spdx.json \
ghcr.io/myorg/myapp:latest
SBOM Formats
- SPDX - Linux Foundation standard, widely adopted
- CycloneDX - OWASP standard, security-focused
- Syft JSON - Anchore's native format
Artifact Registry Security
Content Trust
# Docker Content Trust
export DOCKER_CONTENT_TRUST=1
# Push signed image
docker push myregistry.com/myapp:latest
# Pull only verified images
docker pull myregistry.com/myapp:latest
Registry Access Control
# GitHub - Restrict package access
# In package settings:
# - Visibility: Private
# - Access: Only specific teams/users
# - Actions: Specific repos only
# ECR Policy
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowPushFromCI",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789:role/github-actions"
},
"Action": [
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"ecr:PutImage"
]
}
]
}
Reproducible Builds
Reproducible builds ensure the same source produces identical artifacts.
Go Reproducible Builds
# Dockerfile with reproducible Go build
FROM golang:1.21 as builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# Reproducible build flags
RUN CGO_ENABLED=0 \
GOOS=linux \
GOARCH=amd64 \
go build -trimpath \
-ldflags="-s -w -buildid=" \
-o /app/server ./cmd/server
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]
Docker Reproducibility
# Pin base image by digest, not tag
FROM node:20-alpine@sha256:abc123def456...
# Use specific package versions
RUN apk add --no-cache \
curl=8.5.0-r0 \
openssl=3.1.4-r2
# Lock npm dependencies
COPY package-lock.json ./
RUN npm ci --ignore-scripts
Release Artifact Verification
GitHub Release Checksums
name: Release
on:
push:
tags: ['v*']
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build Binaries
run: |
GOOS=linux GOARCH=amd64 go build -o myapp-linux-amd64
GOOS=darwin GOARCH=amd64 go build -o myapp-darwin-amd64
GOOS=windows GOARCH=amd64 go build -o myapp-windows-amd64.exe
- name: Generate Checksums
run: |
sha256sum myapp-* > checksums.txt
- name: Sign Checksums
run: |
cosign sign-blob --yes \
--output-signature checksums.txt.sig \
checksums.txt
- name: Create Release
uses: softprops/action-gh-release@v1
with:
files: |
myapp-*
checksums.txt
checksums.txt.sig
Verifying Downloaded Artifacts
#!/bin/bash
# verify-download.sh
# Download release files
curl -LO https://github.com/myorg/myapp/releases/download/v1.0.0/myapp-linux-amd64
curl -LO https://github.com/myorg/myapp/releases/download/v1.0.0/checksums.txt
curl -LO https://github.com/myorg/myapp/releases/download/v1.0.0/checksums.txt.sig
# Verify signature
cosign verify-blob \
--certificate-identity-regexp "https://github.com/myorg/.*" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
--signature checksums.txt.sig \
checksums.txt
# Verify checksum
sha256sum -c checksums.txt --ignore-missing
Artifact Security Checklist
- All container images signed with Cosign or Docker Content Trust
- SLSA provenance generated for builds
- SBOMs generated and attached to artifacts
- Registry access restricted to CI/CD and authorized users
- Kubernetes admission policy enforces signature verification
- Base images pinned by digest
- Dependencies locked to specific versions
- Release artifacts include checksums and signatures
Key Takeaways
- Sign everything - Use Cosign keyless signing for container images
- Generate provenance - SLSA framework provides supply chain transparency
- Create SBOMs - Know what's in your software for vulnerability management
- Verify before deploy - Admission controllers enforce signature policies
- Pin versions - Base images and dependencies pinned by digest/version
Part of: CI/CD Pipeline Hardening
Updated: 1/24/2025
Found an issue?