Skip to content

daimoniac/suppline

Repository files navigation

suppline logo

“Self-hosted image intake gateway for Kubernetes.”

Continuously mirrors remote registries, scans, policy-gates, and attests images before they reach your cluster.

suppline mirrors images from public registries into your local registry, continuously scans them with Trivy, evaluates CEL-based security policies, and publishes Sigstore attestations. Clusters then pull only from the local mirror and can enforce “verified-only” deployments via Kyverno/OPA.

Mirror → Scan → Gate → Attest → Run. cloud native, one service, no SaaS dependency — air-gap compatible by design. Increase availability, decrease vendor dependency, improve supply chain security, all in one go.

Overview

suppline automates the complete container supply chain workflow for third party images:

  1. Mirror - Continuously syncs images from remote registries to your local registry using regsync
  2. Scan - Runs Trivy to identify vulnerabilities and generate SBOMs
  3. Evaluate - Applies CEL-based policies with VEX (Vulnerability Exploitability Exchange) support per repository
  4. Attest - Creates signed attestations (SBOM, vulnerabilities, VEX, SCAI) via Sigstore

Runs as a single Go binary with built-in state persistence, REST API, and observability. Clusters pull only from your local mirror — no external registry dependencies. Integrates with Kyverno/OPA policies to enforce only scanned, compliant images in your cluster.

Features

  • Continuous Registry Mirroring - Syncs images from public registries to your local mirror using regsync, keeping your supply chain local and available
  • Bring Your Own Registry - Mirror to any private registry or use built-in local storage
  • Registry Monitoring - Watches for new/updated images in your local mirror
  • Smart Rescanning - Conditional logic based on digest changes and time intervals
  • VEX Statements - Industry-standard CycloneDX VEX (Vulnerability Exploitability Exchange) to exempt specific CVEs with analysis state, justification, and expiry dates
  • Policy Engine - CEL-based policies with per-repository overrides
  • Sigstore Attestations - SBOM, vulnerability, VEX, and SCAI attestations with cosign
  • State Persistence - SQLite-based scan history and vulnerability tracking
  • REST API - Query results, trigger rescans, manage policies
  • Observability - Prometheus metrics, structured JSON logs, health checks
  • Air-Gap Compatible - No external registry dependencies, works in isolated environments

Why Mirror?

Continuous registry mirroring with suppline provides critical benefits:

  • Increased Availability - Clusters pull from your local registry, not external vendors. No more image pull failures due to upstream outages.
  • Decreased Vendor Dependency - Your supply chain is no longer tied to the availability of Docker Hub, Quay, or other public registries.
  • Improved Supply Chain Security - All images pass through your security pipeline before reaching clusters. Enforce policies, scan for vulnerabilities, and attest every image.
  • Air-Gap Deployments - Mirror images once, deploy to isolated networks without external registry access.
  • Compliance & Audit - Complete audit trail of every image, scan result, and policy decision in your local database.
  • Cost Optimization - Reduce egress bandwidth by pulling from local registry instead of remote sources.

How It Works

Remote Registries → Mirroring (regsync) → Local Registry
                                              ↓
                                          Watcher → Queue → Worker → Scanner (Trivy)
                                                                          ↓
                                                                  Policy Engine
                                                                          ↓
                                                                  Attestor (Cosign)
                                                                          ↓
                                                                  State Store (SQLite)
                                                                          ↓
                                                                  REST API / Metrics

Mirroring continuously syncs images from remote registries to your local registry using regsync configuration. Watcher polls your local registry for new/updated images and enqueues scan tasks. Worker processes tasks through the pipeline: scan with Trivy, evaluate policy, create attestations, persist results. API exposes scan data and metrics for integration with Kyverno/OPA policies. Kubernetes clusters pull only from your local mirror, eliminating external registry dependencies.

Getting Started

Prerequisites

  • Container registry credentials
  • Cosign key pair for attestations

1. Configure

cp suppline.yml.example suppline.yml

Edit suppline.yml with your registry credentials and mirroring rules:

version: 1

creds:
  - registry: docker.io
    user: [username]
    pass: [password]
  - registry: myregistry.com
    user: [username]
    pass: [password]

defaults:
  parallel: 2
  x-rescanInterval: 7d
  x-runtimeInUseWindow: 60m
  x-policy:
    expression: "criticalCount == 0"
    failureMessage: "critical vulnerabilities found"

