diff --git a/.github/workflows/docker-image-sign.yml b/.github/workflows/docker-image-sign.yml new file mode 100644 index 00000000..97894003 --- /dev/null +++ b/.github/workflows/docker-image-sign.yml @@ -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 }} \ No newline at end of file diff --git a/security-actions/sign-docker-image/README.md b/security-actions/sign-docker-image/README.md new file mode 100644 index 00000000..1f485426 --- /dev/null +++ b/security-actions/sign-docker-image/README.md @@ -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 : 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*" :@sha256: +``` + +#### 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 }} + ``` \ No newline at end of file diff --git a/security-actions/sign-docker-image/action.yml b/security-actions/sign-docker-image/action.yml new file mode 100644 index 00000000..554ff90b --- /dev/null +++ b/security-actions/sign-docker-image/action.yml @@ -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 : 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<> $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 \ No newline at end of file diff --git a/security-actions/sign-docker-image/scripts/cosign-metadata.sh b/security-actions/sign-docker-image/scripts/cosign-metadata.sh new file mode 100755 index 00000000..c152e739 --- /dev/null +++ b/security-actions/sign-docker-image/scripts/cosign-metadata.sh @@ -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 \ No newline at end of file