cloud security

Securing Container Registry Images

Securing container registry images by implementing vulnerability scanning with Trivy and Grype, enforcing image signing with Cosign and Sigstore, configuring registry access controls, and building CI/CD pipelines that prevent deploying unscanned or unsigned images.

cloud-securitycontainerscosignimage-scanningregistrysupply-chaintrivy
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

When to Use

  • When establishing security controls for container image registries (ECR, ACR, GCR, Docker Hub)
  • When building CI/CD pipelines that enforce vulnerability scanning before image promotion
  • When implementing image signing and verification to prevent supply chain attacks
  • When auditing existing registries for vulnerable, unscanned, or unsigned images
  • When compliance requires software bill of materials (SBOM) for deployed container images

Do not use for runtime container security (use Falco or Sysdig), for Kubernetes admission control (use OPA Gatekeeper or Kyverno after establishing registry controls), or for host-level vulnerability scanning (use Amazon Inspector or Qualys).

Prerequisites

  • Trivy installed (brew install trivy or apt install trivy)
  • Grype installed (curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh)
  • Cosign installed for image signing (go install github.com/sigstore/cosign/v2/cmd/cosign@latest)
  • Syft installed for SBOM generation (curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh)
  • Container registry access (ECR, ACR, GCR, or private registry)

Workflow

Step 1: Scan Images for Vulnerabilities with Trivy

Run comprehensive vulnerability scans against container images before and after pushing to the registry.

# Scan a local image for vulnerabilities
trivy image --severity HIGH,CRITICAL myapp:latest
 
# Scan a remote registry image
trivy image --severity HIGH,CRITICAL 123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest
 
# Scan with JSON output for CI/CD processing
trivy image --format json --output trivy-results.json myapp:latest
 
# Scan for vulnerabilities AND misconfigurations
trivy image --scanners vuln,misconfig,secret myapp:latest
 
# Scan a specific image with SBOM output
trivy image --format spdx-json --output sbom.json myapp:latest
 
# Fail CI/CD if critical vulnerabilities found
trivy image --exit-code 1 --severity CRITICAL myapp:latest

Step 2: Scan with Grype for Additional Coverage

Use Grype as a complementary scanner for broader vulnerability database coverage.

# Scan image with Grype
grype myapp:latest
 
# Scan with severity threshold
grype myapp:latest --fail-on critical
 
# Output in JSON for processing
grype myapp:latest -o json > grype-results.json
 
# Scan an SBOM instead of the image directly
syft myapp:latest -o spdx-json > sbom.json
grype sbom:sbom.json
 
# Scan a directory-based image export
grype dir:/path/to/image-rootfs

Step 3: Generate Software Bill of Materials (SBOM)

Create SBOMs for all images to maintain an inventory of software components and dependencies.

# Generate SBOM with Syft in SPDX format
syft myapp:latest -o spdx-json > sbom-spdx.json
 
# Generate SBOM in CycloneDX format
syft myapp:latest -o cyclonedx-json > sbom-cyclonedx.json
 
# Attach SBOM to the image as an OCI artifact
cosign attach sbom --sbom sbom-spdx.json \
  123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest
 
# Verify SBOM contents
syft myapp:latest -o table | head -50

Step 4: Sign Images with Cosign and Sigstore

Implement image signing to ensure image integrity and authenticity in the supply chain.

# Generate a key pair for signing
cosign generate-key-pair
 
# Sign an image in the registry
cosign sign --key cosign.key \
  123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest
 
# Sign using keyless signing with Sigstore (OIDC-based)
cosign sign --yes \
  123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest
 
# Verify image signature
cosign verify --key cosign.pub \
  123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest
 
# Verify keyless signature
cosign verify \
  --certificate-identity developer@company.com \
  --certificate-oidc-issuer https://accounts.google.com \
  123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest
 
