Cosign is a powerful tool from the Sigstore project that solves the critical challenge of ensuring container image authenticity and integrity in software supply chains.

Cosign supports different ways to sign images, including: - keyless signing through Sigstore’s Fulcio Certificate Authority - signing with our keys - KMS signing - hardware signing

This article illustrates the signing and verification using public/private key pairs and keyless sign-in methods.

Installation

We can install Cosign in several ways, as detailed in the documentation. Below are a couple of installation methods.


Using Homebrew on macOS or Linux:

brew install cosign

Installing from binary on Linux:

curl -O -L "https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64"
sudo mv cosign-linux-amd64 /usr/local/bin/cosign
sudo chmod +x /usr/local/bin/cosign

Key-Based signing

In this section, we’ll create and use a public/private key pair for the signing and verification steps.

Generate signing keys

First, we create a public/private key pair for signing.

cosign generate-key-pair

This command generates two files:


  • cosign.key: Private key
  • cosign.pub: Public key


Next, we’ll use the private key to sign an image and the public key to verify the signature.

Signing an image

In this step, we’ll sign the lucj/btcprice DockerHub image. This image packages a demo application that regularly gets the BTC USD price and sends it to various external systems (HTTPS webhooks, MQTT, MATS). Several tags currently exist for this image, including:


  • 0.0.4 (sha256:7b08c75f45d82bedfcd1a06d970b663203b77092c84280f07f6eba87a3c843f5)
  • 0.0.5 (sha256:e14812c2d6d827a9402b8109c65e024fcd04cb4f998d59329e816a9c908a4e23)

Let’s sign the image using the sha256 associated with the 0.0.5 tag.

cosign sign --key cosign.key lucj/btcprice@sha256:e14812c2d6d827a9402b8109c65e024fcd04cb4f998d59329e816a9c908a4e23

Note

We need to have write access to the registry, in this case, the DockerHub, as the signature is sent to it.

This command creates a signature and pushes it alongside the image in the DockerHub.


Signature in DockerHub

Enforcing the usage of the signed image

In this step, we’ll use Kyverno, a policy engine for Kubernetes and use it to ensure that only the signed images can run on our cluster.


Note

We can use other alternatives like OPA/GateKeeper for the signature verification step.

First, we install Kyverno using Helm.

helm repo add kyverno https://kyverno.github.io/kyverno/
helm install kyverno kyverno/kyverno --version 3.3.4 -n kyverno --create-namespace

Next, we define a Kyverno ClusterPolicy (CRD installed by Kyverno) that prevents an image from being pulled from lucj/btcprice unless it has a valid signature. This resource contains the Cosign public key created previously, so Kyverno can verify the signature.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: check-image
spec:
  webhookConfiguration:
    failurePolicy: Fail
    timeoutSeconds: 30
  background: false
  rules:
    - name: check-image
      match:
        any:
        - resources:
            kinds:
              - Pod
      verifyImages:
      - imageReferences:
        - "docker.io/lucj/btcprice*"
        failureAction: Enforce
        attestors:
        - count: 1
          entries:
          - keys:
              publicKeys: |-
                -----BEGIN PUBLIC KEY-----
                MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE+mMILfgAYx3OxsGNnBBIS71ByNhj
                wjquvzRPDJkHfRiw9ibxxDMfy2wyXuBg/ALryPjL4YfSemr9WCrl8u4RKg==
                -----END PUBLIC KEY-----

Then, we create this ClusterPolicy.

kubectl apply -f check-signature.yaml

Verification

In order to test our configuration, we’ll first create a Deployment based on the image lucj/btcprice@sha256:7b08c75f45d82bedfcd1a06d970b663203b77092c84280f07f6eba87a3c843f5. This tag is the sha256 representation of the image lucj/btcprice:0.0.4 which we did not sign.

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: btcprice
  name: btcprice
spec:
  selector:
    matchLabels:
      app: btcprice
  template:
    metadata:
      labels:
        app: btcprice
    spec:
      containers:
      - image: lucj/btcprice@sha256:7b08c75f45d82bedfcd1a06d970b663203b77092c84280f07f6eba87a3c843f5
        name: btcprice
        env:
        - name: WEBHOOK_ENABLED
          value: "true"

As the error message below explains, Kyverno prevents the creation of the image as it is not signed.

$ kubectl apply -f btcprice.yaml
Error from server: error when creating "btcprice.yaml": admission webhook "mutate.kyverno.svc-fail" denied the request:

resource Deployment/default/btcprice was blocked due to the following policies

check-image:
  autogen-check-image: 'failed to verify image docker.io/lucj/btcprice@sha256:7b08c75f45d82bedfcd1a06d970b663203b77092c84280f07f6eba87a3c843f5:
 .attestors[0].entries[0].keys: no signatures found'

Let’s change the tag to the one corresponding to the signed image.

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: btcprice
  name: btcprice