sync:
  - source: nginx
    target: myregistry.com/nginx
    type: repository
    x-vex:
      - id: CVE-2024-56171
        state: not_affected
        justification: vulnerable_code_not_present
        detail: "Accepted risk"
        expires_at: 2025-12-31T23:59:59Z
  - source: kubernetes/pause
    target: myregistry.com/kubernetes/pause
    type: repository

The sync section defines what images to mirror from remote registries to your local registry. Images are continuously kept in sync, scanned, and evaluated against your policies.

2. Generate Keys

mkdir -p keys
cosign generate-key-pair
mv cosign.key keys/

3. Start

docker compose up -d

4. Verify

curl http://localhost:8081/health
curl http://localhost:8080/api/v1/scans

Configuration

Environment Variables

Variable Default Purpose
SUPPLINE_CONFIG suppline.yml Config file path
LOG_LEVEL info Log level (debug, info, warn, error)
QUEUE_BUFFER_SIZE 1000 Task queue capacity
WORKER_POLL_INTERVAL 5s Worker poll frequency
WORKER_RETRY_ATTEMPTS 3 Max retries for transient failures
TRIVY_SERVER_ADDR localhost:4954 Trivy server address
TRIVY_TIMEOUT 5m Scan timeout
ATTESTATION_COMMAND_TIMEOUT 2m Timeout per cosign attest invocation
SQLITE_PATH suppline.db Database file path
ATTESTATION_KEY_PATH /keys/cosign.key Cosign private key
API_PORT 8080 API server port
SUPPLINE_API_KEY - Optional API authentication
METRICS_PORT 9090 Prometheus metrics port
HEALTH_CHECK_PORT 8081 Health check port

Configuration Format

suppline.yml uses regsync format with suppline extensions for mirroring and security policies. You can use golang templating expansion, useful for e.g. secrets:

version: 1

creds:
  - registry: docker.io
    user: '{{ env "DOCKER_USERNAME" }}'
    pass: '{{ env "DOCKER_PASSWORD" }}'
  - registry: myregistry.com
    user: '{{ env "MYREGISTRY_USERNAME" }}'
    pass: '{{ env "MYREGISTRY_PASSWORD" }}'

defaults:
  parallel: 2
  x-rescanInterval: 7d
  x-runtimeInUseWindow: 60m
  x-policy:
    expression: "criticalCount == 0"
    failureMessage: "critical vulnerabilities found"
  x-vex:                              # Default VEX statements for all targets
    - id: CVE-2024-00001
      state: false_positive
      detail: "Known false positive"
      expires_at: 2025-12-31T23:59:59Z

sync:
  - source: nginx
    target: myregistry.com/nginx
    type: repository
    x-rescanInterval: 3d              # Override default
    x-policy:                         # Override default
      expression: "criticalCount == 0 && highCount <= 5"
    x-vex:                            # Merged with default VEX statements
      - id: CVE-2024-56171
        state: not_affected
        justification: vulnerable_code_not_present
        detail: "Accepted risk"
        expires_at: 2025-12-31T23:59:59Z

Key Fields:

  • source - Source image/repository (from remote registry)
  • target - Target location in your local registry
  • type - repository (all tags) or image (specific tag)
  • x-rescanInterval - How often to rescan unchanged images (default: 24h)
  • x-runtimeInUseWindow - Runtime image is considered in use when last_seen_at is within this window, or when it was present in the most recent cluster sync (default: 60m)
  • x-policy - CEL-based security policy for this mirror
  • x-vex - VEX statements with analysis state, justification, and expiry (merged with defaults)

Images are continuously mirrored from source to target, then scanned and evaluated against policies. Kubernetes clusters pull only from the target registry.

Policies

Policies use CEL (Common Expression Language). Available variables:

  • criticalCount, highCount, mediumCount, lowCount - Vulnerability counts (excluding VEX-exempted)
  • exemptedCount - Number of VEX-exempted CVEs
  • vulnerabilities - Full vulnerability list with details
  • imageRef - Image reference

Common Policies:

# No critical vulnerabilities
expression: "criticalCount == 0"

# No critical or high
expression: "criticalCount == 0 && highCount == 0"

# Allow up to 5 high
expression: "criticalCount == 0 && highCount <= 5"

# Only block fixable critical vulnerabilities
expression: |
  vulnerabilities.filter(v,
    v.severity == "CRITICAL" &&
    v.fixedVersion != "" &&
    !v.exempted
  ).size() == 0

See Policy Guide for more examples and CEL reference.

