Skip to content

Verify signed releases

How-to — check that a release artifact really came from this project. Assumes cosign is installed.

Every release from v1.2.0 onwards is signed: the container image, the Helm chart OCI artifact, and a checksums.txt bundle on the GitHub Release. Signing is keyless, so verification needs no key from the maintainer — only the commands below.

Prerequisites

  • cosign installed
  • The version tag you want to verify

What is signed

Artifact What it is
Container image The multi-architecture (amd64/arm64) manifest list pushed to ghcr.io/przemekhys/homeassistant-operator
Helm chart The chart packaged and pushed as an OCI artifact to oci://ghcr.io/przemekhys/charts/homeassistant-operator
checksums.txt A text file listing the image and chart digests for the release, attached to the GitHub Release along with its own signature

All three are signed keyless, using Sigstore/cosign, bound to this repository's own GitHub Actions release workflow identity. There is no long-lived signing key anywhere — the maintainer never generates, stores, or rotates one. Each signature is backed by a short-lived certificate from Sigstore's Fulcio and a public transparency-log entry in Rekor, which is what the verification commands below check against.

A note on version tags

Two different tags name the same release, and mixing them up is the usual reason a verification command fails:

What Tag Example
Git tag and GitHub Release with v v1.4.0
Container image and Helm chart in the registry without v 1.4.0

The release pipeline strips the v when it publishes to the registry, so ghcr.io/przemekhys/homeassistant-operator:v1.4.0 does not exist — only :1.4.0 does.

Verify everything at once

The quickest route: checksums.txt lists the digests of both artifacts and is itself signed, so verifying it once gives you trustworthy digests for the rest.

VERSION=v1.4.0
IDENTITY='https://github.com/przemekhys/homeassistant-operator/\.github/workflows/release\.yml@refs/tags/.*'
ISSUER=https://token.actions.githubusercontent.com

gh release download "$VERSION" \
  --repo przemekhys/homeassistant-operator \
  -p 'checksums.txt*'

cosign verify-blob \
  --bundle checksums.txt.sigstore.json \
  --certificate-identity-regexp "$IDENTITY" \
  --certificate-oidc-issuer "$ISSUER" \
  checksums.txt

Expected output: Verified OK.

--repo matters: without it gh looks at the git remote of the directory you are standing in, so the command only works inside a clone. Verification should not require one.

The verified file names both digests:

cat checksums.txt
sha256:2e0b7dd8...  container-image  ghcr.io/przemekhys/homeassistant-operator
sha256:147bee7a...  helm-chart       oci://ghcr.io/przemekhys/charts/homeassistant-operator

Verify the container image

Take the digest from the file you just verified — no separate tooling needed:

IMAGE=ghcr.io/przemekhys/homeassistant-operator
DIGEST=$(awk '$2=="container-image" {print $1}' checksums.txt)

cosign verify \
  --certificate-identity-regexp "$IDENTITY" \
  --certificate-oidc-issuer "$ISSUER" \
  "$IMAGE@$DIGEST"

To resolve the digest from the tag yourself instead — which also checks that the tag still points where you expect — use crane:

DIGEST=$(crane digest "$IMAGE:${VERSION#v}")   # note: ${VERSION#v}, the registry tag has no "v"

Verify the Helm chart

CHART=ghcr.io/przemekhys/charts/homeassistant-operator
CHART_DIGEST=$(awk '$2=="helm-chart" {print $1}' checksums.txt)

cosign verify \
  --certificate-identity-regexp "$IDENTITY" \
  --certificate-oidc-issuer "$ISSUER" \
  "$CHART@$CHART_DIGEST"

Note that the reference passed to cosign has no oci:// prefix, even though helm wants one when installing the same artifact.

This check needs no cluster and no Kyverno — anywhere cosign can reach the registry will do, which makes it a natural preflight step in a GitOps pipeline before helm install/upgrade ever runs.

All of the above in one command

hack/verify-signatures.sh runs the same three checks: hack/verify-signatures.sh v1.4.0. It needs cosign, gh and crane on your PATH.

Verify with Kyverno

Not part of the supported API

Kyverno is a third-party admission controller that this project does not ship or control. The policy below reflects the state at the time of writing and may need adjusting for a different Kyverno version.

Tested with: Kyverno 1.13.

Cluster operators can enforce this automatically at admission time with Kyverno, so an unsigned or tampered image is rejected before it ever runs.

Minimum Kyverno version

This sample was validated against the classic kyverno.io/v1 ClusterPolicy API, supported since Kyverno 1.9. It intentionally does not use the newer CEL-based ImageValidatingPolicy (Kyverno 1.14+) so it works on a broader range of clusters.

The full sample lives at hack/kyverno/verify-homeassistant-operator-image.yaml:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-homeassistant-operator-image
  annotations:
    policies.kyverno.io/title: Verify homeassistant-operator image signature
    policies.kyverno.io/category: Software Supply Chain
    policies.kyverno.io/severity: high
    policies.kyverno.io/description: >-
      Requires that any ghcr.io/przemekhys/homeassistant-operator image was signed
      by the project's own GitHub Actions release workflow (keyless Sigstore
      signing), rejecting unsigned or tampered images at admission time.
spec:
  validationFailureAction: Enforce
  webhookTimeoutSeconds: 30
  rules:
    - name: verify-signature
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - "ghcr.io/przemekhys/homeassistant-operator*"
          attestors:
            - count: 1
              entries:
                - keyless:
                    issuer: "https://token.actions.githubusercontent.com"
                    subject: "https://github.com/przemekhys/homeassistant-operator/.github/workflows/release.yml@refs/tags/*"
                    rekor:
                      url: https://rekor.sigstore.dev

Apply it, then try both a genuine and a tampered image:

kubectl apply -f hack/kyverno/verify-homeassistant-operator-image.yaml

# Genuine image: admitted.
kubectl run ha-genuine --image=ghcr.io/przemekhys/homeassistant-operator:v1.2.0 --restart=Never

# Tampered/re-tagged image: rejected by the admission webhook.
kubectl run ha-tampered --image=<attacker-controlled-retag> --restart=Never

Roll out with Audit first

The sample defaults to validationFailureAction: Enforce. For a first adoption in an existing cluster, consider switching it to Audit (log-only) to confirm it matches as expected before enforcing rejections.

Keep this policy in sync if the signing identity changes

The issuer/subject values above are pinned to this repository's release workflow file path and tag-ref pattern. If that workflow is ever renamed or moved, both this doc page and the policy file must be updated together in the same change — otherwise the policy would silently stop matching new releases instead of failing loudly.