Sign and verify a container image with Notation in Azure Pipeline

The Notation task in Azure DevOps is a built-in task to sign and verify container images and other Open Container Initiative (OCI) artifacts within an Azure Pipeline. The Notation task utilizes the Notation CLI to execute these operations, ensuring that the artifacts are signed by a trusted entity and have not been tampered since their creation.

The article walks you through creating an Azure pipeline that builds a container image, pushes it to ACR, and adds signatures using Notation and the Azure Key Vault plugin providing a layer of security and integrity for the artifacts. The goal of the pipeline is to:

  1. Build a container image and push it to Azure Container Registry (ACR).
  2. Sign the image with Notation and Notation Azure Key Vault plugin. The signature is then automatically pushed to ACR.

Prerequisites

  • Create a Key Vault in Azure Key Vault and generate a self-signed signing key and certificate. You can use this document for creating a self-signed key and certificate for testing purposes. If you have a CA-issued certificate, refer to this document for details.
  • Create a registry in Azure Container Registry (ACR).
  • Ensure you have an Azure DevOps repository or GitHub repository.

Create Service Connection

Create a service connection in Azure Pipelines, which allows your pipeline to access external services like Azure Container Registry (ACR), you can follow these steps:

  • Sign an image in Azure Container Registry (ACR) using ACR credentials.

  • Use the Docker task in Azure Pipelines to log in to the ACR. The Docker task is a built-in task in Azure Pipelines that allows you to build, push, and pull Docker images, among other things.

  • Establish a Docker Registry service connection in Azure Pipeline for granting Notation tasks access to your ACR registry, as follows:

    1. Sign in to your organization (https://dev.azure.com/{yourorganization}) and select your project.
    2. Select the Settings button in the bottom-left corner.
    3. Go to Pipelines, and then select Service connection.
    4. Choose New service connection and select Docker Registry.
    5. Next choose Azure Container Registry.
    6. Choose Service Principle in the Authentication Type and enter the Service Principal details including your Azure Subscription and ACR registry.
    7. Enter a user-friendly Connection name to use when referring to this service connection.
  • Create an Azure Resource Manager service connection in Azure Pipelines to authorize access to your Azure Key Vault:

    1. Choose Service principal (automatic).
    2. Next, choose Subscription and find your Azure subscription from the drop-down list.
    3. Choose an available Resource group from the drop-down list.
    4. Enter a user-friendly Service connection name to use when referring to this service connection.
    5. Save it to finish the creation.
  • Grant the access policy to your service principal by following these steps:

    1. Open the created Azure Resource Manager service connection and click Manage Service Principal to enter the Azure service principal portal.
    2. Copy the Application (client) ID. It will be used to grant the permission for the service principal.
    3. Open the Azure Key Vault portal and enter Access Policies page.
    4. Create a new access policy with key sign, secret get and certificate get permission.
    5. Grant this new access policy to a service principle using the Application (client) ID paste from the previous step.
    6. Save it to finish the creation.

Learn more about service connection here.

Create a pipeline and use Notation task

Create an Azure pipeline for your git repository by following these steps:

  1. Navigate to the project in your AOD organization.
  2. Go to Pipelines from the left menu and then select New pipeline.
  3. Choose your git repository. We use the Azure DevOps repository for demonstration convenience.
  4. Configure the pipeline with a Starter Pipeline if you are new to Azure DevOps. Review and create the pipeline by clicking on Save and run.

Note

The example assumes that the default branch is main. If it's not, please follow the guide to update the default branch.

There are two ways to add Notation tasks by editing your Azure pipeline:

Use the Azure DevOps (ADO) panel: The ADO panel provides a user interface where you can add tasks to your pipeline. You can search for Notation tasks and add them to your pipeline using this interface.

Copy from a sample Azure Pipeline file: If you have a sample Azure Pipeline file that already includes Notation tasks, you can copy these tasks from the sample file and paste them into your pipeline file.

Option 1: Use the Azure DevOps (ADO) editing panel

Search the Docker task from the pipeline editing panel on the right side. Use its login command with the Docker Registry service connection to authenticate with ACR.

  1. Choose the Docker Registry service connection created in the previous step from the Container registry drop-down list.
  2. Choose login from the Command drop-down list.
  3. Click Add to add the Docker task with login command to the pipeline file left.

Similarly, search the Docker task from the pipeline editing panel again. Use its buildAndPush command to automatically build the source code to an image and push it to the target ACR repository. It will generate an image digest that will be used for signing in the next step.

  1. Input the repository name to Container repository.
  2. Choose buildAndPush from the the Command drop-down list.
  3. Specify the file path of Dockerfile. For example, use ./Dockerfile if your Dockerfile is stored in the root folder.
  4. Click Add to add the Docker task with buildAndPush command to the pipeline file left.

Search the Notation task from the pipeline editing panel on the right side.

  1. Choose Install from the drop-down list command to run.
  2. Click Add to add the notation install task to the pipeline.
  3. Similarly, search the Notation task from the pipeline editing panel again and choose Sign.
  4. You can skip Artifact references since we sign an image using its latest digest that is built and pushed to the registry by a Docker task. Instead, you can manually specify a digest using <registry_host>/<repository>@<digest>.
  5. Fill out the plugin configuration in the form. We will use the default AKV plugin and the service connection created in the previous step. Copy your Key ID from your AKV into the Key ID.
  6. Check the Self-signed Certificate box since we use a self-signed certificate for demonstration convenience. Instead, you can input your certificate file path in Certificate Bundle File Path if you want to use a CA issued certificate.
  7. Click Add to add the notation sign to the pipeline file left.

Option 2: Edit a sample Azure Pipeline file

  1. If you are familiar with Azure Pipelines and Notation, it's efficient to start with a template pipeline file.
  2. Copy the pipeline template provided in the document to your own pipeline file. This template is designed to use Notation tasks, which are used to sign and verify container images.
  3. After copying the template, fill out the required values according to the references and comments provided below.
See the signing task template of option 1 (Click here).
trigger:
 - main
pool: 
  vmImage: 'ubuntu-latest'

steps:
# log in to registry
- task: Docker@2
  inputs:
    containerRegistry: <your_docker_registry_service_connection>
    command: 'login'
# build and push artifact to registry
- task: Docker@2
  inputs:
    repository: <your_repository_name>
    command: 'buildAndPush'
    Dockerfile: './Dockerfile'
# install notation
- task: Notation@0
  inputs:
    command: 'install'
    version: '1.1.0'
# automatically detect the artifact pushed by Docker task and sign the artifact.
- task: Notation@0
  inputs:
    command: 'sign'
    plugin: 'azureKeyVault'
    akvPluginVersion: <azure_key_vault_plugin_version>
    azurekvServiceConection: <your_akv_service_connection>
    keyid: <your_key_id>
    selfSigned: true

Note

Apart from using the Docker task, you can sign a specified image digest by manually specifying an artifact reference in artifactRefs as follows.

See the example (Click here).
# sign the artifact
- task: Notation@0
  inputs:
    artifactRefs: '<registry_host>/<repository>@<digest>'
    command: 'sign'
    plugin: 'azureKeyVault'
    akvPluginVersion: <azure_key_vault_plugin_version>
    azurekvServiceConection: <akv_service_connection>
    keyid: <key_id>
    selfSigned: true

Trigger the pipeline

Follow the steps to run a pipeline in Azure DevOps and verify its execution.

  1. After filling out the inputs in the pipeline, save and run it to trigger the pipeline.
  2. Go to the Job page of the running pipeline. Here, you can see the execution of each step. This pipeline will build and sign the latest build or the specified digest, and then push the signed image along with its associated signature to the registry.
  3. Upon successful execution, you can see the image pushed to your Azure Container Registry (ACR) with a CBOR Object Signing and Encryption (COSE) format signature attached.

Verify the signed image

Similarly, to verify the signed image, you can use the editing panel or edit the pipeline file to add the notation verify task to your pipeline. The pipeline will verify the signed image with the trust policy and trust store you provided.

Prepare Notation trust policy and trust store

In general, the verifier is different from the signer. For demonstration purposes, we use the same pipeline and ADO repository in this sample. Follow the steps below to create Notation trust policy, trust store, and add the verify task in the pipeline:

  1. In the current ADO repository, create a sample folder .pipeline to store the Notation trust policy .pipeline/trustpolicy/. Create a sample trust policy JSON file trustpolicy.json. Fill out the trust policy template with your own values and save it in the folder.

Note

Note that Notation Trust Store supports currently supports three kinds of identities, including Certificate Authority (CA), SigningAuthority, and Time Stamping Authority (TSA) root certificates. For demonstration purposes, we use Certificate Authority (CA) x509/ca in the trust policy and trust store below. See trust store for details.

See the trust policy template (Click here).
{
    "version": "1.0",
    "trustPolicies": [
        {
            "name": "<yourPolicyName>",
            "registryScopes": [ "<yourRegistry>.azurecr.io/<yourArtifact>" ],
            "signatureVerification": {
                "level" : "strict" 
            },
            "trustStores": [ "ca:<yourTrustStore>"],
            "trustedIdentities": [
                "*"
            ]
        }
    ]
}
  1. In the current ADO repository, create a new folder for Notation trust store /.pipeline/truststore/x509/ca/$<yourTrustStore>/ to store the certificate. If you followed the signing steps in this document to sign your image, use the command below to download your self-signed certificate from Azure Key Vault (AKV):
KEY_NAME=<key_name_you_picked_when_creating_the_key>
AKV_NAME=<akv_name_where_certificate_is_stored>
CERT_ID=$(az keyvault certificate show -n $KEY_NAME --vault-name $AKV_NAME --query 'id' -o tsv)
CERT_PATH=./${KEY_NAME}.pem
az keyvault certificate download --file $CERT_PATH --id $CERT_ID --encoding PEM
  1. Upload the certificate to the trust store folder /.pipeline/truststore/x509/ca/$<yourTrustStore>/ that we created in the last step.

Add notation verify task

  1. Search the Notation task from the pipeline editing panel again and choose Verify.
  2. Fill out the Artifact references with the digest of the signed image.
  3. Enter the value .pipeline/trustpolicy/trustpolicy.json in the Trust Policy File Path.
  4. Enter the value .pipeline/truststore/ in the Trust Store Folder Path.
  5. Click Add to add the notation verify to the pipeline file left.

Your notation verify will be saved as follows.

See the example (Click here).
# sign the artifact
- task: Notation@0
  inputs:
    command: 'verify'
    artifactRefs: '<registry_host>/<repository>@<digest>'
    trustPolicy: .pipeline/trustpolicy.json
    trustStore: .pipeline/truststore/

Trigger the pipeline (Updated)

You can trigger the pipeline again to verify the signed image. Upon successful execution, you can see the logs from the Job page of the running pipeline. The pipeline will verify the signed image with the trust policy and trust store you provided.

Conclusion

This article shows you how to sign and verify a container image with Notation in Azure Pipeline. You can use the Azure DevOps panel or edit the pipeline file to add Notation tasks to your pipeline. The pipeline will build, push, sign, and verify the image with the trust policy and trust store you provided. This process ensures that the artifacts are signed by a trusted entity and have not been tampered with since their creation.