Summary
Project Copacetic (Copa) has merged Go binary patching support (copacetic#1388), shipping in Copa v0.14.0. This is directly relevant to Gatekeeper — our production images (gatekeeper, gator) are built on gcr.io/distroless/static-debian12 which contains no OS packages to patch. The only vulnerability surface is the compiled Go binaries and their dependencies/stdlib.
This issue proposes adding a recurring GitHub Actions workflow that uses Copa to produce patched variants of released images, addressing Go dependency and stdlib CVEs without requiring a full Gatekeeper release.
Motivation
- Gatekeeper images use distroless — traditional OS-level patching tools are ineffective. Copa's new Go patching capability is uniquely suited to this use case.
- We already run Trivy + govulncheck on every push (
scan-vulns.yaml), but scanning only detects vulnerabilities — it doesn't remediate them between releases.
- Users in regulated environments or with strict CVE remediation SLAs often can't wait for the next Gatekeeper release to pick up Go dependency or stdlib fixes.
- Copa patches images without a full rebuild, producing a thin patch layer on top of the existing release image and preserving layer integrity.
- Copa's Go patching can fix both Go module/dependency CVEs and stdlib CVEs (by rebuilding with a newer patch-level Go compiler, e.g.,
go1.26.0 → go1.26.3).
Prerequisites
- Copa v0.14.0 must be released — Go patching (copacetic#1388) is merged to
main but not yet in a release. This workflow should be implemented once v0.14.0 ships.
- VCS commit metadata in binaries — Copa's binary rebuild path requires Go binaries to have embedded VCS commit hashes (
go version -m). This is the default for Go 1.18+ when .git is present in the build context. We should verify our Dockerfile doesn't strip this (it shouldn't — we use standard go build).
Proposed Design
Workflow: .github/workflows/copa-patch.yaml
Trigger: Weekly schedule (e.g., Mondays) + workflow_dispatch for manual runs.
Scope: Patch the latest stable + previous minor release images (e.g., v3.22.x and v3.21.x) for:
- ✅
gatekeeper — distroless + Go binary → Go patching
- ✅
gator — distroless + Go binary → Go patching
- ⏭️
gatekeeper-crds — scratch base + kubectl, no Go binary → skip
Steps (per image, per release):
1. Determine target release tags (latest stable + previous minor)
2. Pull release image from GHCR
3. Trivy scan → JSON report (--ignore-unfixed --pkg-types library)
4. If zero fixable Go vulnerabilities → skip, no patch needed
5. Copa patch:
COPA_EXPERIMENTAL=1 copa patch \
-i ghcr.io/open-policy-agent/gatekeeper:v3.22.0 \
-r scan.json \
--pkg-types library \
--toolchain-patch-level patch \
--library-patch-level patch \
--ignore-errors \
-t v3.22.0-$REVISION
6. Trivy re-scan patched image → verify CVE reduction, log delta
7. Push patched image to DockerHub + GHCR
Tagging Scheme
| Tag |
Type |
Example |
Purpose |
vX.Y.Z-R |
Immutable |
v3.22.0-1, v3.22.0-2 |
Pinnable, auditable patch revision |
vX.Y.Z-patched |
Floating |
v3.22.0-patched |
Always points to latest -R for that release |
-R increments monotonically (-1, -2, -3, ...).
-patched is re-tagged on each successful run to point to the latest -R.
- If a scan finds zero fixable CVEs, no new
-R is produced and -patched stays unchanged.
Registry Push Targets
Consistent with existing release workflow:
- DockerHub:
openpolicyagent/gatekeeper:v3.22.0-1, openpolicyagent/gatekeeper:v3.22.0-patched
- GHCR:
ghcr.io/open-policy-agent/gatekeeper:v3.22.0-1, ghcr.io/open-policy-agent/gatekeeper:v3.22.0-patched
Same for gator.
Workflow Skeleton
name: Copa Patch
on:
schedule:
- cron: '17 8 * * 1' # Weekly, Mondays 08:17 UTC
workflow_dispatch:
inputs:
release_tag:
description: 'Release tag to patch (e.g., v3.22.0). Defaults to latest stable + previous minor.'
required: false
jobs:
resolve-releases:
runs-on: ubuntu-latest
outputs:
releases: ${{ steps.resolve.outputs.matrix }}
steps:
- name: Determine target releases
id: resolve
run: |
# Resolve latest stable + previous minor via gh release list
# Output as JSON matrix: ["v3.22.0", "v3.21.1"]
patch:
needs: resolve-releases
runs-on: ubuntu-latest
strategy:
matrix:
image: [gatekeeper, gator]
release: ${{ fromJson(needs.resolve-releases.outputs.releases) }}
fail-fast: false
steps:
- name: Determine patch revision
# Query GHCR for existing -R tags, compute next revision number
- name: Pull release image
run: docker pull ghcr.io/open-policy-agent/${{ matrix.image }}:${{ matrix.release }}
- name: Scan with Trivy
uses: aquasecurity/trivy-action@master
with:
image-ref: 'ghcr.io/open-policy-agent/${{ matrix.image }}:${{ matrix.release }}'
format: json
output: scan.json
ignore-unfixed: true
pkg-types: library
- name: Check for fixable vulnerabilities
id: check
run: |
# Parse scan.json, exit early if 0 fixable CVEs
# Set output: has_fixes=true/false
- name: Setup BuildKit
if: steps.check.outputs.has_fixes == 'true'
run: docker buildx create --name copa-buildkit --use --bootstrap
- name: Install Copa
if: steps.check.outputs.has_fixes == 'true'
# Install Copa >= v0.14.0
- name: Patch image
if: steps.check.outputs.has_fixes == 'true'
run: |
COPA_EXPERIMENTAL=1 copa patch \
-i ghcr.io/open-policy-agent/${{ matrix.image }}:${{ matrix.release }} \
-r scan.json \
--pkg-types library \
--toolchain-patch-level patch \
--library-patch-level patch \
--ignore-errors \
-t ${{ matrix.release }}-${{ env.REVISION }}
- name: Verify patched image
if: steps.check.outputs.has_fixes == 'true'
# Re-scan with Trivy, log before/after CVE counts
- name: Login to GHCR + DockerHub
if: steps.check.outputs.has_fixes == 'true'
# docker login for both registries
- name: Push patched images
if: steps.check.outputs.has_fixes == 'true'
run: |
TAG="${{ matrix.release }}"
REV="${{ env.REVISION }}"
IMAGE="${{ matrix.image }}"
# Push immutable revision tag
docker push ghcr.io/open-policy-agent/${IMAGE}:${TAG}-${REV}
docker push openpolicyagent/${IMAGE}:${TAG}-${REV}
# Update floating -patched tag
docker tag ghcr.io/open-policy-agent/${IMAGE}:${TAG}-${REV} \
ghcr.io/open-policy-agent/${IMAGE}:${TAG}-patched
docker tag openpolicyagent/${IMAGE}:${TAG}-${REV} \
openpolicyagent/${IMAGE}:${TAG}-patched
docker push ghcr.io/open-policy-agent/${IMAGE}:${TAG}-patched
docker push openpolicyagent/${IMAGE}:${TAG}-patched
Considerations
Copa Go Patching Mechanics
- Gatekeeper uses distroless → no Go toolchain in runtime image → Copa uses the binary rebuild path (extracts VCS metadata, clones source, applies
go get updates, rebuilds binary).
--toolchain-patch-level patch handles stdlib CVEs by rebuilding with a newer Go patch release.
--library-patch-level patch handles Go module CVEs via conservative semver-compatible updates.
--ignore-errors ensures a single module failure doesn't block other patches.
Multi-Architecture
- Gatekeeper builds for
linux/amd64, linux/arm64, linux/arm/v7. Copa patching needs to run per-platform and the patched manifest list should cover all architectures.
When This Blocks
- This workflow is blocked on Copa v0.14.0 being released. Track copacetic releases for availability.
References
Summary
Project Copacetic (Copa) has merged Go binary patching support (copacetic#1388), shipping in Copa v0.14.0. This is directly relevant to Gatekeeper — our production images (
gatekeeper,gator) are built ongcr.io/distroless/static-debian12which contains no OS packages to patch. The only vulnerability surface is the compiled Go binaries and their dependencies/stdlib.This issue proposes adding a recurring GitHub Actions workflow that uses Copa to produce patched variants of released images, addressing Go dependency and stdlib CVEs without requiring a full Gatekeeper release.
Motivation
scan-vulns.yaml), but scanning only detects vulnerabilities — it doesn't remediate them between releases.go1.26.0→go1.26.3).Prerequisites
mainbut not yet in a release. This workflow should be implemented once v0.14.0 ships.go version -m). This is the default for Go 1.18+ when.gitis present in the build context. We should verify our Dockerfile doesn't strip this (it shouldn't — we use standardgo build).Proposed Design
Workflow:
.github/workflows/copa-patch.yamlTrigger: Weekly schedule (e.g., Mondays) +
workflow_dispatchfor manual runs.Scope: Patch the latest stable + previous minor release images (e.g., v3.22.x and v3.21.x) for:
gatekeeper— distroless + Go binary → Go patchinggator— distroless + Go binary → Go patchinggatekeeper-crds—scratchbase + kubectl, no Go binary → skipSteps (per image, per release):
Tagging Scheme
vX.Y.Z-Rv3.22.0-1,v3.22.0-2vX.Y.Z-patchedv3.22.0-patched-Rfor that release-Rincrements monotonically (-1,-2,-3, ...).-patchedis re-tagged on each successful run to point to the latest-R.-Ris produced and-patchedstays unchanged.Registry Push Targets
Consistent with existing release workflow:
openpolicyagent/gatekeeper:v3.22.0-1,openpolicyagent/gatekeeper:v3.22.0-patchedghcr.io/open-policy-agent/gatekeeper:v3.22.0-1,ghcr.io/open-policy-agent/gatekeeper:v3.22.0-patchedSame for
gator.Workflow Skeleton
Considerations
Copa Go Patching Mechanics
go getupdates, rebuilds binary).--toolchain-patch-level patchhandles stdlib CVEs by rebuilding with a newer Go patch release.--library-patch-level patchhandles Go module CVEs via conservative semver-compatible updates.--ignore-errorsensures a single module failure doesn't block other patches.Multi-Architecture
linux/amd64,linux/arm64,linux/arm/v7. Copa patching needs to run per-platform and the patched manifest list should cover all architectures.When This Blocks
References