Skip to content

Commit

Permalink
feat(SLSA/SEC-973): container image signing action (#65)
Browse files Browse the repository at this point in the history
* feat(slsa/image-sign): Keyless image signing using Cosign

filter uniq registry and sign using digest

Always recursively sign manifest digests if present

* update readme for sign action

* fix readme

Signed-off-by: saisatishkarra <saisatish.karra@konghq.com>

* enable signing transaprency by default

Signed-off-by: saisatishkarra <saisatish.karra@konghq.com>

---------

Signed-off-by: saisatishkarra <saisatish.karra@konghq.com>
  • Loading branch information
saisatishkarra authored Oct 17, 2023
1 parent c03e30a commit b7def0b
Show file tree
Hide file tree
Showing 4 changed files with 385 additions and 0 deletions.
88 changes: 88 additions & 0 deletions .github/workflows/docker-image-sign.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
name: Docker Sign Test

on:
pull_request:
branches:
- main
push:
branches:
- main
tags:
- '*'
workflow_dispatch: {}

jobs:
test-sign-docker-image:

permissions:
contents: read
packages: write # needed to upload to packages to registry
id-token: write # needed for signing the images with GitHub OIDC Token

if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
name: Test Sign Docker Image
runs-on: ubuntu-22.04
env:
PRERELEASE_IMAGE: kongcloud/security-test-repo-pub:ubuntu_23_10 #particular reason for the choice of image: test multi arch image
TAGS: kongcloud/security-test-repo-pub:ubuntu_23_10,kongcloud/security-test-repo:ubuntu_23_10
steps:

- uses: actions/checkout@v3

- name: Install regctl
uses: regclient/actions/regctl-installer@main

- name: Parse Image Manifest Digest
id: image_manifest_metadata
run: |
manifest_list_exists="$(
if regctl manifest get "${PRERELEASE_IMAGE}" --format raw-body --require-list -v panic &> /dev/null; then
echo true
else
echo false
fi
)"
echo "manifest_list_exists=$manifest_list_exists"
echo "manifest_list_exists=$manifest_list_exists" >> $GITHUB_OUTPUT
manifest_sha="$(regctl image digest "${PRERELEASE_IMAGE}")"
echo "manifest_sha=$manifest_sha"
echo "manifest_sha=$manifest_sha" >> $GITHUB_OUTPUT
- name: Sign Image digest
id: sign_image
if: steps.image_manifest_metadata.outputs.manifest_sha != ''
uses: ./security-actions/sign-docker-image
with:
cosign_output_prefix: ubuntu-23-10
signature_registry: kongcloud/security-test-repo-sig-pub
tags: ${{ env.TAGS }}
image_digest: ${{ steps.image_manifest_metadata.outputs.manifest_sha }}
local_save_cosign_assets: true
registry_username: ${{ secrets.DOCKERHUB_PUSH_USERNAME }}
registry_password: ${{ secrets.DOCKERHUB_PUSH_TOKEN }}

- name: Push Images
env:
RELEASE_TAG: kongcloud/security-test-repo:v1
run: |
docker pull ${PRERELEASE_IMAGE}
for tag in $RELEASE_TAG; do
regctl -v debug image copy ${PRERELEASE_IMAGE} $tag
done
- name: Sign Image digest
id: sign_image_v1
if: steps.image_manifest_metadata.outputs.manifest_sha != ''
uses: ./security-actions/sign-docker-image
env:
RELEASE_TAG: kongcloud/security-test-repo:v1
with:
cosign_output_prefix: v1 # Optional
local_save_cosign_assets: true # Optional
signature_registry: kongcloud/security-test-repo-sig-pub
tags: ${{ env.RELEASE_TAG }}
image_digest: ${{ steps.image_manifest_metadata.outputs.manifest_sha }}
registry_username: ${{ secrets.DOCKERHUB_PUSH_USERNAME }}
registry_password: ${{ secrets.DOCKERHUB_PUSH_TOKEN }}
156 changes: 156 additions & 0 deletions security-actions/sign-docker-image/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# Security actions

## Action implemented

- [Sign Docker Image](./sign-docker-image/action.yml) is a unified action for container image signing. The action leverages keyless signing to produce an Signature and uploads to Docker Image layer and Public Rekor for transaprency.