# Add attestation with scan results
cosign attest --predicate trivy-results.json \
  --key cosign.key \
  123456789012.dkr.ecr.us-east-1.amazonaws.com/myapp:latest

Step 5: Configure Registry-Level Security Controls

Set up registry-specific security features for ECR, ACR, and GCR.

# AWS ECR: Enable image scanning on push
aws ecr put-image-scanning-configuration \
  --repository-name myapp \
  --image-scanning-configuration scanOnPush=true
 
# ECR: Set image tag immutability (prevent tag overwrites)
aws ecr put-image-tag-mutability \
  --repository-name myapp \
  --image-tag-mutability IMMUTABLE
 
# ECR: Set lifecycle policy to clean up untagged images
aws ecr put-lifecycle-policy \
  --repository-name myapp \
  --lifecycle-policy-text '{
    "rules": [{
      "rulePriority": 1,
      "description": "Remove untagged images after 7 days",
      "selection": {"tagStatus": "untagged", "countType": "sinceImagePushed", "countUnit": "days", "countNumber": 7},
      "action": {"type": "expire"}
    }]
  }'
 
# ECR: Get scan findings for an image
aws ecr describe-image-scan-findings \
  --repository-name myapp \
  --image-id imageTag=latest \
  --query 'imageScanFindings.findingSeverityCounts'
 
# Azure ACR: Enable Defender for container registries
az security pricing create --name ContainerRegistry --tier standard
 
# GCR: Enable Container Analysis
gcloud services enable containeranalysis.googleapis.com
gcloud artifacts docker images list-vulnerabilities \
  LOCATION-docker.pkg.dev/PROJECT/REPO/IMAGE@sha256:DIGEST

Step 6: Build CI/CD Pipeline with Security Gates

Integrate scanning and signing into the CI/CD pipeline as mandatory gates.

# GitHub Actions: Scan, sign, and push image
name: Container Security Pipeline
on: push
 
jobs:
  build-scan-sign:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Build image
        run: docker build -t myapp:${{ github.sha }} .
 
      - name: Trivy vulnerability scan
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: myapp:${{ github.sha }}
          format: json
          output: trivy-results.json
          severity: CRITICAL,HIGH
          exit-code: 1
 
      - name: Generate SBOM
        run: syft myapp:${{ github.sha }} -o spdx-json > sbom.json
 
      - name: Push to ECR
        run: |
          aws ecr get-login-password | docker login --username AWS --password-stdin $ECR_REGISTRY
          docker tag myapp:${{ github.sha }} $ECR_REGISTRY/myapp:${{ github.sha }}
          docker push $ECR_REGISTRY/myapp:${{ github.sha }}
 
      - name: Sign image with Cosign
        run: |
          cosign sign --key env://COSIGN_PRIVATE_KEY \
            $ECR_REGISTRY/myapp:${{ github.sha }}
 
      - name: Attach SBOM
        run: |
          cosign attach sbom --sbom sbom.json \
            $ECR_REGISTRY/myapp:${{ github.sha }}

Key Concepts

Term Definition
Container Image Scanning Automated analysis of container image layers to identify known vulnerabilities in OS packages and application dependencies
Image Signing Cryptographic attestation that verifies the authenticity and integrity of a container image using Cosign or Notation
SBOM Software Bill of Materials, a comprehensive inventory of software components, libraries, and dependencies in a container image
Tag Immutability Registry setting that prevents overwriting existing image tags, ensuring that a tag always refers to the same image digest
Sigstore Open-source project providing keyless signing, transparency logs, and verification tooling for software supply chain security
Image Attestation Cryptographically signed metadata attached to an image (scan results, SBOM, build provenance) that can be verified before deployment

Tools & Systems

  • Trivy: Comprehensive vulnerability scanner for container images, filesystems, git repos, and Kubernetes resources
  • Grype: Anchore's vulnerability scanner with broad vulnerability database coverage for container images and SBOMs
  • Cosign: Sigstore tool for signing, verifying, and attesting container images with key-based or keyless workflows
  • Syft: SBOM generation tool supporting SPDX and CycloneDX formats for container images and filesystems
  • AWS ECR: Container registry with built-in scanning, tag immutability, and lifecycle policies

