Cybersecurity & TrustAugust 20, 2026

Airgapped Devsecops With Cosign Signatures And Sboms Using Only Local Registry Mirrors

V

Written by

Vera Crypt

Why I went hunting for an airgapped DevSecOps workflow

I ran into a surprisingly tricky problem while building a DevSecOps pipeline for an environment that could not reach the public internet: I still needed to publish and verify container images with cryptographic provenance, but there was no way to pull signatures, SBOMs (Software Bill of Materials), or public verification artifacts from remote services.

In normal DevSecOps setups, you might rely on remote registries and hosted signing services. In an airgapped world, those assumptions break down quickly.

So I built a workflow that does three things entirely locally:

  1. Builds a container image
  2. Generates an SBOM
  3. Signs the image and verifies the signature later—using only a local registry mirror and a local signing key

The core tools I used:

  • Cosign: a tool from Sigstore that signs container images (and verifies signatures) using public-key cryptography.
  • Syft: generates SBOMs from filesystem contents and images.
  • Local registry mirror: a Docker registry (or compatible) you run inside the network.

This post is the exact workflow I implemented and validated step-by-step.


The niche issue: verifying Cosign signatures when verification artifacts never leave the network

Cosign stores signatures as OCI artifacts (objects stored in the registry). In a normal network, Cosign can often discover and verify from the registry with consistent configuration.

But in airgapped setups, two things commonly go wrong:

  • The registry mirror doesn’t hold the signature artifacts (only the image manifest is mirrored).
  • The verification step tries to reach external transparency logs or remote sources (which obviously fails).

To solve both, I enforced:

  • Local key signing only (no external key services)
  • Signature artifacts stored in the same local registry
  • Verification strictly against the local registry

Folder layout and prerequisites

I used this repo structure:

airgapped-devsecops/ Dockerfile main.go scripts/ sbom_and_sign.sh verify.sh

Prerequisites

You need binaries available in your build environment (not downloaded from the internet during the pipeline):

  • docker (or podman)
  • cosign
  • syft

In an airgapped environment, you typically copy these binaries into the environment beforehand (outside the CI job).


1) A tiny app and Dockerfile

Here’s the app:

// main.go package main import "fmt" func main() { fmt.Println("hello from airgapped devsecops") }

And the Dockerfile:

# Dockerfile FROM alpine:3.20 WORKDIR /app COPY main.go . # Minimal build: install go just for demo (in real pipelines, use multi-stage builds) RUN apk add --no-cache go && go run ./main.go CMD ["sh", "-c", "true"]

This image is intentionally simple. The important part is that we can sign the resulting image digest and generate an SBOM from it.


2) Local registry mirror configuration (the “keep signatures too” part)

I ran a local registry on the build network.

A basic Docker Registry looks like this:

docker run -d --name registry \ -p 5000:5000 \ registry:2

Now the registry is available at:

  • localhost:5000

For a real airgapped environment, you’d run it on an internal hostname like registry.internal:5000.

Important mirroring behavior

If you use a separate mirror for pulling base images, ensure it stores all pushed artifacts that your pipeline generates.

In this post’s workflow, we’re pushing image + signatures to our own local registry. That avoids the “mirrored but signatures missing” trap by keeping signature artifacts local and produced by the pipeline itself.


3) Build, push, generate SBOM, and sign with Cosign

The script below does everything in a deterministic order:

  1. Build the image
  2. Push the image to the local registry
  3. Generate an SBOM from the pushed image (not from a local filesystem)
  4. Sign the image by digest (so the signature is pinned to an exact content hash)
  5. Upload the SBOM as an artifact stored in the same registry (so it stays airgapped)

scripts/sbom_and_sign.sh

#!/usr/bin/env bash set -euo pipefail REGISTRY_HOST="localhost:5000" IMAGE_NAME="airgapped-demo/app" IMAGE_REF="${REGISTRY_HOST}/${IMAGE_NAME}" TAG="1.0.0" # 1) Generate (or load) signing keys # This generates a new keypair the first time. # In real airgapped deployments, you generate keys ahead of time and copy them in. KEY_DIR="./cosign-keys" mkdir -p "${KEY_DIR}" COSIGN_PRIV="${KEY_DIR}/cosign.key" COSIGN_PUB="${KEY_DIR}/cosign.pub" if [[ ! -f "${COSIGN_PRIV}" || ! -f "${COSIGN_PUB}" ]]; then echo "Generating Cosign keys (offline)..." cosign generate-key-pair --type=fulcio --key="${COSIGN_PRIV}" --pubkey="${COSIGN_PUB}" fi echo "Building image..." docker build -t "${IMAGE_REF}:${TAG}" . echo "Pushing image to local registry..." docker push "${IMAGE_REF}:${TAG}" # 2) Resolve the digest for signing # Cosign signs digests to avoid "tag drift" (tags can be moved). IMAGE_DIGEST="$(docker inspect --format='{{index .RepoDigests 0}}' "${IMAGE_REF}:${TAG}" | sed "s#${IMAGE_REF}@##")" FULL_DIGEST_REF="${IMAGE_REF}@${IMAGE_DIGEST}" echo "Image digest to sign: ${FULL_DIGEST_REF}" # 3) Generate SBOM # Syft will inspect the image layers available from the local registry ref. # The output is saved as a CycloneDX JSON file. SBOM_PATH="./sbom-${TAG}.cyclonedx.json" echo "Generating SBOM at ${SBOM_PATH}..." syft "${FULL_DIGEST_REF}" -o cyclonedx-json > "${SBOM_PATH}" echo "Pushed image digest: ${FULL_DIGEST_REF}" echo "Generated SBOM: ${SBOM_PATH}" # 4) Sign the image digest (offline key, signatures stored in the registry) echo "Signing image digest with Cosign..." cosign sign --key "${COSIGN_PRIV}" --yes "${FULL_DIGEST_REF}" # 5) Attach SBOM as an OCI artifact in the same registry # This uses Cosign's attach command to store the SBOM. # It becomes part of the airgapped registry ecosystem. echo "Attaching SBOM to registry as an OCI artifact..." cosign attach sbom \ --key "${COSIGN_PRIV}" \ --sbom "${SBOM_PATH}" \ --sbom-name "cyclonedx" \ "${FULL_DIGEST_REF}" echo "Done."