- Tools used:
- [Cosign](https://github.com/sigstore/cosign)
### Signing Docker Image

- For workflows where image artifact are being pushed to the registry, this needs to be implemented `after the scan and before the publish` step.

#### Workflow / Job Permissions for Keyless OIDC Signing:
```yaml
permissions:
packages: write
id-token: write # needed for signing the images with GitHub OIDC Token
```
#### Signature Publishing
- `cosign sign` to:
- Generate an signature based on keyless identities using `Github` OIDC provider within workflows
- Be authenicated access to publish docker hub registry
- Uploads the [mapping identities](https://github.com/sigstore/fulcio/blob/main/docs/oid-info.md) to Public Rekor Instance logged forever.
- **May Contain senstitive information for private repositories**; Yet no way to protect PII being uploaded / masked.

#### Verification
- `cosign verify` needs to have:
- access to public rekor instance
- authenicated access to private docker hub registry
- un-authenticated access to public registry

#### Input specification

#### Parameters
```yaml
local_save_cosign_assets:
description: 'Save cosign output assets locally on disk. Ex: certificate and signature of signed artifacts'
required: false
default: false
cosign-output-prefix:
description: 'cosign output prefix. Ex: certificate and signature of signed artifacts'
required: true
signature_registry:
description: 'Separate registry to store image signature to avoid polluting image registry'
required: false
default: ''
tags:
description: 'Comma separated <image>:<tag> that have same digest'
required: true
image_digest:
description: 'specify single sha256 digest associated with the specified image_registries'
required: true
registry_username:
description: 'docker username to login against private docker registry'
required: false
registry_password:
description: 'docker password to login against private docker registry'
required: false
```
#### Output specification

- Generates a signature that is pushed to registry for every single platform and manifest digest

- Generates a log entry in Public Rekor transaprency for every digest being signed with a unique docker repository

- No Build Time artifacts are generated

#### Verification:
Use `cosign verify` command to specify claims and image digest to be verified against the rekor transaparency log and signature / certificate subject identity in the Docker registry

Example:
```
COSIGN_REPOSITORY=kong/notary cosign verify -a repo="Kong/kong-ee" -a workflow="Package & Release" --certificate-oidc-issuer="https://token.actions.githubusercontent.com" --certificate-identity-regexp="https://github.com/Kong/kong-ee/.github/workflows/release.yml*" <image>:<tag>@sha256:<disgest>
```

#### Usage Examples

```yaml
jobs:
test-sign-docker-image:
permissions:
contents: read
packages: write # needed to upload to packages to registry
id-token: write # needed for signing the images with GitHub OIDC Token
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
name: Test Sign Docker Image
runs-on: ubuntu-22.04
env:
PRERELEASE_IMAGE: kongcloud/security-test-repo-pub:ubuntu_23_10 #particular reason for the choice of image: test multi arch image
TAGS: kongcloud/security-test-repo-pub:ubuntu_23_10,kongcloud/security-test-repo:ubuntu_23_10
steps:
- uses: actions/checkout@v3
- name: Install regctl
uses: regclient/actions/regctl-installer@main
- name: Parse Image Manifest Digest
id: image_manifest_metadata
run: |
manifest_list_exists="$(
if regctl manifest get "${PRERELEASE_IMAGE}" --format raw-body --require-list -v panic &> /dev/null; then
echo true
else
echo false
fi
)"
echo "manifest_list_exists=$manifest_list_exists"
echo "manifest_list_exists=$manifest_list_exists" >> $GITHUB_OUTPUT
manifest_sha="$(regctl image digest "${PRERELEASE_IMAGE}")"
echo "manifest_sha=$manifest_sha"
echo "manifest_sha=$manifest_sha" >> $GITHUB_OUTPUT
- name: Sign Image digest
id: sign_image_pre_release
if: steps.image_manifest_metadata.outputs.manifest_sha != ''
uses: ./security-actions/sign-docker-image
with:
cosign_output_prefix: ubuntu-23-10
signature_registry: kongcloud/security-test-repo-sig-pub
tags: ${{ env.TAGS }}
image_digest: ${{ steps.image_manifest_metadata.outputs.manifest_sha }}
local_save_cosign_assets: true
registry_username: ${{ secrets.GHA_DOCKERHUB_PUSH_USER }}
registry_password: ${{ secrets.GHA_KONG_ORG_DOCKERHUB_PUSH_TOKEN }}
- name: Push Images
env:
RELEASE_TAG: kongcloud/security-test-repo:v1
run: |
docker pull ${PRERELEASE_IMAGE}
for tag in $RELEASE_TAG; do
regctl -v debug image copy ${PRERELEASE_IMAGE} $tag
done
- name: Sign Image digest
id: sign_image_promotion
if: steps.image_manifest_metadata.outputs.manifest_sha != ''
uses: ./security-actions/sign-docker-image
env:
RELEASE_TAG: kongcloud/security-test-repo:v1
with:
cosign_output_prefix: v1
signature_registry: kongcloud/security-test-repo-sig-pub
tags: ${{ env.RELEASE_TAG }}
image_digest: ${{ steps.image_manifest_metadata.outputs.manifest_sha }}
local_save_cosign_assets: true
registry_username: ${{ secrets.GHA_DOCKERHUB_PUSH_USER }}
registry_password: ${{ secrets.GHA_DOCKERHUB_PUSH_TOKEN }}
```
107 changes: 107 additions & 0 deletions security-actions/sign-docker-image/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
name: Sign Docker Image
description: Keyeless Image signing with transaprency and uploads to registry for specified docker image
author: 'Kong'
inputs:
local_save_cosign_assets:
description: 'Save cosign output assets locally on disk. Ex: certificate and signature of signed artifacts'
required: false
default: false
cosign_output_prefix:
description: 'cosign file prefix for storing local signatures and certificates. Works when input local_save_cosign_assets is enabled'
required: false
default: ''
signature_registry:
description: 'Separate registry to store image signature to avoid polluting image registry'
required: false
default: ''
tags:
description: 'Comma separated <image>:<tag> that have same digest'
required: true
image_digest:
description: 'specify single sha256 digest associated with the specified image_registries'
required: true
registry_username:
description: 'docker username to login against private docker registry'
required: false
registry_password:
description: 'docker password to login against private docker registry'
required: false

#outputs:
# sbom-cyclonedx-report:
# description: 'SBOM cyclonedx report'
# value: ${{ steps.meta.outputs.sbom_cyclonedx_file }}

runs:
using: composite
steps:

- name: Set Cosign metadata
shell: bash
id: meta
env:
LOCAL_SAVE_COSIGN_ASSETS: ${{ inputs.local_save_cosign_assets }}
ASSET_PREFIX: ${{ inputs.cosign_output_prefix }}
run: $GITHUB_ACTION_PATH/scripts/cosign-metadata.sh

- name: Install Cosign
uses: sigstore/cosign-installer@v3.1.1

- name: Check install!
shell: bash
run: cosign version

- name: Setup image namespace for signing, strip off the tag
shell: bash
env:
INPUT_TAGS: ${{ inputs.tags }}
run: |
set -euox pipefail
TAGS="${INPUT_TAGS//,/ }"
IMAGE_REPOS=$(for tag in \
`echo "${TAGS}"`; do
echo -n "${tag}" | awk -F ":" '{print $1}' -;done|sort -u)
echo $IMAGE_REPOS
echo 'IMAGES<<EOF' >> $GITHUB_ENV
echo $IMAGE_REPOS >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
- name: Login to Container Registry
uses: docker/login-action@v2.1.0
if: ${{ inputs.registry_username != '' && inputs.registry_password != '' }}
with:
username: ${{ inputs.registry_username }}
password: ${{ inputs.registry_password }}

- name: Sign the images with GitHub OIDC Token
id: sign
env:
COSIGN_REPOSITORY: ${{ inputs.signature_registry }}
COSIGN_ARGS: ${{ steps.meta.outputs.cosign_signing_args}}
IMAGE_REGISTRIES: ${{ env.IMAGES }} # Space separated image registries that have same digest
IMAGE_DIGEST: ${{ inputs.image_digest }} # Single Digest associated with the registry images
shell: bash
run: |
set -euox pipefail
for img in $IMAGES; do
cosign sign ${{ env.COSIGN_ARGS }} \
-a "repo=${{ github.repository }}" \
-a "workflow=${{ github.workflow }}" \
-a "sha=${{ github.sha }}" \
"$img"@${{ env.IMAGE_DIGEST }}
done
# Upload Cosign Artifacts (public cert and signatures)
- name: Upload Cosign Artifacts
uses: actions/upload-artifact@v3
if: ${{ inputs.local_save_cosign_assets == 'true' && inputs.cosign_output_prefix != '' }}
with:
name: signed-image-assets
path: |
${{inputs.cosign_output_prefix}}*.crt
${{inputs.cosign_output_prefix}}.sig*
if-no-files-found: warn
34 changes: 34 additions & 0 deletions security-actions/sign-docker-image/scripts/cosign-metadata.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env bash

set -euo pipefail

readonly signature_ext=".sig"
readonly signing_cert_ext=".crt"

readonly rekor_transparency="true"

# Always Recurisvely sign one/ all manifest digests for docker manifest distribution /list mediaType
signing_args="--yes --recursive --tlog-upload=${rekor_transparency}"

# if [[ ${MULTI_PLATFORM} ]]; then
# signing_args+=" --recursive"
# fi

if [[ "${LOCAL_SAVE_COSIGN_ASSETS}" == "true" ]]; then
if [[ -n "${ASSET_PREFIX}" ]]; then
signature_file="${ASSET_PREFIX##*/}${signature_ext}"
certificate_file="${ASSET_PREFIX##*/}${signing_cert_ext}"
else
echo '::error ::set input cosign_output_prefix in $0'
exit 1
# signature_file="${ASSET_PREFIX##*/}${signature_ext}"
# certificate_file="${ASSET_PREFIX##*/}${signing_cert_ext}"
fi

echo "signature_file=${signature_file}" >> $GITHUB_OUTPUT
echo "certificate_file=${certificate_file}" >> $GITHUB_OUTPUT
signing_args+=" --output-certificate=${certificate_file} --output-signature=${signature_file}"
fi

echo "COSIGN SIGNING ARGS: ${signing_args}"
echo "cosign_signing_args=${signing_args}" >> $GITHUB_OUTPUT

0 comments on commit b7def0b

Please sign in to comment.