Cosign is used to sign and verify various kind of artifacts.

Logo

Cosign supports different way to sign images, including:

  • keyless signing through Sigstore’s Fulcio Certificate Authority
  • signing with our personal keys
  • KMS signing
  • hardware signing

This demo illustrates the creation and usage of a public/private key pair.

Installation

Cosign can be installed in several ways at details in the documentation. On macOS, it can be installed using Homebrew.

brew install cosign

Next we create a public/private key pair

cosign generate-key-pair

It generates 2 files:

  • cosign.key: private key
  • cosign.pub: public key

Signing an image

In this step, we’ll sign the lucj/btcprice DockerHub image. This image packages a demo application which gets the BTC USD price on a regular basis, and sends it to various external systems.

Several tags exist, including:

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

We only signed the image using the sha256 associated to the 0.0.5 tag.

cosign sign --key cosign.key lucj/btcprice@sha256:e14812c2d6d827a9402b8109c65e024fcd04cb4f998d59329e816a9c908a4e23
⚠️
To sign an image existing in the DockerHub, it is required to have write access to this registry from the command line as the signature is sent to the registry.

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

Signature in DockerHub

Enforcing the usage of the signed image

To ensure only the signed image can be run on our cluster, we install Kyverno policy engine.

⚠️
Kyverno is not the only policy engine which can be used, other alternatives including OPA/GateKeeper exist.
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) which prevents an image from lucj/btcprice from being pulled unless it has a valid signature. This resource contains to cosign public key so Kyverno can verify the signature.

check-signature.yaml
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

First, we try to create a Deployment based on the image 0.0.4 using its sha256 lucj/btcprice@sha256:7b08c75f45d82bedfcd1a06d970b663203b77092c84280f07f6eba87a3c843f5.

btcprice.yaml
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"

We are prevented from creating this Deployment as the image 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 now change the image tag, using the one corresponding to the signed image.

btcprice.yaml
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

Then, we verify the Pod is running fine.

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

The application is configured to send BTC price to a default backend, which we can verify is working fine.

BTC price in webhooks

Everything is fine as Kyverno allowed this Pod to be deployed.

Key takeaways

In this demo, we generated a key pair and used it to sign and verify an image. Feel free to explore the other signature mechanisms detailed in the cosign documentation.