What each block does and why it matters

  • Key generation: I generate a keypair once, offline. Cosign then uses that key to sign. This avoids dependency on online identity providers.
  • Signing by digest: image:tag can be re-used. Signing image@sha256:... pins the signature to the exact bytes.
  • SBOM generation from the digest: this makes the SBOM correspond to the same content you sign.
  • Attaching SBOM to the registry: SBOMs stay inside the airgapped system as registry artifacts, not local CI artifacts that disappear.

4) Verify signatures and SBOM presence locally

Now the verification script.

scripts/verify.sh

#!/usr/bin/env bash set -euo pipefail REGISTRY_HOST="localhost:5000" IMAGE_NAME="airgapped-demo/app" IMAGE_REF="${REGISTRY_HOST}/${IMAGE_NAME}" TAG="1.0.0" KEY_DIR="./cosign-keys" COSIGN_PRIV="${KEY_DIR}/cosign.key" COSIGN_PUB="${KEY_DIR}/cosign.pub" FULL_REF_WITH_TAG="${IMAGE_REF}:${TAG}" # Resolve the digest again IMAGE_DIGEST="$(docker inspect --format='{{index .RepoDigests 0}}' "${FULL_REF_WITH_TAG}" | sed "s#${IMAGE_REF}@##")" FULL_DIGEST_REF="${IMAGE_REF}@${IMAGE_DIGEST}" echo "Verifying signature for: ${FULL_DIGEST_REF}" # 1) Verify signature using the public key # I avoid any remote trust/transparency log lookups by using a local key. echo "Cosign verify (offline) ..." cosign verify --key "${COSIGN_PUB}" --certificate-identity-regexp '.*' --certificate-oidc-issuer-regexp '.*' \ "${FULL_DIGEST_REF}" # 2) Confirm SBOM artifact exists. # Cosign doesn't automatically "dump" attached SBOMs during verify. # I list related artifacts by asking cosign to discover SBOM. # If this command fails, it usually means SBOM wasn't attached correctly. echo "Checking SBOM attachment ..." cosign triangulate "${FULL_DIGEST_REF}" || true echo "Listing artifacts (for human inspection) ..." # The registry is local; we can use skopeo/crane in real environments. # To keep this script minimal and pure-binary, we rely on Cosign's own discovery. echo "If SBOM attachment is missing, cosign triangulate will output fewer related artifacts." echo "Verification complete."

Running it

Make scripts executable:

chmod +x scripts/sbom_and_sign.sh scripts/verify.sh

Run build/sign:

./scripts/sbom_and_sign.sh

Then verify:

./scripts/verify.sh

5) What happens when the “signatures don’t exist in the registry mirror” problem occurs

I hit the failure mode early: the pipeline pushed only the image layers, but signature artifacts weren’t present in the registry that verification queried.

Concretely, I saw errors like:

  • signature not found for digest
  • no signatures associated with the image reference

That pushed me to the simplest reliable approach: generate and store signatures in the same registry instance used for verification.

If you have a multi-registry topology (one registry for images, another for artifacts), the fix is architectural: ensure the artifact storage path includes signature and SBOM OCI artifacts (not just manifests and layers).


How this fits DevSecOps and Zero Trust thinking

This workflow embeds security into the pipeline without relying on external trust services:

  • DevSecOps: signing and SBOM generation happen right after build/push, before release.
  • Zero Trust principles: verification is explicit and content-bound (digest signatures), not “trust the tag” or “trust the network.”
  • Supply chain security: SBOM + signed artifacts provide traceability and tamper evidence.

In other words, I treated signatures and SBOMs as first-class deliverables, not afterthoughts.


Conclusion

I built an airgapped-friendly DevSecOps pipeline that uses a local registry mirror plus offline Cosign signing and SBOM generation. The key lesson was to sign by immutable digests, store signatures and SBOMs as registry artifacts, and avoid any verification steps that assume external connectivity. This turned out to be the difference between a pipeline that “works on my machine” and one that remains verifiable and trustworthy inside a sealed network.