Skip to main content

Image Verification

This guide explains how to verify the integrity and provenance of Surface Security container images before deploying them.

Overview

Surface Security images include three layers of supply chain security:

LayerMethodPurpose
Cosign KeylessSigstore OIDCAutomated CI signature on every build
SBOMCycloneDX (Syft)Software bill of materials for dependency auditing
SLSA ProvenanceBuildKitBuild provenance attestation
Portal Threshold2-of-3 YubiKey Ed25519High-trust release signing ceremony

Prerequisites

# Install cosign
# https://docs.sigstore.dev/cosign/system_config/installation/
brew install cosign # macOS
# or
go install github.com/sigstore/cosign/v2/cmd/cosign@latest

Images

All images are published to GitHub Container Registry:

ImageDescription
ghcr.io/<owner>/surfacesec-apiBackend API server
ghcr.io/<owner>/surfacesec-ingestionEvent ingestion worker
ghcr.io/<owner>/surfacesec-preflightDatabase migration preflight check
ghcr.io/<owner>/surfacesec-frontendNext.js admin dashboard

Verify Cosign Keyless Signature

Every image built by CI is signed using Sigstore keyless signing (OIDC identity from GitHub Actions).

# Verify a specific image version
cosign verify \
--certificate-identity-regexp=".*" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
ghcr.io/<owner>/surfacesec-api:1.2.3

Successful output includes the Rekor transparency log entry and the signing certificate chain.

Download and Inspect SBOM

Each image has a CycloneDX SBOM attached as an OCI artifact.

# Download SBOM
cosign download sbom ghcr.io/<owner>/surfacesec-api:1.2.3 > sbom-api.cdx.json

# Inspect with jq
jq '.components | length' sbom-api.cdx.json
jq '.components[] | .name + "@" + .version' sbom-api.cdx.json

# Scan for vulnerabilities (requires grype)
grype sbom:sbom-api.cdx.json

Verify SLSA Provenance

Images include SLSA provenance attestations generated by BuildKit.

# Verify provenance
cosign verify-attestation \
--type slsaprovenance \
--certificate-identity-regexp=".*" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
ghcr.io/<owner>/surfacesec-api:1.2.3

Verify Portal Threshold Signatures

For official releases, images are additionally signed through a 2-of-3 YubiKey threshold ceremony via the Surface Portal.

Connected Verification

If your deployment can reach the Portal, the backend automatically fetches verification keys and verifies update bundle signatures during the update process. No manual action is needed.

Air-Gapped Verification

For air-gapped deployments, verification keys are embedded in the backend binary at build time.

  1. Verify manually with surface-sign CLI:

    # Download the signing manifest for your release version
    # (available as a GitHub Actions artifact on the release-sign workflow run)

    # Verify the manifest signatures
    surface-sign verify \
    --manifest signing-manifest-1.2.3.json \
    --keys verification-keys.json \
    --threshold 2
  2. Verify image digests match:

    # Compare the digest in the signing manifest with the actual image
    docker inspect --format='{{index .RepoDigests 0}}' surfacesec-api:1.2.3

Air-Gapped Image Transfer

For environments without internet access:

# On a connected machine: pull, verify, and save
VERSION=1.2.3
IMAGES="api ingestion preflight frontend"

for img in $IMAGES; do
# Verify before saving
cosign verify \
--certificate-identity-regexp=".*" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
"ghcr.io/<owner>/surfacesec-${img}:${VERSION}"

# Download SBOM for compliance records
cosign download sbom "ghcr.io/<owner>/surfacesec-${img}:${VERSION}" \
> "sbom-${img}.cdx.json"
done

# Save all images to a tarball
docker save \
ghcr.io/<owner>/surfacesec-api:${VERSION} \
ghcr.io/<owner>/surfacesec-ingestion:${VERSION} \
ghcr.io/<owner>/surfacesec-preflight:${VERSION} \
ghcr.io/<owner>/surfacesec-frontend:${VERSION} \
| gzip > surfacesec-images-${VERSION}.tar.gz

# Generate digest file for verification after transfer
sha256sum surfacesec-images-${VERSION}.tar.gz > surfacesec-images-${VERSION}.sha256

# Transfer tarball + sha256 + SBOMs to air-gapped environment
# On the air-gapped machine: verify and load
sha256sum -c surfacesec-images-${VERSION}.sha256
docker load < surfacesec-images-${VERSION}.tar.gz

# Tag and push to local registry
for img in api ingestion preflight frontend; do
docker tag "ghcr.io/<owner>/surfacesec-${img}:${VERSION}" \
"registry.internal/surfacesec/${img}:${VERSION}"
docker push "registry.internal/surfacesec/${img}:${VERSION}"
done

Verification Key Lifecycle

EventAction
First boot (air-gapped)Embedded keys are loaded from the binary
First Portal contactKeys fetched from GET /api/v1/deployment/verification-keys
Key rotationNew keys must be signed by 2-of-3 existing keyholders (version monotonicity enforced)
Manual overridePlace a verification-keys.json file in the data directory