Supply Chain Security with GCP Artifact Registry: Vulnerability Scanning and Binary Authorization
Implementing end-to-end container supply chain security using Artifact Registry vulnerability scanning, Binary Authorization, and attestation workflows.

A container image is code you didn't write, packaged by people you don't know, pulled from infrastructure you don't control. Supply chain security isn't paranoia — it's acknowledging that your production workloads inherit every vulnerability in every layer of every base image. Artifact Registry with Binary Authorization gives you provenance, scanning, and policy enforcement in a single integrated pipeline.
After a critical CVE in a transitive dependency reached our production GKE clusters (Log4Shell was the wake-up call), we implemented a full supply chain security architecture. This is the implementation.
The Threat Model
Supply chain attacks target the build and deployment pipeline:
- Compromised base images — Malicious layers in public Docker Hub images
- Dependency confusion — Internal package names squatted on public registries
- CI/CD pipeline injection — Attackers modifying build steps to inject backdoors
- Unscanned deployments — Containers reaching production without vulnerability assessment
- Image tag mutability — Tags pointing to different digests over time
Architecture: Defense in Depth
Our supply chain security architecture has four layers:
| Layer | Control | Tool | Enforcement |
|---|---|---|---|
| Build | Reproducible builds | Cloud Build + Kaniko | Hermetic builds |
| Store | Vulnerability scanning | Artifact Registry + On-Demand Scanning | Block critical CVEs |
| Attest | Provenance verification | Binary Authorization | Cryptographic attestation |
| Deploy | Admission control | GKE + Binary Authorization | Reject unsigned images |
Layer 1: Hardened Build Pipeline
Every image builds in Cloud Build with strict controls:
# cloudbuild.yaml - Secure container build pipeline
steps:
# Step 1: Verify source integrity
- name: 'gcr.io/cloud-builders/git'
entrypoint: 'bash'
args:
- '-c'
- |
# Verify commit signature
git verify-commit HEAD || exit 1
echo "Commit signature verified"
# Step 2: Dependency audit
- name: 'node:20-slim'
entrypoint: 'bash'
args:
- '-c'
- |
npm audit --audit-level=critical
if [ $? -ne 0 ]; then
echo "CRITICAL: npm audit found critical vulnerabilities"
exit 1
fi
# Step 3: Build with Kaniko (no Docker daemon = no privilege escalation)
- name: 'gcr.io/kaniko-project/executor:latest'
args:
- '--destination=${_REGION}-docker.pkg.dev/${PROJECT_ID}/${_REPO}/${_IMAGE}:${SHORT_SHA}'
- '--destination=${_REGION}-docker.pkg.dev/${PROJECT_ID}/${_REPO}/${_IMAGE}:latest'
- '--cache=true'
- '--cache-repo=${_REGION}-docker.pkg.dev/${PROJECT_ID}/${_REPO}/cache'
- '--reproducible'
- '--single-snapshot'
# Step 4: Vulnerability scan before attestation
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
entrypoint: 'bash'
args:
- '-c'
- |
IMAGE="${_REGION}-docker.pkg.dev/${PROJECT_ID}/${_REPO}/${_IMAGE}:${SHORT_SHA}"
# Trigger on-demand scan
gcloud artifacts docker images scan "$IMAGE" \
--format='value(response.scan)' > /workspace/scan_id.txt
SCAN_ID=$(cat /workspace/scan_id.txt)
# Wait for scan completion and get results
gcloud artifacts docker images list-vulnerabilities "$SCAN_ID" \
--format='json' > /workspace/vulns.json
# Check for critical/high vulnerabilities
CRITICAL=$(jq '[.[] | select(.vulnerability.effectiveSeverity=="CRITICAL")] | length' /workspace/vulns.json)
HIGH=$(jq '[.[] | select(.vulnerability.effectiveSeverity=="HIGH")] | length' /workspace/vulns.json)
echo "Scan results: $CRITICAL critical, $HIGH high vulnerabilities"
if [ "$CRITICAL" -gt 0 ]; then
echo "BLOCKED: Critical vulnerabilities found"
jq '.[] | select(.vulnerability.effectiveSeverity=="CRITICAL") | .vulnerability.shortDescription' /workspace/vulns.json
exit 1
fi
if [ "$HIGH" -gt 5 ]; then
echo "BLOCKED: More than 5 high vulnerabilities"
exit 1
fi
# Step 5: Create attestation (Binary Authorization)
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
entrypoint: 'bash'
args:
- '-c'
- |
IMAGE="${_REGION}-docker.pkg.dev/${PROJECT_ID}/${_REPO}/${_IMAGE}:${SHORT_SHA}"
DIGEST=$(gcloud artifacts docker images describe "$IMAGE" --format='value(image_summary.digest)')
# Create attestation using Cloud KMS key
gcloud beta container binauthz attestations sign-and-create \
--project="${PROJECT_ID}" \
--artifact-url="${_REGION}-docker.pkg.dev/${PROJECT_ID}/${_REPO}/${_IMAGE}@${DIGEST}" \
--attestor="projects/${PROJECT_ID}/attestors/build-attestor" \
--attestor-project="${PROJECT_ID}" \
--keyversion="projects/${PROJECT_ID}/locations/global/keyRings/binauthz/cryptoKeys/attestor-key/cryptoKeyVersions/1"
options:
logging: CLOUD_LOGGING_ONLY
machineType: 'E2_HIGHCPU_8'
Layer 2: Artifact Registry Configuration
# terraform/artifact-registry.tf
resource "google_artifact_registry_repository" "production" {
location = "us-central1"
repository_id = "production"
format = "DOCKER"
description = "Production container images"
docker_config {
immutable_tags = true # Tags cannot be overwritten
}
cleanup_policies {
id = "keep-minimum-versions"
action = "KEEP"
most_recent_versions {
keep_count = 10
}
}
cleanup_policies {
id = "delete-old-untagged"
action = "DELETE"
condition {
older_than = "2592000s" # 30 days
tag_state = "UNTAGGED"
}
}
# Enable vulnerability scanning
vulnerability_scanning_config {
enablement = "ENABLED"
scanning_level = "STANDARD" # or "ENTERPRISE" for deeper analysis
}
}
# Remote repository for proxying Docker Hub (controls upstream access)
resource "google_artifact_registry_repository" "docker_hub_proxy" {
location = "us-central1"
repository_id = "dockerhub-proxy"
format = "DOCKER"
description = "Docker Hub proxy with vulnerability scanning"
mode = "REMOTE_REPOSITORY"
remote_repository_config {
docker_repository {
public_repository = "DOCKER_HUB"
}
}
}
The immutable_tags = true setting is critical: it prevents tag overwriting attacks where an attacker pushes a malicious image to the same tag.
Layer 3: Binary Authorization Policy
# terraform/binary-authorization.tf
resource "google_binary_authorization_policy" "production" {
project = var.project_id
default_admission_rule {
evaluation_mode = "REQUIRE_ATTESTATION"
enforcement_mode = "ENFORCED_BLOCK_AND_AUDIT_LOG"
require_attestations_by = [
google_binary_authorization_attestor.build_attestor.name,
google_binary_authorization_attestor.security_attestor.name,
]
}
# Allow specific system images without attestation
admission_whitelist_patterns {
name_pattern = "gcr.io/google-containers/*"
}
admission_whitelist_patterns {
name_pattern = "gke.gcr.io/*"
}
admission_whitelist_patterns {
name_pattern = "gcr.io/gke-release/*"
}
# Cluster-specific overrides for staging (allow unattested)
cluster_admission_rules {
cluster = "us-central1.staging-cluster"
evaluation_mode = "ALWAYS_ALLOW"
enforcement_mode = "DRYRUN_AUDIT_LOG_ONLY"
}
global_policy_evaluation_mode = "ENABLE"
}
resource "google_binary_authorization_attestor" "build_attestor" {
name = "build-attestor"
project = var.project_id
attestation_authority_note {
note_reference = google_container_analysis_note.build_note.name
public_keys {
id = "projects/${var.project_id}/locations/global/keyRings/binauthz/cryptoKeys/attestor-key/cryptoKeyVersions/1"
pkix_public_key {
public_key_pem = var.attestor_public_key
signature_algorithm = "RSA_SIGN_PKCS1_4096_SHA512"
}
}
}
}
Layer 4: Runtime Monitoring
Even with attestation, we continuously scan running containers:
# scripts/continuous_scan_monitor.py
"""Monitor Artifact Registry scan results and alert on new CVEs."""
from google.cloud import containeranalysis_v1
from google.cloud import monitoring_v3
import datetime
def check_running_image_vulnerabilities(project_id: str, cluster_name: str):
"""Cross-reference running images with latest scan results."""
client = containeranalysis_v1.ContainerAnalysisClient()
grafeas_client = client.get_grafeas_client()
# Get all vulnerability occurrences in the project
filter_str = 'kind="VULNERABILITY" AND resourceUrl="https://us-central1-docker.pkg.dev/"'
occurrences = grafeas_client.list_occurrences(
parent=f"projects/{project_id}",
filter=filter_str
)
critical_vulns = []
for occ in occurrences:
vuln = occ.vulnerability
if vuln.effective_severity.name in ("CRITICAL", "HIGH"):
# Check if fix is available
for pkg in vuln.package_issue:
if pkg.fixed_version.kind != "MAXIMUM":
critical_vulns.append({
"image": occ.resource_uri,
"cve": vuln.short_description,
"severity": vuln.effective_severity.name,
"package": pkg.affected_package,
"fix_version": str(pkg.fixed_version),
"discovered": occ.create_time.isoformat(),
})
if critical_vulns:
emit_alert(project_id, critical_vulns)
return critical_vulns
def emit_alert(project_id: str, vulnerabilities: list):
"""Send custom metric to Cloud Monitoring for alerting."""
client = monitoring_v3.MetricServiceClient()
series = monitoring_v3.TimeSeries()
series.metric.type = "custom.googleapis.com/security/critical_cves_in_production"
series.resource.type = "global"
point = monitoring_v3.Point()
point.value.int64_value = len(vulnerabilities)
now = datetime.datetime.utcnow()
point.interval.end_time.seconds = int(now.timestamp())
series.points = [point]
client.create_time_series(
name=f"projects/{project_id}",
time_series=[series]
)
Vulnerability Scan Results Dashboard
Our scanning catches issues before they reach production:
| Month | Images Scanned | Critical Blocked | High Blocked | Mean Fix Time |
|---|---|---|---|---|
| Jan | 1,240 | 8 | 34 | 4.2 hours |
| Feb | 1,380 | 12 | 41 | 3.8 hours |
| Mar | 1,520 | 6 | 28 | 2.9 hours |
| Apr | 1,640 | 15 | 52 | 3.1 hours |
| May | 1,780 | 9 | 38 | 2.4 hours |
Operational Impact
| Metric | Before | After | Change |
|---|---|---|---|
| Mean time to detect CVE | 14 days | 4 minutes | -99.98% |
| Critical CVEs in production | 3-5 at any time | 0 | -100% |
| Unauthorized image deployments | ~12/month | 0 (blocked) | -100% |
| Image build reproducibility | Not verified | 100% verified | Complete |
| Supply chain audit compliance | Manual, quarterly | Automated, continuous | Always compliant |
| Developer friction (build time) | N/A | +3 minutes | Acceptable |
Base Image Strategy
We maintain curated base images that are pre-scanned and attested:
# Base images rebuilt weekly with latest patches
# Stored in: us-central1-docker.pkg.dev/my-project/base-images/
# Node.js base - distroless for minimal attack surface
FROM gcr.io/distroless/nodejs20-debian12:nonroot
# Only contains: Node.js runtime, CA certs, /tmp
# No shell, no package manager, no utilities
# Python base - distroless
FROM gcr.io/distroless/python3-debian12:nonroot
# Go base - static binary
FROM gcr.io/distroless/static-debian12:nonroot
Distroless images reduce the vulnerability surface by 90%+ compared to full OS images like ubuntu or alpine.
Conclusion
Container supply chain security isn't a single tool — it's a pipeline of controls that each eliminate a category of risk. Artifact Registry provides the scanning foundation. Binary Authorization ensures only verified images deploy. Immutable tags prevent tampering. Continuous monitoring catches newly-discovered vulnerabilities in running workloads.
The total added latency to our build pipeline is 3 minutes. The security posture improvement is transformational: from discovering CVEs weeks after deployment to blocking them before they ever reach a cluster. That trade-off isn't even close.
Recommended reading

Per-Team Cost Allocation in Shared Kubernetes Clusters: From Chaos to Clarity
Implementing accurate per-namespace cost allocation in multi-tenant Kubernetes clusters, covering request vs. usage attribution, shared resource amortization, and building showback dashboards that drive accountability.

Measuring and Eliminating Toil: From 40% to 12% of Engineering Time
A systematic approach to identifying, measuring, and automating toil—the repetitive operational work that scales linearly with service growth and prevents engineers from doing creative work.

Serverless Postgres in Production: Branching, Scale-to-Zero, and the End of Database Provisioning
Running Neon serverless Postgres in production for 8 months — covering database branching workflows, scale-to-zero economics, connection pooling, and migration from RDS.

Comments
No comments yet. Be the first to share your thoughts.