Common Scenarios

Scenario: Implementing a Secure Image Promotion Pipeline

Context: A development team pushes images to a dev registry without security controls. The security team needs to implement a promotion pipeline that scans, signs, and promotes only approved images to the production registry.

Approach:

  1. Configure ECR scanning on push for the development repository
  2. Add Trivy scanning as a CI/CD gate that blocks images with CRITICAL vulnerabilities
  3. Generate SBOMs with Syft and store alongside image scan results
  4. Sign approved images with Cosign after scanning passes
  5. Configure the production registry to require image signatures for all pushes
  6. Set up Kyverno or OPA Gatekeeper in production Kubernetes to verify signatures before pod creation
  7. Implement lifecycle policies to clean up untagged and old images in both registries

Pitfalls: Vulnerability databases are updated constantly. An image that passes scanning today may have new CRITICAL vulnerabilities discovered tomorrow. Implement continuous scanning of already-deployed images, not just at build time. Image signing keys must be securely stored in KMS or Vault, not in CI/CD environment variables.

Output Format

Container Registry Security Report
=====================================
Registry: 123456789012.dkr.ecr.us-east-1.amazonaws.com
Repositories: 24
Report Date: 2026-02-23
 
IMAGE INVENTORY:
  Total images: 342
  Images scanned: 298 (87%)
  Images signed: 156 (46%)
  Images with SBOM: 134 (39%)
 
VULNERABILITY SUMMARY:
  Critical vulnerabilities:    23 (across 8 images)
  High vulnerabilities:       145 (across 34 images)
  Medium vulnerabilities:     456 (across 67 images)
  Images with no vulns:       89
 
CRITICAL IMAGES REQUIRING REMEDIATION:
  myapp:1.2.3           - 5 CRITICAL (CVE-2026-xxxx in openssl)
  api-gateway:2.0.1     - 3 CRITICAL (CVE-2026-yyyy in log4j)
  worker:latest         - 4 CRITICAL (CVE-2026-zzzz in glibc)
 
REGISTRY CONFIGURATION:
  Scan on push enabled:     18 / 24 repositories
  Tag immutability:         12 / 24 repositories
  Lifecycle policies:       20 / 24 repositories
  Image signing enforced:    8 / 24 repositories
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 1

api-reference.md1.9 KB

API Reference: Securing Container Registry Images

Trivy CLI

trivy image [OPTIONS] IMAGE
Flag Description
--severity Filter by severity: CRITICAL,HIGH,MEDIUM,LOW
--format Output format: table, json, sarif, spdx-json
--exit-code 1 Exit with code 1 if vulnerabilities found
--scanners Scanner types: vuln, misconfig, secret
--output FILE Write results to file

Cosign CLI

Command Description
cosign sign --key KEY IMAGE Sign an image with a private key
cosign verify --key KEY IMAGE Verify image signature
cosign generate-key-pair Generate signing key pair
cosign attest --predicate FILE IMAGE Attach signed attestation
cosign attach sbom --sbom FILE IMAGE Attach SBOM to image

Syft CLI (SBOM Generation)

syft IMAGE -o FORMAT > output.json

Formats: spdx-json, cyclonedx-json, table, json

boto3 ECR Client

Method Description
describe_repositories() Get repository config (scan settings, mutability)
put_image_scanning_configuration() Enable/disable scan on push
put_image_tag_mutability() Set tag immutability (MUTABLE/IMMUTABLE)
put_lifecycle_policy() Set image cleanup rules
describe_image_scan_findings() Get scan results for an image
list_images() List images (filter by tagged/untagged)
get_lifecycle_policy() Get current lifecycle policy

ECR Scan Findings Structure

