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.
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.
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.
Next, we use GitHub authentication and need to authorize Sigstore to access our account.
Then, we get a successful authentication message.
From the DockerHub, we can verify that this process created a new signature related to the tag we specified.
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.