API

The API is mainly used for the UI. For details, see the swagger documentation included in the binary at http://localhost:8080/swagger.

Query Endpoints

# Get scan record
GET /api/v1/scans/{digest}

# List scans
GET /api/v1/scans?repository=nginx&limit=10

# Search vulnerabilities
GET /api/v1/vulnerabilities?cve_id=CVE-2024-56171&severity=CRITICAL

# List VEX statements
GET /api/v1/vex

# List inactive/expired VEX statements
GET /api/v1/vex/inactive

# List failed images
GET /api/v1/images/failed

Action Endpoints

# Trigger rescan
POST /api/v1/scans/trigger
{ "digest": "sha256:abc123...", "repository": "nginx" }

# Re-evaluate all policies
POST /api/v1/policy/reevaluate

Observability

# Health check
GET /health

# Prometheus metrics
GET /metrics

Key Metrics:

  • suppline_scans_total - Total scans by status
  • suppline_policy_passed_total - Images passing policy
  • suppline_policy_failed_current{source="registry|runtime"} - Current images failing policy in registry vs runtime
  • suppline_vulnerabilities_total - Vulnerabilities by severity
  • suppline_queue_depth - Current queue depth

Deployment

Docker Compose

This will spin up trivy, regsync, suppline, suppline-ui and registry containers comprising the solution.

docker compose up -d
docker compose logs -f suppline
docker compose down

Kubernetes

edit the values.yaml and values-secrets.yaml (or use env variables) in charts/suppline and substitute your configuration.

Install the solution using helm into your namespace:

helm install --upgrade -f charts/suppline/values.yaml -f charts/suppline/values-secrets.yaml suppline charts/suppline

Standalone

make build
trivy server --listen localhost:4954 &
export SUPPLINE_CONFIG=suppline.yml
export ATTESTATION_KEY=<base64-encoded cosign private key>
export ATTESTATION_KEY_PASSWORD=<cosign password>
./suppline

Development

Setup

make deps
make dev-setup
make build
make test

Project Structure

cmd/suppline/              # Entry point
internal/
  ├── api/                 # HTTP API
  ├── attestation/         # Sigstore integration
  ├── config/              # Config parsing
  ├── policy/              # CEL policy engine
  ├── queue/               # Task queue
  ├── registry/            # OCI registry client
  ├── scanner/             # Trivy integration
  ├── statestore/          # SQLite persistence
  ├── watcher/             # Registry monitoring
  └── worker/              # Pipeline orchestration
test/integration/          # Integration tests
ui/                        # web frontend

Testing

make test                  # Unit tests
make test-integration      # Integration tests
make test-all              # All tests with coverage

Monitoring

Metrics

Prometheus metrics on :9090/metrics:

  • suppline_scans_total - Total scans by status
  • suppline_policy_passed_total - Images passing policy
  • suppline_policy_failed_current{source="registry|runtime"} - Current images failing policy in registry vs runtime
  • suppline_queue_depth - Current queue depth
  • suppline_vulnerabilities_total - Vulnerabilities by severity
  • suppline_scan_duration_seconds - Scan duration histogram

Logging

JSON-formatted structured logs with fields: time, level, msg, digest, repository, critical, high, exempted, etc.

Health

curl http://localhost:8081/health

Returns status of: config, queue, worker, trivy, database, watcher

Troubleshooting

Trivy connection failed

curl http://localhost:4954/healthz
docker compose logs trivy

Authentication errors

