Webhook icon
Commands icon
Docker icon
If icon
UploadFiles icon
Apply icon
Log icon
Fail icon

Deploy only container images with a verified Sigstore signature

Verify image signatures with cosign before deploy in Kestra. Allow only trusted signers, pin images to verified digests, block unsigned images.

Categories
CloudInfrastructureinfrastructure

Signing images only helps if something checks the signature before the image runs. This flow verifies every image of a release with cosign against the identity that is allowed to sign it, for example your release workflow on GitHub Actions. An unsigned image, or one signed by anyone else, blocks the whole release.

Each verified image is then pinned to the digest that was verified. The Deployment it renders references repo@sha256:..., never the tag, so a tag that is moved after the check cannot change what runs.

It runs with no setup against two public Chainguard images, which are signed keylessly by Chainguard's release workflow. Run it with add_unsigned_image: true to add docker.io/library/busybox:1.36, which carries no signature, and watch the release get blocked.

This blueprint was created by zkasuran.

How it works

  1. verify_signatures (io.kestra.plugin.scripts.shell.Commands, alpine:3.20) installs cosign v3.1.3 and checks the binary's SHA-256. For each image it runs cosign verify --certificate-identity-regexp --certificate-oidc-issuer, then records:
    • verified: the pinned repo@digest and the signer,
    • rejected: the reason, such as no signatures found or none of the expected identities matched. It outputs checked, verified, rejected and pinned.
  2. gate (io.kestra.plugin.core.flow.If) passes only when every image verified.
    • render_manifest writes a Deployment with the pinned digests. publish_manifest (io.kestra.plugin.core.namespace.UploadFiles) stores it in deploy/approved/<app>.yaml as the record of what was allowed to ship.
    • With apply_to_cluster: true, kubectl_apply (io.kestra.plugin.kubernetes.kubectl.Apply) applies it.
    • Otherwise block_release (io.kestra.plugin.core.execution.Fail) names every rejected image and why. No manifest is published.
  3. on_release (io.kestra.plugin.core.trigger.Webhook) lets CI call the gate before a rollout.

Inputs

  • images (ARRAY): image references, tags allowed.
  • add_unsigned_image (BOOL, default false): demo of the blocked path.
  • certificate_identity_regexp (STRING): the allowed signer. For your own GitHub Actions release: ^https://github.com/<org>/<repo>/.github/workflows/release.yaml@refs/tags/.
  • certificate_oidc_issuer (STRING, default https://token.actions.githubusercontent.com).
  • app_name (STRING, default demo-app), apply_to_cluster (BOOL, default false), k8s_namespace (STRING, default default).

Prerequisites

  • A Kestra worker that can run Docker containers and reach GitHub releases, the image registries and the Sigstore services.
  • For apply_to_cluster, Kubernetes credentials available to the worker (in-cluster service account or kubeconfig).

Quick start

  1. Run it. Both Chainguard images verify and deploy/approved/demo-app.yaml holds the digest-pinned Deployment.
  2. Run it with add_unsigned_image: true. The run fails with docker.io/library/busybox:1.36 (no signatures found).
  3. Run it with certificate_identity_regexp set to ^https://github.com/acme/. Both images are signed, but not by acme, so both are rejected.
  4. Replace the images and the identity with yours, then call the webhook from CI.

Expected outputs

  • outputs.verify_signatures.vars: checked, verified, rejected, pinned, results.
  • Namespace file deploy/approved/<app_name>.yaml.

Things to know

  • The cosign binary is the linux-amd64 release. On arm64 workers, change the URL and the checksum to cosign-linux-arm64.
  • Key-based signatures work too: replace the identity flags with --key and a secret.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.