Verifying Image Signatures with Preflight Checks

A question came up this morning about checking image signatures when distributing software through the Replicated Platform. This is something I’ve tackled before with a solution I’m particularly fond of.

While end customers might reach for a Kyverno policy to enforce image signature verification, Kyverno isn’t something you typically ship with commercial software. The real insight here is that for software distribution, what matters most is verifying signatures at the critical moments: installation and upgrade time. This calls for a more lightweight approach, and preflight checks fit perfectly.

Verification with Known Keys

The preflight check is pretty straightforward, differing depending on how you sign your images. For key-based signing, the approach centers on embedding your public key directly in the check and running cosign verification against your target images.

The preflight runs a pod containing cosign and your public key, then attempts verification against the specified images. If verification succeeds, you get a clear “verified” output that the analyzer can parse for a passing result. The security context ensures the validation runs with minimal privileges—no root access, read-only filesystem, and dropped capabilities.

apiVersion: troubleshoot.sh/v1beta2
kind: Preflight
metadata:
  name: cosign-signature-check
spec:
  collectors:
    - runPod:
        name: image-signature
        namespace: default
        podSpec:
          containers:
            - name: validator
              image: nixery.dev/shell/coreutils/openssl/jq/cosign
              imagePullPolicy: IfNotPresent
              env:
                - name: COSIGN_KEY
                  value: |
                    # Replace with your own signing key
                    -----BEGIN PUBLIC KEY-----
                    MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAETPIGSVFx2AgTBcr+bZ8DNAMql4fW
                    kxjqKKsbcy9uQw7HjhsqPnZniQNAUwl1wDkUQ3Y7w6zK4OV9k38DT9bmow==
                    -----END PUBLIC KEY-----
              command:
                - bash
                - -c
                - |
                  set -x
                  echo "${COSIGN_KEY}" > /cosign-keys/cosign.pub
                  if cosign verify --key /cosign-keys/cosign.pub $0 $@ ; then
                    echo "verified"
                  else
                    echo "failed"
                  fi
              args:
                - ttl.sh/shell:1h
                - ttl.sh/cosign:1h
              resources:
                limits:
                  cpu: 500m
                  memory: 256Mi
                requests:
                  cpu: 100m
                  memory: 64Mi
              securityContext:
                allowPrivilegeEscalation: false
                privileged: false
                readOnlyRootFilesystem: true
                runAsNonRoot: true
                runAsUser: 65534
                capabilities:
                  drop:
                    - ALL
              volumeMounts:
                - name: sigstore
                  mountPath: /.sigstore
                  readOnly: false
                - name: cosign-key
                  mountPath: /cosign-keys
                  readOnly: false
          volumes:
            - name: sigstore
              emptyDir: {}
            - name: cosign-key
              emptyDir: {}
  analyzers:
    - textAnalyze:
        checkName: Image Signature Verification
        fileName: image-signature/image-signature.log
        regex: 'verified'
        outcomes:
          - pass:
              when: "true"
              message: Images signed by the expected signature
          - fail:
              when: "false"
              message: Images are not signed by the expected signature

Keyless Signing Takes a Different Path

If you’re using keyless signing, you don’t have a known key to validate against. Instead, you’re verifying against identity certificates tied to OIDC providers. We can’t specify a key, so we specify the exact identity and issuer used during signing.

The preflight adapts by replacing the embedded key with environment variables that define the expected signing identity. The cosign command switches from key-based verification to certificate identity verification, checking both the signer’s identity and the OIDC issuer that authenticated them.

apiVersion: troubleshoot.sh/v1beta2
kind: Preflight
metadata:
  name: cosign-keyless-signature-check
spec:
  collectors:
    - runPod:
        name: image-signature
        namespace: default
        podSpec:
          containers:
            - name: validator
              image: nixery.dev/shell/coreutils/openssl/jq/cosign
              imagePullPolicy: IfNotPresent
              env:
                - name: COSIGN_CERTIFICATE_IDENTITY
                  value: "your-identity@example.com"
                - name: COSIGN_CERTIFICATE_OIDC_ISSUER
                  value: "https://github.com/login/oauth"
              command:
                - bash
                - -c
                - |
                  set -x
                  if cosign verify \
                    --certificate-identity="${COSIGN_CERTIFICATE_IDENTITY}" \
                    --certificate-oidc-issuer="${COSIGN_CERTIFICATE_OIDC_ISSUER}" \
                    $0 $@ ; then
                    echo "verified"
                  else
                    echo "failed"
                  fi
              args:
                - ttl.sh/shell:1h
                - ttl.sh/cosign:1h
              resources:
                limits:
                  cpu: 500m
                  memory: 256Mi
                requests:
                  cpu: 100m
                  memory: 64Mi
              securityContext:
                allowPrivilegeEscalation: false
                privileged: false
                readOnlyRootFilesystem: true
                runAsNonRoot: true
                runAsUser: 65534
                capabilities:
                  drop:
                    - ALL
              volumeMounts:
                - name: sigstore
                  mountPath: /.sigstore
                  readOnly: false
          volumes:
            - name: sigstore
              emptyDir: {}
  analyzers:
    - textAnalyze:
        checkName: Image Signature Verification
        fileName: image-signature/image-signature.log
        regex: 'verified'
        outcomes:
          - pass:
              when: "true"
              message: Images signed by the expected keyless signature
          - fail:
              when: "false"
              message: Images are not signed by the expected keyless signature

Air-gapped Environments Need Special Consideration

As usual, air-gapped deployments introduce their own constraints. Keyless signing becomes impractical since you can’t reach external OIDC providers for identity validation. This means you should sign your image with a known key. You’ll also need to include the signatures themselves in your airgap bundle by adding them as additional images in your Replicated configuration.

Why I Use This Approach

The approach feel elegant in its timing and simplicity. Rather than running continuous policy enforcement, these preflight checks activate precisely when signature verification matters most. Customers get immediate, actionable feedback about image integrity without deploying or managing policy engines in their clusters.

This solution adapts to different signing strategies while integrating seamlessly with your existing preflight checks. Whether you choose keyless signing for its CI/CD advantages or key-based signing for tighter organizational control, you’re delivering the security assurance customers need exactly when they need it. That’s the kind of lightweight, targeted approach that works well in commercial software distribution.