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.

#gcp#artifact-registry#containers#security
Cover image for the article: Supply Chain Security with GCP Artifact Registry: Vulnerability Scanning and Binary Authorization

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:

  1. Compromised base images — Malicious layers in public Docker Hub images
  2. Dependency confusion — Internal package names squatted on public registries
  3. CI/CD pipeline injection — Attackers modifying build steps to inject backdoors
  4. Unscanned deployments — Containers reaching production without vulnerability assessment
  5. Image tag mutability — Tags pointing to different digests over time

Container Supply Chain Attack Surface

Architecture: Defense in Depth

Our supply chain security architecture has four layers:

LayerControlToolEnforcement
BuildReproducible buildsCloud Build + KanikoHermetic builds
StoreVulnerability scanningArtifact Registry + On-Demand ScanningBlock critical CVEs
AttestProvenance verificationBinary AuthorizationCryptographic attestation
DeployAdmission controlGKE + Binary AuthorizationReject 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:

MonthImages ScannedCritical BlockedHigh BlockedMean Fix Time
Jan1,2408344.2 hours
Feb1,38012413.8 hours
Mar1,5206282.9 hours
Apr1,64015523.1 hours
May1,7809382.4 hours

Monthly Vulnerability Trend

Operational Impact

MetricBeforeAfterChange
Mean time to detect CVE14 days4 minutes-99.98%
Critical CVEs in production3-5 at any time0-100%
Unauthorized image deployments~12/month0 (blocked)-100%
Image build reproducibilityNot verified100% verifiedComplete
Supply chain audit complianceManual, quarterlyAutomated, continuousAlways compliant
Developer friction (build time)N/A+3 minutesAcceptable

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.

Comments

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