spec:
  selector:
    matchLabels:
      app: btcprice
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: btcprice
    spec:
      containers:
      - image: lucj/btcprice@sha256:e14812c2d6d827a9402b8109c65e024fcd04cb4f998d59329e816a9c908a4e23
        name: btcprice
        env:
        - name: WEBHOOK_ENABLED
          value: "true"

Next, we create the Deployment.

$ kubectl apply -f btcprice.yaml
deployment.apps/btcprice created

The Deployment was correctly created and the associated Pod is running.

$ kubectl get po
NAME                        READY   STATUS    RESTARTS   AGE
btcprice-7476b66687-rtvzl   1/1     Running   0          3s

We can verify the application sends BTC price to a default backend as illustrated below.


BTC price in webhooks

Keyless signing

A recommended practice is keyless signing. This process generates an ephemeral key pair and uses it to create a digital signature for a given artifact.

Signing an image

The following commands use this method to sign the image corresponding to the tag 0.0.4.

cosign sign lucj/btcprice@sha256:7b08c75f45d82bedfcd1a06d970b663203b77092c84280f07f6eba87a3c843f5

Since we are using keyless signing, Cosign generates ephemeral keys based on user information, as shown below. Depending on the authentication method, this information can contain the user’s email address.

Generating ephemeral keys...
Retrieving signed certificate...

        The sigstore service, hosted by sigstore a Series of LF Projects, LLC, is provided pursuant to the Hosted Project Tools Terms of Use, available at https://lfprojects.org/policies/hosted-project-tools-terms-of-use/.
        Note that if your submission includes personal data associated with this signed artifact, it will be part of an immutable record.
        This may include the email address associated with the account with which you authenticate your contractual Agreement.
        This information will be used for signing this artifact and will be stored in public transparency logs and cannot be removed later, and is subject to the Immutable Record notice at https://lfprojects.org/policies/hosted-project-tools-immutable-records/.

By typing 'y', you attest that (1) you are not submitting the personal data of any other person; and (2) you understand and agree to the statement and the Agreement terms at the URLs listed above.
Are you sure you would like to continue? [y/N] 

First, we accept the conditions to authenticate using one of the available OIDC providers.


keyless 1


Next, we use GitHub authentication and need to authorize Sigstore to access our account.


keyless 2


Then, we get a successful authentication message.


keyless 3


From the DockerHub, we can verify that this process created a new signature related to the tag we specified.


keyless 4

Verification

Instead of using Kyverno to verify the signature, as we did in the previous step, we can directly verify it using Cosign’s verify command. This one requires the email address associated with the GitHub account used during the authentication step.

cosign verify \
 --certificate-identity EMAIL_ADDRESS \
    --certificate-oidc-issuer https://github.com/login/oauth \
 lucj/btcprice@sha256:7b08c75f45d82bedfcd1a06d970b663203b77092c84280f07f6eba87a3c843f5

Upon successful signature verification, this command returns a result showing the signature was correctly verified.

Verification for index.docker.io/lucj/btcprice@sha256:7b08c75f45d82bedfcd1a06d970b663203b77092c84280f07f6eba87a3c843f5 --
The following checks were performed on each of these signatures:
  - The cosign claims were validated
  - Existence of the claims in the transparency log was verified offline
  - The code-signing certificate was verified using trusted certificate authority certificates

[
  {
    "critical": {
      "identity": {
        "docker-reference": "index.docker.io/lucj/btcprice"
      },
      "image": {
        "docker-manifest-digest": "sha256:7b08c75f45d82bedfcd1a06d970b663203b77092c84280f07f6eba87a3c843f5"
      },
      "type": "cosign container image signature"
    },
    "optional": {
      "1.3.6.1.4.1.57264.1.1": "https://github.com/login/oauth",
      "Bundle": {
        "SignedEntryTimestamp": "MEQCIAsD2...6LIO3Jg==",
        "Payload": {
          "body": "eyJhc...19fX0=",
          "integratedTime": 1739900351,
          "logIndex": 172203215,
          "logID": "c0d23...801d"
        }
      },
      "Issuer": "https://github.com/login/oauth",
      "Subject": "EMAIL_ADDRESS"
    }
  }
]

Using the verify command, we can check the signature of the first image we signed. This time, we get an error as this image was not signed using keyless signing method but with dedicated keys instead.

$ cosign verify \
 --certificate-identity EMAIL_ADDRESS \
    --certificate-oidc-issuer https://github.com/login/oauth \
 lucj/btcprice@sha256:e14812c2d6d827a9402b8109c65e024fcd04cb4f998d59329e816a9c908a4e23
Error: no matching signatures: error verifying bundle: nil certificate provided
main.go:69: error during command execution: no matching signatures: error verifying bundle: nil certificate provided

Key takeaways

Cosign, from the Sigstore project, is straightforward to use yet very powerful. In this article, we illustrated the usage of cosign with both a generated key pair and the keyless signature method. Once signed, the signature verification can be done using cosign itself or third-party tools, like Kyverno or OPA/GateKeeper, making it easy to integrate the signature verification in a CI/CD pipeline.


Feel free to explore Cosign feature sets in the official documentation.