# Verify credentials in suppline.yml
cosign login docker.io -u [username] -p [password]
# Verify attestations generated by suppline
cosign verify-attestation --type https://in-toto.io/attestation/scai/attribute-report/v0.3 --key keys/cosign.pub --insecure-ignore-tlog myprivateregistry/alpine:3.22 | jq -r .payload | base64 -d | jq -r
Verification for myprivateregistry/beats_filebeat-oss@sha256:1d2de3fdbbf6494560a65a8d07961082b8b1652732fef839005f3e945f7a01d0 --
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - The signatures were verified against the specified public key
{
  "_type": "https://in-toto.io/Statement/v0.1",
  "predicateType": "https://in-toto.io/attestation/scai/attribute-report/v0.3",
  "subject": [
    {
      "name": "index.docker.io/myprivateregistry/beats_filebeat-oss",
      "digest": {
        "sha256": "1d2de3fdbbf6494560a65a8d07961082b8b1652732fef839005f3e945f7a01d0"
      }
    }
  ],
  "predicate": {
    "attribute": "container-security-assessment",
    "attributes": [
      {
        "attribute": "vex-exempted-vulnerability",
        "evidence": {
          "cveId": "CVE-2021-43527",
          "description": "NSS (Network Security Services) versions prior to 3.73 or 3.68.1 ESR are vulnerable to a heap overflow when handling DER-encoded DSA or RSA-PSS signatures. Applications using NSS for handling signatures encoded within CMS, S/MIME, PKCS \\#7, or PKCS \\#12 are likely to be impacted. Applications using NSS for certificate validation or other TLS, X.509, OCSP or CRL functionality may be impacted, depending on how they configure NSS. *Note: This vulnerability does NOT impact Mozilla Firefox.* However, email clients and PDF viewers that use NSS for signature verification, such as Thunderbird, LibreOffice, Evolution and Evince are believed to be impacted. This vulnerability affects NSS < 3.73 and NSS < 3.68.1.",
          "fixedVersion": "3.67.0-4.el7_9",
          "packageName": "nss",
          "severity": "CRITICAL",
          "state": "not_affected",
          "justification": "vulnerable_code_not_in_execute_path",
          "detail": "DSA/RSA not used",
          "version": "3.53.1-7.el7_9"
        }
      }
    ],
    "evidence": {
      "lastScanned": "2025-11-22T14:49:02.617663494Z",
      "scanStatus": "passed-with-vex-exemptions",
      "validUntil": "2025-11-30T14:49:02.617663494Z"
    },
    "target": {
      "uri": "pkg:docker/myprivateregistry/beats_filebeat-oss@sha256:1d2de3fdbbf6494560a65a8d07961082b8b1652732fef839005f3e945f7a01d0"
    }
  }
}

You can also check the attestations of type cyclonedx and vuln that suppline also generates for each digest.

Database locked

SQLite has limited concurrent write support Use PostgreSQL for high-throughput or multiple instances

Queue filling up

curl http://localhost:8081/health
export WORKER_POLL_INTERVAL=10s
export QUEUE_BUFFER_SIZE=2000

Debug mode

export LOG_LEVEL=debug
./suppline

Security

  • Store Cosign keys in Kubernetes secrets or vault
  • Use SUPPLINE_API_KEY for API authentication in production
  • Never commit registry credentials to version control
  • Enable TLS for Trivy server (TRIVY_INSECURE=false)
  • Use network policies to restrict access in Kubernetes
  • Apply minimal RBAC permissions to service accounts

Integration

MCP server (LLM access)

suppline-mcp is a Model Context Protocol server that wraps the suppline REST API so LLM clients can answer supply-chain questions directly — e.g. "tell me about policy failures for images currently deployed to runtime" or "are there any expired VEX statements?".

Build the binary alongside the main service:

make build-mcp

Registered tools (all read-only by default):

  • list_scans, get_scan, list_failed_images
  • list_vex_statements, list_inactive_vex
  • query_vulnerabilities, get_vulnerability, vulnerability_stats
  • list_repositories, get_repository
  • list_kubernetes_clusters, get_cluster_images

Pass --allow-writes to additionally expose trigger_rescan and reevaluate_policy.

Local use with Cursor / Claude Desktop (stdio transport)

Add an entry to your MCP client config (Cursor: ~/.cursor/mcp.json, Claude Desktop: claude_desktop_config.json):

{
  "mcpServers": {
    "suppline": {
      "command": "/absolute/path/to/suppline-mcp",
      "args": ["--transport", "stdio"],
      "env": {
        "SUPPLINE_URL": "http://localhost:8080",
        "SUPPLINE_API_KEY": "dev-secret-key"
      }
    }
  }
}

The server logs to stderr (so the stdio JSON-RPC channel on stdout stays clean) and expects the suppline REST API to be reachable at SUPPLINE_URL.

Shared / remote use (Streamable HTTP transport)

suppline-mcp \
  --transport http \
  --addr :8082 \
  --mount /mcp \
  --suppline-url http://suppline:8080 \
  --suppline-api-key "$SUPPLINE_API_KEY"

The server exposes:

  • POST /mcp — Streamable HTTP MCP endpoint
  • GET /healthz — health probe

Point an MCP-aware client (Cursor remote MCP, Claude Desktop Streamable HTTP, etc.) at http://<host>:8082/mcp.

Documentation

Support

About

a cloud native supply chain security solution for third party container images

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors