Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(SLSA/SEC-973): container image signing action #65

Merged
merged 4 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/[email protected]

- 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/[email protected]
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
Loading