Supply chain attacks used to be a large-enterprise problem: a compromised build dependency at a scale few smaller shops matched. NIS2 changes who is actually on the hook. The directive extends security obligations down the supplier chain, which means a mid-size company’s IT provider, and often that provider’s own suppliers, inherit requirements that used to stop at large regulated firms. “We’re too small to be a target” was never quite true; it is now also not a valid compliance answer. The practical response fits inside the pipeline you already run: know what is in your images, gate on known vulnerabilities, and prove where an image came from before it runs.

Scanning as a pipeline gate, not an afterthought

A vulnerability scanner reads an image’s layers, matches installed packages against CVE databases, and reports what it finds. Trivy and Grype are the two open-source references; both run as a CI step and both can fail the build past a severity threshold:

# GitHub Actions step
- name: Scan image
  uses: aquasecurity/trivy-action@0.24.0
  with:
    image-ref: registry.example.com/app:${{ github.sha }}
    severity: CRITICAL,HIGH
    exit-code: '1'   # fails the job if matching CVEs are found

The gate is only as useful as its threshold discipline. Failing the build on every medium-severity finding on day one buries the team in noise and teaches everyone to ignore the scanner; start at critical, add high once the backlog of existing findings is triaged, and treat a scan exemption the same way you would treat a disabled test: documented, time-boxed, and revisited. The scan result is also only as fresh as the last run: an image built once and deployed for months carries whatever CVEs get published against its packages after the fact, which is why re-scanning images already sitting in the registry on a schedule matters as much as scanning at build time.

SBOM: knowing what’s inside without re-scanning from scratch

A Software Bill of Materials is the inventory a scanner already computes, kept as an artifact instead of discarded after the scan:

syft registry.example.com/app:v1.4.2 -o cyclonedx-json > sbom.json

The SBOM’s value shows up later, not at build time: when a new CVE is published against a library, checking exposure across every image you have ever shipped is a query over stored SBOMs, not a re-scan of every historical artifact you may not even be able to rebuild anymore. Attach it to the image as an OCI artifact alongside the build, and the inventory travels with the thing it describes.

Keyless signing: proving where an image came from

Scanning tells you what is inside an image. Signing tells you the image is the one your pipeline actually produced, not a same-named image pushed by someone else with registry write access. The traditional approach, a long-lived private signing key, just relocates the key-management problem scanning was supposed to sidestep: a leaked signing key trusted by every consumer is a bigger liability than the artifact it protects.

Sigstore’s cosign in keyless mode removes the stored key entirely, using the same OIDC federation mechanic that replaces static cloud credentials in a pipeline: the CI job proves its identity via a short-lived OIDC token, an ephemeral keypair is generated for that signing operation alone, and a public transparency log (Rekor) permanently records the signature and the identity that produced it.

# from CI, with the OIDC-issued identity token already available
cosign sign --yes registry.example.com/app@sha256:1e9c...a41f

No private key to store, rotate, or leak, because none persists past the signing operation. Verifying is a lookup against the transparency log and the recorded identity, not a shared secret:

cosign verify \
  --certificate-identity="https://github.com/acme/app/.github/workflows/build.yml@refs/heads/main" \
  --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
  registry.example.com/app@sha256:1e9c...a41f

Verifying at admission: the gate that actually matters

A signature nobody checks protects nothing. policy-controller (Sigstore’s own admission webhook) or Kyverno’s verifyImages rule can require a valid, matching signature before the Kubernetes API server admits a pod at all:

# Kyverno ClusterPolicy, abbreviated
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signatures
spec:
  rules:
    - name: check-signature
      match:
        resources:
          kinds: [Pod]
      verifyImages:
        - imageReferences: ["registry.example.com/*"]
          attestors:
            - entries:
                - keyless:
                    issuer: "https://token.actions.githubusercontent.com"
                    subject: "https://github.com/acme/app/.github/workflows/build.yml@refs/heads/main"

An unsigned image, or one signed by an identity the policy does not name, is rejected before a single container starts, which closes the loop the scan and the signature only opened: what got scanned is what got signed, and only what got signed actually runs.

Where this fits

None of this replaces network-level cluster security; a default-deny NetworkPolicy still governs what a running pod can reach, regardless of how well its image was vetted before admission. Supply chain controls answer an earlier question: is this the thing we meant to run. Any pipeline that builds and publishes to a container registry, this site’s own deployment workflow included, which pushes to GHCR, is exactly the kind of setup these controls are built for. Wiring scanning, SBOMs and signature verification into a pipeline that was not designed with them from day one is routine work inside industrializing a CI/CD chain, and increasingly not optional for anyone selling into a regulated supply chain.