> For the complete documentation index, see [llms.txt](https://developer.harness.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developer.harness.io/continuous-delivery/troubleshooting-and-resources/tutorials/cloud-native-cicd-pipelines/cosign-opa.md).

# Secure Container Image Signing with Cosign and OPA

Add Cosign image verification and OPA policy enforcement to a Kubernetes deployment pipeline

This tutorial shows how to sign a container image with [Cosign](https://github.com/sigstore/cosign), verify the signature in a Harness Deploy stage, and enforce the result with Open Policy Agent (OPA) policies. You reuse the Guestbook sample application pipeline from the [Kubernetes Manifest tutorial](/continuous-delivery/troubleshooting-and-resources/tutorials/kubernetes-container-deployments/manifest.md), then add Shell Script and Policy steps before **Rollout Deployment** so only an approved signed image can deploy.

Harness Continuous Delivery (CD) and GitOps include OPA so you can evaluate policies during pipeline execution. Cosign keyless signing authenticates through OpenID Connect (OIDC) with trusted providers such as Google, GitHub, or Microsoft.

This tutorial is also available as a video.

{% embed url="<https://www.youtube.com/watch?v=PLvjcCCStzs>" %}

***

## What you will learn from this topic

* How to [sign images with Cosign](#sign-images-with-cosign) and push signatures to your registry
* How to set the [deployment goal](#deployment-goal) for digest and annotation checks
* How to [verify the image in the pipeline](#verify-the-image-in-the-pipeline) with a Shell Script step
* How to [write and attach OPA policies](#write-and-attach-opa-policies) that deny unapproved digests or environments
* How to [test policy enforcement](#test-policy-enforcement) with passing and failing cases

***

## Before you begin

Complete the following before you start:

* **Harness account:** A Harness Enterprise account (paid or trial). If you do not have an account, [sign up](https://app.harness.io/auth/#/signup/?module=cd\&utm_source=website\&utm_medium=harness-developer-hub\&utm_campaign=cd-plg\&utm_content=tutorials-cd-kubernetes-cosign-opa).
* **Kubernetes Manifest tutorial:** Complete the [Kubernetes Manifest tutorial](/continuous-delivery/troubleshooting-and-resources/tutorials/kubernetes-container-deployments/manifest.md) (GitOps workflow or CD pipeline). This tutorial reuses the pipeline and resources from that topic.
* **Platform concepts:** Familiarity with Harness [pipelines](https://developer.harness.io/docs/platform/get-started/key-concepts#pipelines), [stages](https://developer.harness.io/docs/platform/get-started/key-concepts#stages), and [steps](https://developer.harness.io/docs/platform/get-started/key-concepts#steps-and-step-groups).
* **Docker Hub (or another registry):** Ability to push images and Cosign signatures to a registry you control.
* **OIDC identity:** An identity and issuer you can use for Cosign keyless signing (Google, Microsoft, GitHub, or GitLab).

{% hint style="warning" %}
**Policy-based governance**

Policy-based governance is a paid feature on the Harness platform.
{% endhint %}

***

## Architecture

Without Cosign or OPA, a CD pipeline can pull an image and deploy it without verification. With Cosign and OPA, verification and policy evaluation run before the deploy step.

![Developer flow with Cosign and OPA](https://3694223630-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fy1JhZ4oKIppwY7d5AhPj%2Fuploads%2Fgit-blob-aed3c16a9902433d4823f914d96afa88daed6a9a%2Fdeveloper-flow-with-cosign-opa.png?alt=media)

***

## Sign images with Cosign

In this section you pull the public Guestbook image (a sample application), tag it for development and production, sign it with Cosign, and push it to an image registry. The examples use Docker Hub.

Perform the following steps to sign and push the images:

{% stepper %}
{% step %}

### Log in to the registry

Log in to Docker Hub from the terminal so you can push images:

```bash
docker login
```

{% endstep %}

{% step %}

### Pull and tag the Guestbook image

Pull the public Guestbook image, then tag development and production variants:

```bash
docker pull gcr.io/heptio-images/ks-guestbook-demo:0.1

docker tag gcr.io/heptio-images/ks-guestbook-demo:0.1 YOUR_DOCKERHUB_USERNAME/guestbook-dev:0.1

docker tag gcr.io/heptio-images/ks-guestbook-demo:0.1 YOUR_DOCKERHUB_USERNAME/guestbook-prod:0.1
```

{% endstep %}

{% step %}

### Install Cosign and set image variables

[Download and install Cosign](https://github.com/sigstore/cosign#installation), then export image references:

```bash
export IMAGE_DEV=YOUR_DOCKERHUB_USERNAME/guestbook-dev:0.1

export IMAGE_PROD=YOUR_DOCKERHUB_USERNAME/guestbook-prod:0.1
```

{% endstep %}

{% step %}

### Push the images

Push both tags to your registry:

```bash
docker push $IMAGE_DEV

docker push $IMAGE_PROD
```

{% endstep %}

{% step %}

### Sign by digest

Check your Docker repository for the image digest for both images (in the form `sha256:xxxx...`). Both images can share the same digest when tagged from the same source image. Sign by digest, not by tag, so you do not sign a different image than intended.

```bash
cosign sign YOUR_DOCKERHUB_USERNAME/guestbook-dev@YOUR_IMAGE_DIGEST -a env=dev

cosign sign YOUR_DOCKERHUB_USERNAME/guestbook-prod@YOUR_IMAGE_DIGEST -a env=prod
```

The `-a` flag adds an annotation to each signature. A browser window opens for OIDC sign-in. After authentication, Sigstore reports success:

![Cosign verify successful](https://3694223630-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fy1JhZ4oKIppwY7d5AhPj%2Fuploads%2Fgit-blob-fd64e45df94191daa4b90255e4ff36a98948dbd1%2Fcosign-verify-success.png?alt=media)

Terminal output looks like this:

```bash
Generating ephemeral keys...
Retrieving signed certificate...

The sigstore service, hosted by Sigstore a Series of LF Projects, LLC, is ...

...
Are you sure you would like to continue? [y/N] Y
Your browser will now be opened to:
...
Successfully verified SCT...

tlog entry created with index: XXXXXXXX
Pushing signature to: index.docker.io/YOUR_DOCKERHUB_USERNAME/guestbook-dev
```

{% endstep %}
{% endstepper %}

After signing, your registry shows additional repositories or digests for the detached Cosign signatures. Cosign stores a separate cryptographic signature object with the image so verification can retrieve both the image and its signature.

***

## Deployment goal

Before developers deploy, put verification in place so the image is checked before rollout. You can have three container images for the same Guestbook sample application. Here is an example of how digests and annotations compare:

| Image                                  | Tag | Digest             | Annotation | Signed |
| -------------------------------------- | --- | ------------------ | ---------- | ------ |
| gcr.io/heptio-images/ks-guestbook-demo | 0.1 | sha256:fe18...7f47 | -          | No     |
| dewandemo/guestbook-dev                | 0.1 | sha256:aa11...9975 | env:dev    | Yes    |
| dewandemo/guestbook-prod               | 0.1 | sha256:aa11...9975 | env:prod   | Yes    |

In this example, the goal is to allow deployment of `dewandemo/guestbook-dev@sha256:aa11...9975` with annotation `env:dev`, and deny any other image. Replace the example image details with your own registry, digest, and annotations.

***

## Verify the image in the pipeline

From the [Kubernetes Manifest tutorial](/continuous-delivery/troubleshooting-and-resources/tutorials/kubernetes-container-deployments/manifest.md), you have a stage **deploy-guestbook** in the deployment pipeline **harness\_guestbook\_pipeline** with a single **Rollout Deployment** step. Add two steps before **Rollout Deployment**: image verification with Cosign, then policy enforcement with OPA.

![Updated deploy-guestbook stage](https://3694223630-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fy1JhZ4oKIppwY7d5AhPj%2Fuploads%2Fgit-blob-4ff3b91afd42c2d62d0e901887e22dff9aefc3d2%2Fupdated-deploy-guestbook-stage.png?alt=media)

### Add the Cosign verify Shell Script step

Add a [Shell Script step](/continuous-delivery/use-continuous-delivery/cd-building-blocks/cd-steps/utilities/shell-script-step.md). Name it **cosign\_verify**. Keep the other settings at their defaults, and add the following script. Replace `YOUR_DOCKERHUB_USERNAME`, `YOUR_OIDC_CERTIFICATE_IDENTITY` (for example your associated email), and `YOUR_OIDC_ISSUER` (google, microsoft, github, or gitlab).

OIDC issuer values:

* Google: `https://accounts.google.com`
* Microsoft: `https://login.microsoftonline.com`
* GitHub: `https://github.com/login/oauth`
* GitLab: `https://gitlab.com`

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

output=$(cosign verify YOUR_DOCKERHUB_USERNAME/guestbook-dev:0.1 --certificate-identity=YOUR_OIDC_CERTIFICATE_IDENTITY --certificate-oidc-issuer=YOUR_OIDC_ISSUER)

image_registry=$(echo "$output" | awk -F'"docker-reference":"' '{print $2}' | cut -d '"' -f 1)
image_digest=$(echo "$output" | awk -F'"docker-manifest-digest":"' '{print $2}' | cut -d '"' -f 1)
image_signed_string=$(echo "$output" | awk -F'"type":"' '{print $2}' | cut -d '"' -f 1)
image_sign_issuer=$(echo "$output" | awk -F'"Issuer":"' '{print $2}' | cut -d '"' -f 1)
image_env=$(echo "$output" | awk -F'"env":"' '{print $2}' | cut -d '"' -f 1)
```

The script installs Cosign, runs `cosign verify`, and parses fields from the response into step variables. Define [script output variables](/continuous-delivery/use-continuous-delivery/cd-building-blocks/cd-steps/utilities/shell-script-step.md#script-output-variables) for all five outputs. Example mapping:

| Script output variable     | Bash output variable  |
| -------------------------- | --------------------- |
| sov\_image\_registry       | image\_registry       |
| sov\_image\_digest         | image\_digest         |
| sov\_image\_signed\_string | image\_signed\_string |
| sov\_image\_sign\_issuer   | image\_sign\_issuer   |
| sov\_image\_env            | image\_env            |

When you reference these variables later, use the format `<+execution.steps.cosign_verify.output.outputVariables.sov_image_registry>`. Your step identifier might differ from `cosign_verify`.

The first four variables help confirm that the correct digest comes from an approved registry with an expected OIDC issuer. The last variable is the `env` annotation. Adjust the script and variables to match the checks you need.

***

## Write and attach OPA policies

Add a [Policy step](/continuous-delivery/use-continuous-delivery/cd-building-blocks/advanced/cd-governance/add-a-governance-policy-step-to-a-pipeline.md) after **cosign\_verify** (for example **policy\_enforcement**). You can define and store policies in the OPA service in Harness. Policies are written in [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/).

### Create the policies

From **Project Setup**, go to **Policies** and create two policies named **Check Image Digest** and **Check Environment**. Store both inline in Harness.

**Check Image Digest** policy:

```rego
package main

deny {
    not input.digest == "YOUR_IMAGE_DIGEST_FOR_GUESTBOOK-DEV"
}
```

This Rego policy denies access if the `digest` value in the input is not `YOUR_IMAGE_DIGEST_FOR_GUESTBOOK-DEV`. Replace the placeholder with your actual image digest.

**Check Environment** policy:

```rego
package main

deny {
    not input.env == "dev"
}
```

This Rego policy denies access unless the `env` value in the input is `dev`.

Save both policies. In Harness, you add Rego policies to a Policy Set. On configured events (for example saving or running a pipeline, or evaluating on a step), Harness evaluates the action with that Policy Set.

### Create the Policy Set and wire the step

Perform the following steps to attach the policies to the pipeline:

1. From the **Policies** navigation menu, select **Policy Sets**, then select **+ New Policy Set**.
2. Enter a name (for example `Check Image Sign Policy Set`), select **Custom** for **Entity Type that this policy set applies to**, and select **On Step** for **On what event should the policy set be evaluated**.
3. Select **Next**, select the **Check Image Digest** and **Check Environment** policies, then select **Finish**.
4. Open the pipeline, select the **policy\_enforcement** step, keep the default settings, select `Check Image Sign Policy Set` for the policy set, and add this payload:

```json
{
    "digest": "<+execution.steps.cosign_verify.output.outputVariables.sov_image_digest>",
    "env": "<+execution.steps.cosign_verify.output.outputVariables.sov_image_env>"
}
```

The payload reads values from the script output variables defined in the previous step. If your Shell Script step uses a different identifier, update the expressions to match it.

***

## Test policy enforcement

Validate the setup with a passing case and two failing cases. The steps below assume you [forked harnesscd-example-apps](https://github.com/harness-community/harnesscd-example-apps/fork).

### Passing case

Update `https://github.com/YOUR_GITHUB_USERNAME/harnesscd-example-apps/blob/master/guestbook/guestbook-ui-deployment.yaml` and set `spec.containers.image` to `YOUR_DOCKERHUB_USERNAME/guestbook-dev:0.1`. On your Harness pipeline, the **cosign\_verify** step already checks for this signed container image. Run the pipeline. Both **Check Image Digest** and **Check Environment** policies should pass. Under **Project Setup** -> **Policies** -> **Evaluations**, open the recent evaluation to confirm the policy and payload match. The **Rollout Deployment** step runs and the deployment succeeds.

### Failing case: unsigned image

Update the same deployment manifest and set `spec.containers.image` to the public unsigned image `gcr.io/heptio-images/ks-guestbook-demo:0.1`. In the **cosign\_verify** Shell Script step, update the verify command:

```bash
output=$(cosign verify gcr.io/heptio-images/ks-guestbook-demo:0.1 --certificate-identity=YOUR_OIDC_CERTIFICATE_IDENTITY --certificate-oidc-issuer=YOUR_OIDC_ISSUER)
```

Policy validation fails because of mismatches between the policy and the payload. Both **Check Image Digest** and **Check Environment** fail, and the deployment does not proceed.

### Failing case: wrong environment annotation

Update the deployment manifest and set `spec.containers.image` to `YOUR_DOCKERHUB_USERNAME/guestbook-prod:0.1`. In the **cosign\_verify** Shell Script step, update the verify command:

```bash
output=$(cosign verify YOUR_DOCKERHUB_USERNAME/guestbook-prod:0.1 --certificate-identity=YOUR_OIDC_CERTIFICATE_IDENTITY --certificate-oidc-issuer=YOUR_OIDC_ISSUER)
```

**Check Image Digest** can pass when digests match, but **Check Environment** fails when the annotation is `prod` instead of `dev`. The deployment does not proceed.

***

## Optional follow-on: registry policy

Five script output variables were declared, but only `sov_image_digest` and `sov_image_env` appear in the sample policies. Add another policy to the Policy Set that requires the image to come from an approved registry. Use `sov_image_registry` in the payload. You can also enforce the other unused output variables the same way.

***

## Troubleshooting

<details>

<summary>Cosign verify fails during the Shell Script step</summary>

Confirm the Delegate can reach the image registry and the Sigstore services your Cosign version requires. Confirm `YOUR_OIDC_CERTIFICATE_IDENTITY` and `YOUR_OIDC_ISSUER` match the identity used at sign time. Prefer verifying by digest when tags can move.

</details>

<details>

<summary>Policy evaluation fails even for the signed development image</summary>

Confirm the Policy step payload expressions use the correct step identifier and output variable names. Confirm the digest string in Rego matches the digest Cosign returns, including the `sha256:` prefix.

</details>

<details>

<summary>Deployment continues when you expect a deny</summary>

Confirm the Policy Set event is **On Step**, the Policy step references that Policy Set, and the policy action is configured to fail the step on deny. Go to [Add a governance Policy step](/continuous-delivery/use-continuous-delivery/cd-building-blocks/advanced/cd-governance/add-a-governance-policy-step-to-a-pipeline.md) to review Policy step behavior.

</details>

***

## Next steps

* [Generate SLSA Provenance](https://developer.harness.io/docs/software-supply-chain-assurance/use-scs/artifact-security/slsa/generate-slsa): Add supply-chain attestation steps in Harness pipelines.
* [Verify SLSA Provenance](https://developer.harness.io/docs/software-supply-chain-assurance/use-scs/artifact-security/slsa/verify-slsa): Verify provenance before you promote an artifact.
* [Add a governance Policy step](/continuous-delivery/use-continuous-delivery/cd-building-blocks/advanced/cd-governance/add-a-governance-policy-step-to-a-pipeline.md): Apply OPA checks at other pipeline events.
* [Kubernetes Manifest tutorial](/continuous-delivery/troubleshooting-and-resources/tutorials/kubernetes-container-deployments/manifest.md): Revisit the base Guestbook deployment this topic extends.

{% @harness-feedback/feedback %}