{
    "findingSeverityCounts": {"CRITICAL": 2, "HIGH": 5},
    "findings": [
        {"name": "CVE-2024-xxxx", "severity": "CRITICAL", "uri": "..."}
    ]
}

References

Scripts 1

agent.py8.0 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Agent for auditing container registry image security: scanning, signing, and SBOM."""

import boto3
import subprocess
import json
import os
import argparse
from datetime import datetime


def scan_image_trivy(image, severity="HIGH,CRITICAL", output_format="json"):
    """Scan a container image with Trivy for vulnerabilities."""
    print(f"[*] Scanning {image} with Trivy...")
    cmd = ["trivy", "image", "--severity", severity, "--format", output_format, image]
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
        if output_format == "json" and result.stdout:
            data = json.loads(result.stdout)
            total_vulns = 0
            for target in data.get("Results", []):
                vulns = target.get("Vulnerabilities", [])
                total_vulns += len(vulns)
                if vulns:
                    print(f"  Target: {target.get('Target', 'unknown')}: {len(vulns)} vulnerabilities")
                    for v in vulns[:5]:
                        print(f"    [{v.get('Severity', '?')}] {v.get('VulnerabilityID', '?')} "
                              f"- {v.get('PkgName', '?')} {v.get('InstalledVersion', '?')}")
            print(f"[*] Total vulnerabilities found: {total_vulns}")
            return data
        else:
            print(result.stdout)
            return None
    except FileNotFoundError:
        print("  [-] Trivy not installed. Install: brew install trivy")
        return None
    except subprocess.TimeoutExpired:
        print("  [-] Scan timed out")
        return None


def generate_sbom(image, output_format="spdx-json", output_file="sbom.json"):
    """Generate SBOM using Syft for a container image."""
    print(f"\n[*] Generating SBOM for {image}...")
    cmd = ["syft", image, "-o", output_format]
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
        if result.stdout:
            with open(output_file, "w") as f:
                f.write(result.stdout)
            data = json.loads(result.stdout)
            packages = data.get("packages", [])
            print(f"  [+] SBOM generated: {len(packages)} packages")
            print(f"  [+] Saved to {output_file}")
            return data
        return None
    except FileNotFoundError:
        print("  [-] Syft not installed. Install: curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh")
        return None


def verify_image_signature(image, key_file=None):
    """Verify image signature using Cosign."""
    print(f"\n[*] Verifying signature for {image}...")
    cmd = ["cosign", "verify"]
    if key_file:
        cmd.extend(["--key", key_file])
    cmd.append(image)
    try:
        result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
        if result.returncode == 0:
            print(f"  [+] Signature VALID for {image}")
            return True
        else:
            print(f"  [-] Signature verification FAILED: {result.stderr.strip()}")
            return False
    except FileNotFoundError:
        print("  [-] Cosign not installed")
        return None


def audit_ecr_repository(repo_name, region="us-east-1"):
    """Audit an ECR repository for security configuration."""
    ecr = boto3.client("ecr", region_name=region)
    findings = []
    try:
        scan_config = ecr.describe_repositories(repositoryNames=[repo_name])["repositories"][0]
        repo_uri = scan_config["repositoryUri"]
        print(f"\n[*] Auditing ECR repository: {repo_name}")
        print(f"  URI: {repo_uri}")

        img_scan = scan_config.get("imageScanningConfiguration", {})
        if not img_scan.get("scanOnPush", False):
            findings.append({"check": "scan_on_push", "status": "FAIL", "detail": "Scan on push disabled"})
            print("  [!] Scan on push: DISABLED")
        else:
            print("  [+] Scan on push: ENABLED")

        mutability = scan_config.get("imageTagMutability", "MUTABLE")
        if mutability == "MUTABLE":
            findings.append({"check": "tag_immutability", "status": "FAIL", "detail": "Tags are mutable"})
            print("  [!] Tag immutability: MUTABLE (tags can be overwritten)")
        else:
            print("  [+] Tag immutability: IMMUTABLE")

        try:
            lifecycle = ecr.get_lifecycle_policy(repositoryName=repo_name)
            print("  [+] Lifecycle policy: CONFIGURED")
        except ecr.exceptions.LifecyclePolicyNotFoundException:
            findings.append({"check": "lifecycle_policy", "status": "FAIL", "detail": "No lifecycle policy"})
            print("  [!] Lifecycle policy: NOT CONFIGURED")

        images = ecr.list_images(repositoryName=repo_name, filter={"tagStatus": "UNTAGGED"})
        untagged = len(images.get("imageIds", []))
        if untagged > 0:
            print(f"  [!] Untagged images: {untagged}")

    except ecr.exceptions.RepositoryNotFoundException:
        print(f"  [-] Repository '{repo_name}' not found")
    return findings


def get_ecr_scan_findings(repo_name, tag="latest", region="us-east-1"):
    """Get vulnerability scan findings for an ECR image."""
    ecr = boto3.client("ecr", region_name=region)
    try:
        response = ecr.describe_image_scan_findings(
            repositoryName=repo_name, imageId={"imageTag": tag},
        )
        findings = response.get("imageScanFindings", {})
        severity_counts = findings.get("findingSeverityCounts", {})
        print(f"\n[*] ECR scan findings for {repo_name}:{tag}")
        for sev, count in sorted(severity_counts.items()):
            print(f"  [{sev}] {count}")
        vuln_findings = findings.get("findings", [])
        for v in vuln_findings[:10]:
            print(f"  - {v.get('name', '?')}: {v.get('description', '')[:100]}")
        return findings
    except Exception as e:
        print(f"  [-] Error: {e}")
        return {}


def full_audit(image, repo_name=None, region="us-east-1", output_dir="."):
    """Run complete container image security audit."""
    print("[*] Starting container image security audit...")
    os.makedirs(output_dir, exist_ok=True)
    report = {"audit_date": datetime.now().isoformat(), "image": image}

    scan_results = scan_image_trivy(image)
    report["trivy_scan"] = scan_results

    sbom_path = os.path.join(output_dir, "sbom.json")
    sbom = generate_sbom(image, output_file=sbom_path)
    report["sbom_packages"] = len(sbom.get("packages", [])) if sbom else 0

    sig_valid = verify_image_signature(image)
    report["signature_valid"] = sig_valid

    if repo_name:
        ecr_findings = audit_ecr_repository(repo_name, region)
        report["ecr_audit"] = ecr_findings

    report_path = os.path.join(output_dir, "container_security_report.json")
    with open(report_path, "w") as f:
        json.dump(report, f, indent=2, default=str)
    print(f"\n[*] Full report saved to {report_path}")


def main():
    parser = argparse.ArgumentParser(description="Container Registry Image Security Agent")
    parser.add_argument("action", choices=["scan", "sbom", "verify", "ecr-audit", "ecr-findings", "full-audit"])
    parser.add_argument("--image", help="Container image reference")
    parser.add_argument("--repo", help="ECR repository name")
    parser.add_argument("--tag", default="latest", help="Image tag for ECR scan results")
    parser.add_argument("--key", help="Cosign public key file")
    parser.add_argument("--region", default="us-east-1")
    parser.add_argument("-o", "--output", default=".")
    args = parser.parse_args()

    if args.action == "scan":
        scan_image_trivy(args.image)
    elif args.action == "sbom":
        generate_sbom(args.image)
    elif args.action == "verify":
        verify_image_signature(args.image, args.key)
    elif args.action == "ecr-audit":
        audit_ecr_repository(args.repo, args.region)
    elif args.action == "ecr-findings":
        get_ecr_scan_findings(args.repo, args.tag, args.region)
    elif args.action == "full-audit":
        full_audit(args.image, args.repo, args.region, args.output)


if __name__ == "__main__":
    main()
Keep exploring