Build and sign application containers
At the end of this tutorial you will learn how to sign a container image without using a cosign.
Prerequisites
Before you get started with the tutorial make sure you have completed the Build Go Application Containers CI Pipeline.
Ensure the following tools are installed locally:
Overview
As part of the Build Go Application Containers CI Pipeline tutorial we saw how to build a Go application container image using a CI pipeline. In this tutorial we improve the pipeline to sign the container image and ensure its authenticity. Please read History and Research to learn why we need to secure the software supply chain.
Generate signing keys
- Assuming you have the fork of https://github.com/harness-apps/go-fruits-api, navigate to the folder where it was cloned. Refer to this folder as
$TUTORIAL_HOME
.
cd $TUTORIAL_HOME
- Generate a random password and store it in a variable called
$COSIGN_PASSWORD
,
export COSIGN_PASSWORD=$(openssl rand -hex 16)
Make sure to note down the $COSIGN_PASSWORD
as it might be needed later.
echo $COSIGN_PASSWORD
- Once you have generated the password, generate the key pair that we will use to sign.
cosign generate-key-pair
The command generates the cosign.key
and cosign.pub
under $TUTORIAL_HOME
.
Make sure the cosign.key
, cosign.pub
are excluded from your SCM. In other words, add them to .gitignore
Create signing secrets
- Navigate to Project Setup, and then select Secrets.
- Select + New Secret , and then select Text.
- Set the Secret Name to be
cosign_password
, and then fill your cosign private key password$COSIGN_PASSSWORD
as Secret Value.
- Select + New Secret, and then select File.
- Fill the details for the secret
cosign_private_key
as shown.
For the Select File, browse and pick the
cosign.key
from the$TUTORIAL_HOME
.Repeat the same process of creating another
__File__
secret to store the cosign public key, this time using the filecosign.pub
from the$TUTORIAL_HOME
.Fill the details for the secret
cosign_public_key
as shown.
With this we now have four secrets in our project:
Update the pipeline to sign the container Image
We now have all of the required resources: cosign_password, cosign_private_key and cosign_public_key to sign the container image using cosign.
Now, update the existing build and push step of our Build Go
pipeline to sign the fruits-api
container image.
- Navigate to Projects, and then Pipelines.
- Select Build Go pipeline to open the pipeline editor.
- Select the step build and push and update the Command to be as shown:
Command:
echo -n "$DOCKER_HUB_PASSWORD" | ko auth login docker.io -u "$DOCKER_HUB_USERNAME" --password-stdin
IMAGE_REF=$(ko build --bare --platform linux/amd64 --platform linux/arm64 .)
cosign sign --key env://COSIGN_PRIVATE_KEY $IMAGE_REF
We also need to configure few environment variables that are required by ko
and cosign
to build and push the signed image to fruits-api
container repository.
- Update the Environment Variables section with following values:
DOCKER_HUB_USERNAME: $DOCKER_HUB_USERNAME
DOCKER_HUB_PASSWORD: <+secrets.getValue("docker_hub_password")>
KO_DOCKER_REPO: docker.io/$DOCKER_HUB_USERNAME/fruits-api
COSIGN_PRIVATE_KEY: <+secrets.getValue("cosign_private_key")>
COSIGN_PASSWORD: <+secrets.getValue("cosign_password")>
The step also exposes an output variable called IMAGE_REF
which could be used by other steps, for example to verify the signature by using pipeline expression <+steps.build_and_push.output.outputVariables.IMAGE_REF>
.
- As marked, ensure the
DOCKER_HUB_PASSWORD
,COSIGN_PRIVATE_KEY
andCOSIGN_PASSWORD
are of the Expression type. secrets.getValue
is an expression that allows you to get the value from the secretdocker_hub_password
that was created earlier in the tutorial. Check the docs for more information.- All
$DOCKER_HUB_USERNAME
references should your Docker Hub Username.
- Select Apply Changes to save the step, and then select Save to save the pipeline.
With those changes saved, you are ready to lint, test, build and push your go application to container registry(DockerHub).
Run the CI pipeline
- As you did earlier, select Run from the pipeline editor window.
Leaving all the default entries in place, namely Git Branch and Branch Name to be main, select Run Pipeline to start the pipeline run.
Now you are ready to execute. Select Run Pipeline.
After the pipeline runs successfully, head back to Docker Hub to check the image references.
As you see the logs the container was built and the image reference is signed using our $COSIGN_PRIVATE_KEY
.
Verify the signed image
To verify the image signature, use the public key $COSIGN_PUBLIC_KEY
against the image sha256
digest.
To get the digest of an image you can use crane.
export IMAGE_REF=$(crane digest docker.io/$DOCKER_USERNAME/fruits-api:latest)
Run the following command to verify the signature using our cosign public key,
cosign verify --key "$TUTORIAL_HOME/cosign.pub" \
"docker.io/$DOCKER_USERNAME/[email protected]$IMAGE_REF"
The command should show an output as shown if the signature is valid, with $DOCKER_USERNAME
and $IMAGE_REF
replacing your Docker Hub username and image digest from the previous step.
Verification for index.docker.io/$DOCKER_USERNAME/[email protected]$IMAGE_REF --
The following checks were performed on each of these signatures:
- The cosign claims were validated
- The signatures were verified against the specified public key
[{"critical":{"identity":{"docker-reference":"index.docker.io/$DOCKER_USERNAME/fruits-api"},"image":{"docker-manifest-digest":"$IMAGE_REF"},"type":"cosign container image signature"},"optional":null}]
In an ideal scenario the verification is done by governance tools like OPA or kyverno. The integration with those tools can be done using Harness Platform with Harness Continuous Delivery.
Can you update the pipeline to a sign verification step?
Clue: Use $IMAGE_REF
from build and push step output variables.
Continuing on your continuous integration journey
You can now execute your builds whenever you want in a consistent fashion. You can modify the trigger to watch for SCM events so upon commit, for example, the pipeline is kicked off automatically. All of the objects you create are available for you to reuse. Lastly, you can even save your backing work / have it as part of your source code. Everything that you do in Harness is represented by YAML; feel free to store it as part of your project.
After you have built your artifact, the next step is to deploy your artifact. This is where Continuous Delivery steps in. Make sure to check out some other CD Tutorials.