diff --git a/.github/actions/seal-restore/action.yml b/.github/actions/seal-restore/action.yml new file mode 100644 index 00000000000..beadad90cbc --- /dev/null +++ b/.github/actions/seal-restore/action.yml @@ -0,0 +1,82 @@ +name: "Restore sealed source code" +description: "Restore sealed source code and confirm integrity hash" + +# PROCESS +# +# 1. Exports artifact name using Prefix + GitHub Run ID (unique for each release trigger) +# 2. Compress entire source code as tarball OR given files +# 3. Create and export integrity hash for tarball +# 4. Upload artifact +# 5. Remove archive + +# USAGE +# +# - name: Seal and upload +# id: seal_source_code +# uses: ./.github/actions/seal +# with: +# artifact_name_prefix: "source" +# +# - name: Restore sealed source code +# uses: ./.github/actions/seal-restore +# with: +# integrity_hash: ${{ needs.seal_source_code.outputs.integrity_hash }} +# artifact_name: ${{ needs.seal_source_code.outputs.artifact_name }} + +# NOTES +# +# To be used together with .github/actions/seal + +inputs: + integrity_hash: + description: "Integrity hash to verify" + required: true + artifact_name: + description: "Sealed artifact name to restore" + required: true + +runs: + using: "composite" + steps: + - id: adjust-path + run: echo "${{ github.action_path }}" >> $GITHUB_PATH + shell: bash + + - name: Download artifacts + uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + with: + name: ${{ inputs.artifact_name }} + path: . + + - id: integrity_hash + name: Create integrity hash for downloaded artifact + run: | + HASH=$(sha256sum "${ARTIFACT_NAME}.tar" | awk '{print $1}') + + echo "current_hash=${HASH}" >> "$GITHUB_OUTPUT" + env: + ARTIFACT_NAME: ${{ inputs.artifact_name }} + shell: bash + + - id: verify_hash + name: Verify sealed artifact integrity hash + run: test "${CURRENT_HASH}" = "${PROVIDED_HASH}" || exit 1 + env: + ARTIFACT_NAME: ${{ inputs.artifact_name }} + PROVIDED_HASH: ${{ inputs.integrity_hash }} + CURRENT_HASH: ${{ steps.integrity_hash.outputs.current_hash }} + shell: bash + + # Restore and overwrite tarball in current directory + - id: overwrite + name: Extract tarball + run: tar -xvf "${ARTIFACT_NAME}".tar + env: + ARTIFACT_NAME: ${{ inputs.artifact_name }} + shell: bash + + - name: Remove archive + run: rm -f "${ARTIFACT_NAME}.tar" + env: + ARTIFACT_NAME: ${{ inputs.artifact_name }} + shell: bash diff --git a/.github/actions/seal/action.yml b/.github/actions/seal/action.yml new file mode 100644 index 00000000000..3438056c872 --- /dev/null +++ b/.github/actions/seal/action.yml @@ -0,0 +1,93 @@ +name: "Seal and hash source code" +description: "Seal and export source code as a tarball artifact along with its integrity hash" + +# PROCESS +# +# 1. Exports artifact name using Prefix + GitHub Run ID (unique for each release trigger) +# 2. Compress entire source code as tarball OR given files +# 3. Create and export integrity hash for tarball +# 4. Upload artifact +# 5. Remove archive + +# USAGE +# +# - name: Seal and upload +# id: seal_source_code +# uses: ./.github/actions/seal +# with: +# artifact_name_prefix: "source" + +inputs: + files: + description: "Files to seal separated by space" + required: false + artifact_name_prefix: + description: "Prefix to use when exporting artifact" + required: true + +outputs: + integrity_hash: + description: "Source code integrity hash" + value: ${{ steps.integrity_hash.outputs.integrity_hash }} + artifact_name: + description: "Artifact name containTemporary branch created with staged changed" + value: ${{ steps.export_artifact_name.outputs.artifact_name }} + +runs: + using: "composite" + steps: + - id: adjust-path + run: echo "${{ github.action_path }}" >> $GITHUB_PATH + shell: bash + + - id: export_artifact_name + name: Export final artifact name + run: echo "artifact_name=${ARTIFACT_PREFIX}-${GITHUB_RUN_ID}" >> "$GITHUB_OUTPUT" + env: + GITHUB_RUN_ID: ${{ github.run_id }} + ARTIFACT_PREFIX: ${{ inputs.artifact_name_prefix }} + shell: bash + + # By default, create a tarball of the current directory minus .git + # otherwise it breaks GH Actions when restoring it + - id: compress_all + if: ${{ !inputs.files }} + name: Create tarball for entire source + run: tar --exclude-vcs -cvf "${ARTIFACT_NAME}".tar * + env: + ARTIFACT_NAME: ${{ steps.export_artifact_name.outputs.artifact_name }} + shell: bash + + # If a list of files are given, then create a tarball for those only + - id: compress_selected_files + if: ${{ inputs.files }} + name: Create tarball for selected files + run: tar --exclude-vcs -cvf "${ARTIFACT_NAME}".tar "${FILES}" + env: + FILES: ${{ inputs.files }} + ARTIFACT_NAME: ${{ steps.export_artifact_name.outputs.artifact_name }} + shell: bash + + - id: integrity_hash + name: Create and export integrity hash for tarball + run: | + HASH=$(sha256sum "${ARTIFACT_NAME}.tar" | awk '{print $1}') + + echo "integrity_hash=${HASH}" >> "$GITHUB_OUTPUT" + env: + ARTIFACT_NAME: ${{ steps.export_artifact_name.outputs.artifact_name }} + shell: bash + + - name: Upload artifacts + uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2 + with: + if-no-files-found: error + name: ${{ steps.export_artifact_name.outputs.artifact_name }} + path: ${{ steps.export_artifact_name.outputs.artifact_name }}.tar + retention-days: 1 + + - name: Remove archive + run: rm -f "${ARTEFACT_NAME}.tar" + env: + ARTIFACT_NAME: ${{ steps.export_artifact_name.outputs.artifact_name }} + shell: bash diff --git a/.github/actions/upload-release-provenance/action.yml b/.github/actions/upload-release-provenance/action.yml new file mode 100644 index 00000000000..e4c1e52c0d2 --- /dev/null +++ b/.github/actions/upload-release-provenance/action.yml @@ -0,0 +1,67 @@ +name: "Upload provenance attestation to release" +description: "Download and upload newly generated provenance attestation to latest release." + +# PROCESS +# +# 1. Downloads provenance attestation artifact generated earlier in the release pipeline +# 2. Updates latest GitHub draft release pointing to newly git release tag +# 3. Uploads provenance attestation file to latest GitHub draft release + +# USAGE +# +# - name: Upload provenance +# id: upload-provenance +# uses: ./.github/actions/upload-release-provenance +# with: +# release_version: ${{ needs.seal.outputs.RELEASE_VERSION }} +# provenance_name: ${{needs.provenance.outputs.provenance-name}} +# github_token: ${{ secrets.GITHUB_TOKEN }} + +# NOTES +# +# There are no outputs. +# + +inputs: + provenance_name: + description: "Provenance artifact name to download" + required: true + release_version: + description: "Release version (e.g., 2.20.0)" + required: true + github_token: + description: "GitHub token for GitHub CLI" + required: true + +runs: + using: "composite" + steps: + - id: adjust-path + run: echo "${{ github.action_path }}" >> $GITHUB_PATH + shell: bash + + - id: download-provenance + name: Download newly generated provenance + uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # v3.0.1 + with: + name: ${{ inputs.provenance_name }} + + - id: sync-release-tag + name: Update draft release tag to release commit tag + run: | + CURRENT_DRAFT_RELEASE=$(gh release list | awk '{ if ($2 == "Draft") print $1}') + gh release edit "${CURRENT_DRAFT_RELEASE}" --tag v"${RELEASE_VERSION}" + env: + RELEASE_VERSION: ${{ inputs.release_version }} + GH_TOKEN: ${{ inputs.github_token }} + shell: bash + + - id: upload-provenance + name: Upload provenance to release tag + # clobber flag means overwrite release asset if available (eventual consistency, retried failed steps) + run: gh release upload --clobber v"${RELEASE_VERSION}" "${PROVENANCE_FILE}" + env: + RELEASE_VERSION: ${{ inputs.release_version }} + PROVENANCE_FILE: ${{ inputs.provenance_name }} + GH_TOKEN: ${{ inputs.github_token }} + shell: bash diff --git a/.github/actions/verify-provenance/verify_provenance.sh b/.github/actions/verify-provenance/verify_provenance.sh new file mode 100755 index 00000000000..ca8bf57c6be --- /dev/null +++ b/.github/actions/verify-provenance/verify_provenance.sh @@ -0,0 +1,111 @@ +#!/bin/bash +set -uo pipefail # prevent accessing unset env vars, prevent masking pipeline errors to the next command + +#docs +#title :verify_provenance.sh +#description :This script will download and verify a signed Powertools for AWS Lambda (Python) release build with SLSA Verifier +#author :@heitorlessa +#date :July 1st 2023 +#version :0.1 +#usage :bash verify_provenance.sh {release version} +#notes :Meant to use in GitHub Actions or locally (MacOS, Linux, WSL). +#os_version :Ubuntu 22.04.2 LTS +#============================================================================== + +# Check if RELEASE_VERSION is provided as a command line argument +if [[ $# -eq 1 ]]; then + export readonly RELEASE_VERSION="$1" +else + echo "ERROR: Please provider Powertools release version as a command line argument." + echo "Example: bash verify_provenance.sh 2.20.0" + exit 1 +fi + +export readonly ARCHITECTURE=$(uname -m | sed 's/x86_64/amd64/g') # arm64, x86_64 ->amd64 +export readonly OS_NAME=$(uname -s | tr '[:upper:]' '[:lower:]') # darwin, linux +export readonly SLSA_VERIFIER_VERSION="2.3.0" +export readonly SLSA_VERIFIER_CHECKSUM_FILE="SHA256SUM.md" +export readonly SLSA_VERIFIER_BINARY="./slsa-verifier-${OS_NAME}-${ARCHITECTURE}" + +export readonly RELEASE_BINARY="aws_lambda_powertools-${RELEASE_VERSION}-py3-none-any.whl" +export readonly ORG="aws-powertools" +export readonly REPO="powertools-lambda-python" +export readonly PROVENANCE_FILE="multiple.intoto.jsonl" + +export readonly FILES=("${SLSA_VERIFIER_BINARY}" "${SLSA_VERIFIER_CHECKSUM_FILE}" "${PROVENANCE_FILE}" "${RELEASE_BINARY}") + +function debug() { + TIMESTAMP=$(date -u "+%FT%TZ") # 2023-05-10T07:53:59Z + echo ""${TIMESTAMP}" DEBUG - $1" +} + +function download_slsa_verifier() { + debug "[*] Downloading SLSA Verifier for - Binary: slsa-verifier-${OS_NAME}-${ARCHITECTURE}" + curl --location --silent -O "https://github.com/slsa-framework/slsa-verifier/releases/download/v${SLSA_VERIFIER_VERSION}/slsa-verifier-${OS_NAME}-${ARCHITECTURE}" + + debug "[*] Downloading SLSA Verifier checksums" + curl --location --silent -O "https://raw.githubusercontent.com/slsa-framework/slsa-verifier/f59b55ef2190581d40fc1a5f3b7a51cab2f4a652/${SLSA_VERIFIER_CHECKSUM_FILE}" + + debug "[*] Verifying SLSA Verifier binary integrity" + CURRENT_HASH=$(sha256sum "${SLSA_VERIFIER_BINARY}" | awk '{print $1}') + if [[ $(grep "${CURRENT_HASH}" "${SLSA_VERIFIER_CHECKSUM_FILE}") ]]; then + debug "[*] SLSA Verifier binary integrity confirmed" + chmod +x "${SLSA_VERIFIER_BINARY}" + else + debug "[!] Failed integrity check for SLSA Verifier binary: ${SLSA_VERIFIER_BINARY}" + exit 1 + fi +} + +function download_provenance() { + debug "[*] Downloading attestation for - Release: https://github.com/${ORG}/${REPO}/releases/v${RELEASE_VERSION}" + + curl --location --silent -O "https://github.com/${ORG}/${REPO}/releases/download/v${RELEASE_VERSION}/${PROVENANCE_FILE}" +} + +function download_release_artifact() { + debug "[*] Downloading ${RELEASE_VERSION} release from PyPi" + python -m pip download \ + --only-binary=:all: \ + --no-deps \ + --quiet \ + aws-lambda-powertools=="${RELEASE_VERSION}" +} + +function verify_provenance() { + debug "[*] Verifying attestation with slsa-verifier" + "${SLSA_VERIFIER_BINARY}" verify-artifact \ + --provenance-path "${PROVENANCE_FILE}" \ + --source-uri github.com/${ORG}/${REPO} \ + ${RELEASE_BINARY} +} + +function cleanup() { + debug "[*] Cleaning up previously downloaded files" + rm "${SLSA_VERIFIER_BINARY}" + rm "${SLSA_VERIFIER_CHECKSUM_FILE}" + rm "${PROVENANCE_FILE}" + rm "${RELEASE_BINARY}" + echo "${FILES[@]}" | xargs -n1 echo "Removed file: " +} + +function main() { + download_slsa_verifier + download_provenance + download_release_artifact + verify_provenance + cleanup +} + +main + +# Lessons learned +# +# 1. If source doesn't match provenance +# +# FAILED: SLSA verification failed: source used to generate the binary does not match provenance: expected source 'awslabs/aws-lambda-powertools-python', got 'heitorlessa/aws-lambda-powertools-test' +# +# 2. Avoid building deps during download in Test registry endpoints +# +# FAILED: Could not find a version that satisfies the requirement poetry-core>=1.3.2 (from versions: 1.2.0) +# diff --git a/.github/workflows/publish_v2_layer.yml b/.github/workflows/publish_v2_layer.yml index 0a139da79be..82ff1488f55 100644 --- a/.github/workflows/publish_v2_layer.yml +++ b/.github/workflows/publish_v2_layer.yml @@ -24,6 +24,8 @@ name: Deploy v2 layer to all regions # with: # latest_published_version: ${{ needs.seal.outputs.RELEASE_VERSION }} # pre_release: ${{ inputs.pre_release }} +# source_code_artifact_name: ${{ needs.seal.outputs.artifact_name }} +# source_code_integrity_hash: ${{ needs.seal.outputs.integrity_hash }} on: @@ -32,6 +34,14 @@ on: latest_published_version: description: "Latest PyPi published version to rebuild latest docs for, e.g. 2.0.0, 2.0.0a1 (pre-release)" required: true + source_code_artifact_name: + description: "Artifact name to restore sealed source code" + type: string + required: true + source_code_integrity_hash: + description: "Sealed source code integrity hash" + type: string + required: true pre_release: description: "Publishes documentation using a pre-release tag (2.0.0a1)." default: false @@ -48,10 +58,22 @@ on: default: false type: boolean required: false + source_code_artifact_name: + description: "Artifact name to restore sealed source code" + type: string + required: true + source_code_integrity_hash: + description: "Sealed source code integrity hash" + type: string + required: true permissions: contents: read + +env: + RELEASE_COMMIT: ${{ github.sha }} + jobs: build-layer: permissions: @@ -68,9 +90,16 @@ jobs: - name: checkout uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 with: - fetch-depth: 0 + ref: ${{ env.RELEASE_COMMIT }} + + - name: Restore sealed source code + uses: ./.github/actions/seal-restore + with: + integrity_hash: ${{ inputs.source_code_integrity_hash }} + artifact_name: ${{ inputs.source_code_artifact_name }} + - name: Install poetry - run: pipx install poetry + run: pipx install git+https://github.com/python-poetry/poetry@68b88e5390720a3dd84f02940ec5200bfce39ac6 # v1.5.0 - name: Setup Node.js uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 with: @@ -128,6 +157,8 @@ jobs: artefact-name: "cdk-layer-artefact" environment: "layer-beta" latest_published_version: ${{ inputs.latest_published_version }} + source_code_artifact_name: ${{ inputs.source_code_artifact_name }} + source_code_integrity_hash: ${{ inputs.source_code_integrity_hash }} prod: needs: beta @@ -144,6 +175,8 @@ jobs: artefact-name: "cdk-layer-artefact" environment: "layer-prod" latest_published_version: ${{ inputs.latest_published_version }} + source_code_artifact_name: ${{ inputs.source_code_artifact_name }} + source_code_integrity_hash: ${{ inputs.source_code_integrity_hash }} sar-beta: needs: beta # canaries run on Layer Beta env @@ -160,6 +193,9 @@ jobs: artefact-name: "cdk-layer-artefact" environment: "layer-beta" package-version: ${{ inputs.latest_published_version }} + source_code_artifact_name: ${{ inputs.source_code_artifact_name }} + source_code_integrity_hash: ${{ inputs.source_code_integrity_hash }} + sar-prod: needs: sar-beta @@ -176,6 +212,9 @@ jobs: artefact-name: "cdk-layer-artefact" environment: "layer-prod" package-version: ${{ inputs.latest_published_version }} + source_code_artifact_name: ${{ inputs.source_code_artifact_name }} + source_code_integrity_hash: ${{ inputs.source_code_integrity_hash }} + # Updating the documentation with the latest Layer ARNs is a two-phase process # @@ -201,7 +240,14 @@ jobs: - name: Checkout repository # reusable workflows start clean, so we need to checkout again uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 with: - fetch-depth: 0 + ref: ${{ env.RELEASE_COMMIT }} + + - name: Restore sealed source code + uses: ./.github/actions/seal-restore + with: + integrity_hash: ${{ inputs.source_code_integrity_hash }} + artifact_name: ${{ inputs.source_code_artifact_name }} + - name: Download CDK layer artifact uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 566fe9db3da..96b66d71823 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,14 +7,14 @@ name: Release # 1. [Seal] Bump to release version and export source code with integrity hash # 2. [Quality check] Restore sealed source code, run tests, linting, security and complexity base line # 3. [Build] Restore sealed source code, create and export hashed build artifact for PyPi release (wheel, tarball) -# 4. [Release] Restore built artifact, and publish package to PyPi prod repository -# 5. [Create Tag] Restore sealed source code, and create a new git tag using released version -# 6. [PR to bump version] Restore sealed source code, and create a PR to update trunk with latest released project metadata -# 7. [Publish Layer] Compile Layer and kick off pipeline for beta, prod, and canary releases -# 8. [Publish Layer] Update docs with latest Layer ARNs and Changelog -# 9. [Publish Layer] Create PR to update trunk so staged docs also point to the latest Layer ARN, when merged -# 10. [Publish Layer] Builds a new user guide and API docs with release version; update /latest pointing to newly released version -# 11. [Post release] Close all issues labeled "pending-release" and notify customers about the release +# 4. [Provenance] Generates provenance for build, signs attestation with GitHub OIDC claims to confirm it came from this release pipeline, commit, org, repo, branch, hash, etc. +# 5. [Release] Restore built artifact, and publish package to PyPi prod repository +# 6. [Create Tag] Restore sealed source code, create a new git tag using released version, uploads provenance to latest draft release +# 7. [PR to bump version] Restore sealed source code, and create a PR to update trunk with latest released project metadata +# 8. [Publish Layer] Compile Layer and kick off pipeline for beta, prod, and canary releases +# 9. [Publish Layer] Update docs with latest Layer ARNs and Changelog +# 10. [Publish Layer] Create PR to update trunk so staged docs also point to the latest Layer ARN, when merged +# 12. [Post release] Close all issues labeled "pending-release" and notify customers about the release # # === Manual activities === # @@ -22,7 +22,11 @@ name: Release # 2. Update draft release notes after this workflow completes # 3. If not already set, use `v` as a tag, e.g., v1.26.4, and select develop as target branch +# NOTE +# # See MAINTAINERS.md "Releasing a new version" for release mechanisms +# +# Every job is isolated and starts a new fresh container. env: RELEASE_COMMIT: ${{ github.sha }} @@ -65,7 +69,8 @@ jobs: permissions: contents: read outputs: - SOURCE_CODE_HASH: ${{ steps.integrity.outputs.SOURCE_CODE_HASH }} + integrity_hash: ${{ steps.seal_source_code.outputs.integrity_hash }} + artifact_name: ${{ steps.seal_source_code.outputs.artifact_name }} RELEASE_VERSION: ${{ steps.release_version.outputs.RELEASE_VERSION }} steps: - name: Export release version @@ -83,7 +88,7 @@ jobs: - name: Install poetry run: | pipx install git+https://github.com/python-poetry/poetry@68b88e5390720a3dd84f02940ec5200bfce39ac6 # v1.5.0 - pipx inject poetry git+https://github.com/monim67/poetry-bumpversion@ef49c63acef7fe8680789ddb31f376cc898f0012 # v0.3.0 + pipx inject poetry git+https://github.com/monim67/poetry-bumpversion@315fe3324a699fa12ec20e202eb7375d4327d1c4 # v0.3.1 - name: Bump package version id: versioning @@ -91,27 +96,11 @@ jobs: env: RELEASE_VERSION: ${{ steps.release_version.outputs.RELEASE_VERSION}} - - name: Create integrity hash - id: integrity - run: echo "SOURCE_CODE_HASH=${HASH}" >> "$GITHUB_OUTPUT" - env: - # paths to hash and why they're important to protect - # - # aws_lambda_powertools/ - source code - # pyproject.toml - project metadata - # poetry.lock - project dependencies - # layer/ - layer infrastructure and pipeline - # .github/ - github scripts and actions used in the release - # docs/ - user guide documentation - # examples/ - user guide code snippets - HASH: ${{ hashFiles('aws_lambda_powertools/**', 'pyproject.toml', 'poetry.lock', 'layer/**', '.github/**', 'docs/**', 'examples/**')}} - - - name: Upload sealed source code - uses: ./.github/actions/upload-artifact + - name: Seal and upload + id: seal_source_code + uses: ./.github/actions/seal with: - name: source-${{ steps.integrity.outputs.SOURCE_CODE_HASH }} - path: . - + artifact_name_prefix: "source" # This job runs our automated test suite, complexity and security baselines # it ensures previously merged have been tested as part of the pull request process @@ -124,8 +113,6 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - env: - SOURCE_INTEGRITY_HASH: ${{ needs.seal.outputs.SOURCE_CODE_HASH }} steps: # NOTE: we need actions/checkout to configure git first (pre-commit hooks in make dev) - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 @@ -133,10 +120,10 @@ jobs: ref: ${{ env.RELEASE_COMMIT }} - name: Restore sealed source code - uses: ./.github/actions/download-artifact + uses: ./.github/actions/seal-restore with: - name: source-${{ env.SOURCE_INTEGRITY_HASH }} - path: . + integrity_hash: ${{ needs.seal.outputs.integrity_hash }} + artifact_name: ${{ needs.seal.outputs.artifact_name }} - name: Debug cache restore run: cat pyproject.toml @@ -157,16 +144,16 @@ jobs: # it checks out code from release commit for custom actions to work # then restores the sealed source code (overwrites any potential tampering) # it's done separately from release job to enforce least privilege. - # We export just the final build artifact for release (release-) + # We export just the final build artifact for release build: runs-on: ubuntu-latest needs: [quality_check, seal] permissions: contents: read outputs: - BUILD_INTEGRITY_HASH: ${{ steps.integrity.outputs.BUILD_INTEGRITY_HASH }} - env: - SOURCE_INTEGRITY_HASH: ${{ needs.seal.outputs.SOURCE_CODE_HASH }} + integrity_hash: ${{ steps.seal_build.outputs.integrity_hash }} + artifact_name: ${{ steps.seal_build.outputs.artifact_name }} + attestation_hashes: ${{ steps.encoded_hash.outputs.attestation_hashes }} steps: # NOTE: we need actions/checkout to configure git first (pre-commit hooks in make dev) - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 @@ -174,10 +161,10 @@ jobs: ref: ${{ env.RELEASE_COMMIT }} - name: Restore sealed source code - uses: ./.github/actions/download-artifact + uses: ./.github/actions/seal-restore with: - name: source-${{ env.SOURCE_INTEGRITY_HASH }} - path: . + integrity_hash: ${{ needs.seal.outputs.integrity_hash }} + artifact_name: ${{ needs.seal.outputs.artifact_name }} - name: Install poetry run: pipx install git+https://github.com/python-poetry/poetry@68b88e5390720a3dd84f02940ec5200bfce39ac6 # v1.5.0 @@ -190,64 +177,69 @@ jobs: - name: Build python package and wheel run: poetry build - # NOTE: Ran out of time to create a composite action out of this - # because GitHub Action inputs do not support arrays and it became fragile to join multiple strings then split - # keeping these hard coded for now until we have a cleaner way to reuse files/dirs we want to hash - - name: Source code tampering check - run: test "${SOURCE_INTEGRITY_HASH}" = "${CURRENT_HASH}" || exit 1 - env: - CURRENT_HASH: ${{ hashFiles('aws_lambda_powertools/**', 'pyproject.toml', 'poetry.lock', 'layer/**', '.github/**', 'docs/**', 'examples/**')}} - - - name: Create integrity hash for build artifact - id: integrity - run: echo "BUILD_INTEGRITY_HASH=${HASH}" >> "$GITHUB_OUTPUT" - env: - # paths to hash and why they're important to protect - # - # dist/ - package distribution build - HASH: ${{ hashFiles('dist/**')}} - - - name: Upload build artifact - uses: ./.github/actions/upload-artifact + - name: Seal and upload + id: seal_build + uses: ./.github/actions/seal with: - name: build-${{ steps.integrity.outputs.BUILD_INTEGRITY_HASH}} - path: dist/ + artifact_name_prefix: "build" + files: "dist/" + + # NOTE: SLSA retraces our build to its artifact to ensure it wasn't tampered + # coupled with GitHub OIDC, SLSA can then confidently sign it came from this release pipeline+commit+branch+org+repo+actor+integrity hash + - name: Create attestation encoded hash for provenance + id: encoded_hash + working-directory: dist + run: echo "attestation_hashes=$(sha256sum ./* | base64 -w0)" >> "$GITHUB_OUTPUT" + + # This job creates a provenance file that describes how our release was built (all steps) + # after it verifies our build is reproducible within the same pipeline + # it confirms that its own software and the CI build haven't been tampered with (Trust but verify) + # lastly, it creates and sign an attestation (multiple.intoto.jsonl) that confirms + # this build artifact came from this GitHub org, branch, actor, commit ID, inputs that triggered this pipeline, and matches its integrity hash + # NOTE: supply chain threats review (we protect against all of them now): https://slsa.dev/spec/v1.0/threats-overview + provenance: + needs: [seal, build] + permissions: + contents: write # nested job explicitly require despite upload assets being set to false + actions: read # To read the workflow path. + id-token: write # To sign the provenance. + # NOTE: provenance fails if we use action pinning... it's a Github limitation + # because SLSA needs to trace & attest it came from a given branch; pinning doesn't expose that information + # https://github.com/slsa-framework/slsa-github-generator/blob/main/internal/builders/generic/README.md#referencing-the-slsa-generator + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.7.0 + with: + base64-subjects: ${{ needs.build.outputs.attestation_hashes }} + upload-assets: false # we upload its attestation in create_tag job, otherwise it creates a new release # This job uses release artifact to publish to PyPi # it exchanges JWT tokens with GitHub to obtain PyPi credentials # since it's already registered as a Trusted Publisher. # It uses the sealed build artifact (.whl, .tar.gz) to release it release: - needs: [build, seal] + needs: [build, seal, provenance] environment: release runs-on: ubuntu-latest permissions: id-token: write # OIDC for PyPi Trusted Publisher feature env: RELEASE_VERSION: ${{ needs.seal.outputs.RELEASE_VERSION }} - BUILD_INTEGRITY_HASH: ${{ needs.build.outputs.BUILD_INTEGRITY_HASH }} steps: # NOTE: we need actions/checkout in order to use our local actions (e.g., ./.github/actions) - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 with: ref: ${{ env.RELEASE_COMMIT }} - - name: Restore sealed build - uses: ./.github/actions/download-artifact + - name: Restore sealed source code + uses: ./.github/actions/seal-restore with: - name: build-${{ env.BUILD_INTEGRITY_HASH }} - path: . - - - name: Source code tampering check - run: test "${BUILD_INTEGRITY_HASH}" = "${CURRENT_HASH}" || exit 1 - env: - CURRENT_HASH: ${{ hashFiles('dist/**')}} + integrity_hash: ${{ needs.build.outputs.integrity_hash }} + artifact_name: ${{ needs.build.outputs.artifact_name }} - name: Upload to PyPi prod if: ${{ !inputs.skip_pypi }} uses: pypa/gh-action-pypi-publish@f5622bde02b04381239da3573277701ceca8f6a0 # v1.8.7 - # March 1st: PyPi test is under maintenance.... + # PyPi test maintenance affected us numerous times, leaving for history purposes # - name: Upload to PyPi test # if: ${{ !inputs.skip_pypi }} # uses: pypa/gh-action-pypi-publish@f5622bde02b04381239da3573277701ceca8f6a0 # v1.8.7 @@ -261,12 +253,10 @@ jobs: # otherwise the release commit will be used as the basis for the tag. # Later, we create a PR to update trunk with our newest release version (e.g., bump_version job) create_tag: - needs: [release, seal] + needs: [release, seal, provenance] runs-on: ubuntu-latest permissions: contents: write - env: - SOURCE_INTEGRITY_HASH: ${{ needs.seal.outputs.SOURCE_CODE_HASH }} steps: # NOTE: we need actions/checkout to authenticate and configure git first - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 @@ -274,18 +264,10 @@ jobs: ref: ${{ env.RELEASE_COMMIT }} - name: Restore sealed source code - uses: ./.github/actions/download-artifact + uses: ./.github/actions/seal-restore with: - name: source-${{ env.SOURCE_INTEGRITY_HASH }} - path: . - - # NOTE: Ran out of time to create a composite action out of this - # because GitHub Action inputs do not support arrays and it became fragile when making it reusable with strings - # keeping these hard coded for now until we have a cleaner way to reuse files/dirs we want to hash - - name: Source code tampering check - run: test "${SOURCE_INTEGRITY_HASH}" = "${CURRENT_HASH}" || exit 1 - env: - CURRENT_HASH: ${{ hashFiles('aws_lambda_powertools/**', 'pyproject.toml', 'poetry.lock', 'layer/**', '.github/**', 'docs/**', 'examples/**')}} + integrity_hash: ${{ needs.seal.outputs.integrity_hash }} + artifact_name: ${{ needs.seal.outputs.artifact_name }} - id: setup-git name: Git client setup and refresh tip @@ -303,6 +285,14 @@ jobs: env: RELEASE_VERSION: ${{ needs.seal.outputs.RELEASE_VERSION }} + - name: Upload provenance + id: upload-provenance + uses: ./.github/actions/upload-release-provenance + with: + release_version: ${{ needs.seal.outputs.RELEASE_VERSION }} + provenance_name: ${{needs.provenance.outputs.provenance-name}} + github_token: ${{ secrets.GITHUB_TOKEN }} + # Creates a PR with the latest version we've just released # since our trunk is protected against any direct pushes from automation bump_version: @@ -311,8 +301,6 @@ jobs: contents: write # create-pr action creates a temporary branch pull-requests: write # create-pr action creates a PR using the temporary branch runs-on: ubuntu-latest - env: - SOURCE_INTEGRITY_HASH: ${{ needs.seal.outputs.SOURCE_CODE_HASH }} steps: # NOTE: we need actions/checkout to authenticate and configure git first - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 @@ -320,18 +308,10 @@ jobs: ref: ${{ env.RELEASE_COMMIT }} - name: Restore sealed source code - uses: ./.github/actions/download-artifact + uses: ./.github/actions/seal-restore with: - name: source-${{ env.SOURCE_INTEGRITY_HASH }} - path: . - - # NOTE: Ran out of time to create a composite action out of this - # because GitHub Action inputs do not support arrays and it became fragile when making it reusable with strings - # keeping these hard coded for now until we have a cleaner way to reuse files/dirs we want to hash - - name: Source code tampering check - run: test "${SOURCE_INTEGRITY_HASH}" = "${CURRENT_HASH}" || exit 1 - env: - CURRENT_HASH: ${{ hashFiles('aws_lambda_powertools/**', 'pyproject.toml', 'poetry.lock', 'layer/**', '.github/**', 'docs/**', 'examples/**')}} + integrity_hash: ${{ needs.seal.outputs.integrity_hash }} + artifact_name: ${{ needs.seal.outputs.artifact_name }} - name: Create PR id: create-pr @@ -363,6 +343,8 @@ jobs: with: latest_published_version: ${{ needs.seal.outputs.RELEASE_VERSION }} pre_release: ${{ inputs.pre_release }} + source_code_artifact_name: ${{ needs.seal.outputs.artifact_name }} + source_code_integrity_hash: ${{ needs.seal.outputs.integrity_hash }} post_release: needs: [seal, release, publish_layer] @@ -378,6 +360,13 @@ jobs: - uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 with: ref: ${{ env.RELEASE_COMMIT }} + + - name: Restore sealed source code + uses: ./.github/actions/seal-restore + with: + integrity_hash: ${{ needs.seal.outputs.integrity_hash }} + artifact_name: ${{ needs.seal.outputs.artifact_name }} + - name: Close issues related to this release uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 with: diff --git a/.github/workflows/reusable_deploy_v2_layer_stack.yml b/.github/workflows/reusable_deploy_v2_layer_stack.yml index 3186009493e..360f4c8fbc9 100644 --- a/.github/workflows/reusable_deploy_v2_layer_stack.yml +++ b/.github/workflows/reusable_deploy_v2_layer_stack.yml @@ -46,10 +46,21 @@ on: description: "Latest version that is published" required: true type: string + source_code_artifact_name: + description: "Artifact name to restore sealed source code" + type: string + required: true + source_code_integrity_hash: + description: "Sealed source code integrity hash" + type: string + required: true permissions: contents: read +env: + RELEASE_COMMIT: ${{ github.sha }} # it gets propagated from the caller for security reasons + jobs: deploy-cdk-stack: runs-on: ubuntu-latest @@ -126,8 +137,17 @@ jobs: steps: - name: checkout uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + with: + ref: ${{ env.RELEASE_COMMIT }} + + - name: Restore sealed source code + uses: ./.github/actions/seal-restore + with: + integrity_hash: ${{ inputs.source_code_integrity_hash }} + artifact_name: ${{ inputs.source_code_artifact_name }} + - name: Install poetry - run: pipx install poetry + run: pipx install git+https://github.com/python-poetry/poetry@68b88e5390720a3dd84f02940ec5200bfce39ac6 # v1.5.0 - name: aws credentials uses: aws-actions/configure-aws-credentials@5fd3084fc36e372ff1fff382a39b10d03659f355 # v2.2.0 with: diff --git a/.github/workflows/reusable_deploy_v2_sar.yml b/.github/workflows/reusable_deploy_v2_sar.yml index 1ca76a2a706..beab36f24c2 100644 --- a/.github/workflows/reusable_deploy_v2_sar.yml +++ b/.github/workflows/reusable_deploy_v2_sar.yml @@ -28,6 +28,8 @@ name: Deploy V2 SAR # artefact-name: "cdk-layer-artefact" # environment: "layer-beta" # package-version: ${{ inputs.latest_published_version }} +# source_code_artifact_name: ${{ inputs.source_code_artifact_name }} +# source_code_integrity_hash: ${{ inputs.source_code_integrity_hash }} permissions: id-token: write @@ -38,6 +40,7 @@ env: AWS_REGION: eu-west-1 SAR_NAME: aws-lambda-powertools-python-layer TEST_STACK_NAME: serverlessrepo-v2-powertools-layer-test-stack + RELEASE_COMMIT: ${{ github.sha }} # it gets propagated from the caller for security reasons on: workflow_call: @@ -58,6 +61,14 @@ on: description: "GitHub Environment to use for encrypted secrets" required: true type: string + source_code_artifact_name: + description: "Artifact name to restore sealed source code" + type: string + required: true + source_code_integrity_hash: + description: "Sealed source code integrity hash" + type: string + required: true jobs: deploy-sar-app: @@ -67,13 +78,28 @@ jobs: matrix: architecture: ["x86_64", "arm64"] steps: - - name: Checkout + - name: checkout uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + with: + ref: ${{ env.RELEASE_COMMIT }} + + - name: Restore sealed source code + uses: ./.github/actions/seal-restore + with: + integrity_hash: ${{ inputs.source_code_integrity_hash }} + artifact_name: ${{ inputs.source_code_artifact_name }} + + - name: AWS credentials uses: aws-actions/configure-aws-credentials@5fd3084fc36e372ff1fff382a39b10d03659f355 # v2.2.0 with: aws-region: ${{ env.AWS_REGION }} role-to-assume: ${{ secrets.AWS_LAYERS_ROLE_ARN }} + + # NOTE + # We connect to Layers account to log our intent to publish a SAR Layer + # we then jump to our specific SAR Account with the correctly scoped IAM Role + # this allows us to have a single trail when a release occurs for a given layer (beta+prod+SAR beta+SAR prod) - name: AWS credentials SAR role uses: aws-actions/configure-aws-credentials@5fd3084fc36e372ff1fff382a39b10d03659f355 # v2.2.0 id: aws-credentials-sar-role diff --git a/.github/workflows/secure_workflows.yml b/.github/workflows/secure_workflows.yml index 1b29de4bc0c..418c4c446a5 100644 --- a/.github/workflows/secure_workflows.yml +++ b/.github/workflows/secure_workflows.yml @@ -33,3 +33,5 @@ jobs: uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 - name: Ensure 3rd party workflows have SHA pinned uses: zgosalvez/github-actions-ensure-sha-pinned-actions@f32435541e24cd6a4700a7f52bb2ec59e80603b1 # v2.1.4 + with: + allowlist: slsa-framework/slsa-github-generator diff --git a/MAINTAINERS.md b/MAINTAINERS.md index bc75596003b..be831ef46d4 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -220,18 +220,23 @@ section Build Checksum : active, 8s Build release artifact : active, 39s Seal : active, 8s +section Provenance + Attest build : active, 8s + Sign attestation : active, attestation, 10:06, 8s + section Release Checksum : active, 8s PyPi temp credentials : active, 8s - Publish PyPi : active, pypi, 10:06, 29s + Publish PyPi : active, pypi, 10:07, 29s -PyPi release : milestone, m2, 10:06,1s +PyPi release : milestone, m2, 10:07,1s section Git release Checksum : active, after pypi, 8s Git Tag : active, 8s Bump package version : active, 8s Create PR : active, 8s + Upload attestation : active, 8s section Layer release Build (x86+ARM) : active, layer_build, 10:08, 6m @@ -323,6 +328,13 @@ timeline : Build release artifact : Seal and upload artifact + Provenance : Detect build environment + : Generate SLSA Builder + : Verify SLSA Builder provenance + : Create and sign provenance + : Seal and upload artifact + : Write to public ledger + Release : Restore sealed build : Integrity check : PyPi ephemeral credentials diff --git a/SECURITY.md b/SECURITY.md index 9ac1494b13d..885ffb18f2c 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,8 +1,7 @@ -## Reporting a Vulnerability +## Reporting a vulnerability -If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security -via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/) or directly via email to aws-security@amazon.com. +If you discover a potential security issue in this project, we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/) or directly via email to . Please do **not** create a public GitHub issue. diff --git a/docs/media/continuous_deployment_practices.png b/docs/media/continuous_deployment_practices.png new file mode 100644 index 00000000000..645190fb9ae Binary files /dev/null and b/docs/media/continuous_deployment_practices.png differ diff --git a/docs/media/continuous_integration_practices.png b/docs/media/continuous_integration_practices.png new file mode 100644 index 00000000000..03a69a477a3 Binary files /dev/null and b/docs/media/continuous_integration_practices.png differ diff --git a/docs/security.md b/docs/security.md new file mode 100644 index 00000000000..edaedaf1ecd --- /dev/null +++ b/docs/security.md @@ -0,0 +1,110 @@ +--- +title: Security +description: Security practices and processes for Powertools for AWS Lambda (Python) +--- + + + +## Overview + +[![Open Source Security Foundation Best Practices](https://bestpractices.coreinfrastructure.org/projects/7535/badge)](https://bestpractices.coreinfrastructure.org/projects/7535) + +This page describes our security processes and supply chain practices. + +!!! info "We continuously check and evolve our practices, therefore it is possible some diagrams may be eventually consistent." + +--8<-- "SECURITY.md" + +## Supply chain + +### Verifying signed builds + +!!! note "Starting from v2.20.0 releases, builds are [reproducible](https://slsa.dev/spec/v0.1/faq#q-what-about-reproducible-builds){target="_blank"} and signed publicly." + +
+![SLSA Supply Chain Threats](https://slsa.dev/images/v1.0/supply-chain-threats.svg) + +Supply Chain Threats visualized by SLSA +
+ +#### Terminology + +We use [SLSA](https://slsa.dev/spec/v1.0/about){target="_blank"} to ensure our builds are reproducible and to adhere to [supply chain security practices](https://slsa.dev/spec/v1.0/threats-overview). + +Within our [releases page](https://github.com/aws-powertools/powertools-lambda-python/releases), you will notice a new metadata file: `multiple.intoto.jsonl`. It's metadata to describe **where**, **when**, and **how** our build artifacts were produced - or simply, **attestation** in SLSA terminology. + +For this to be useful, we need a **verification tool** - [SLSA Verifier](https://github.com/slsa-framework/slsa-verifier). SLSA Verifier decodes attestation to confirm the authenticity, identity, and the steps we took in our release pipeline (_e.g., inputs, git commit/branch, GitHub org/repo, build SHA256, etc._). + +#### HOWTO + +You can do this manually or automated via a shell script. We maintain the latter to ease adoption in CI systems (feel free to modify to your needs). + +=== "Manually" + + * Download [SLSA Verifier binary](https://github.com/slsa-framework/slsa-verifier#download-the-binary) + * Download the [latest release artifact from PyPi](https://pypi.org/project/aws-lambda-powertools/#files) (either wheel or tar.gz ) + * Download `multiple.intoto.jsonl` attestation from the [latest release](https://github.com/aws-powertools/powertools-lambda-python/releases/latest) under _Assets_ + + !!! note "Next steps assume macOS as the operating system, and release v2.20.0" + + You should have the following files in the current directory: + + * **SLSA Verifier tool**: `slsa-verifier-darwin-arm64` + * **Powertools Release artifact**: `aws_lambda_powertools-2.20.0-py3-none-any.whl` + * **Powertools attestation**: `multiple.intoto.jsonl` + + You can now run SLSA Verifier with the following options: + + ```bash + ./slsa-verifier-darwin-arm64 verify-artifact \ + --provenance-path "multiple.intoto.jsonl" \ + --source-uri github.com/aws-powertools/powertools-lambda-python \ + aws_lambda_powertools-2.20.0-py3-none-any.whl + ``` + +=== "Automated" + + ```shell title="Verifying a release with verify_provenance.sh script" + bash verify_provenance.sh 2.20.0 + ``` + + !!! question "Wait, what does this script do?" + + I'm glad you asked! It takes the following actions: + + 1. **Downloads SLSA Verifier** using the pinned version (_e.g., 2.3.0) + 2. **Verifies the integrity** of our newly downloaded SLSA Verifier tool + 3. **Downloads attestation** file for the given release version + 4. **Downloads `aws-lambda-powertools`** release artifact from PyPi for the given release version + 5. **Runs SLSA Verifier against attestation**, GitHub Source, and release binary + 6. **Cleanup** by removing downloaded files to keep your current directory tidy + + ??? info "Expand or [click here](https://github.com/heitorlessa/aws-lambda-powertools-python/blob/refactor/ci-seal/.github/actions/verify-provenance/verify_provenance.sh#L95){target="_blank"} to see the script source code" + + ```bash title=".github/actions/verify-provenance/verify_provenance.sh" + ---8<-- ".github/actions/verify-provenance/verify_provenance.sh" + ``` + +### Continuous integration practices + +!!! note "We adhere to industry recommendations from the [OSSF Scorecard project](https://bestpractices.coreinfrastructure.org/en/criteria){target="_blank"}, among [others](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions){target="_blank"}." + +Since all code changes require a pull request (PR) along with one or more reviewers, we automate quality and security checks **before**, **during**, and **after** a PR is merged to trunk (`develop`). + +This is a snapshot of our automated checks at a glance. + + + +![Continuous Integration practices](./media/continuous_integration_practices.png) + +### Continuous deployment practices + +!!! note "We adhere to industry recommendations from the [OSSF Scorecard project](https://bestpractices.coreinfrastructure.org/en/criteria){target="_blank"}, among [others](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions){target="_blank"}." + +Releases are triggered by maintainers along with a reviewer - [detailed info here](https://github.com/aws-powertools/powertools-lambda-python/blob/develop/MAINTAINERS.md#releasing-a-new-version){target="_blank"}. In addition to [checks that run for every code change](#continuous-integration-practices), our pipeline requires a manual approval before releasing. + +We use a combination of provenance and signed attestation for our builds, source code sealing, SAST scanners, Python specific static code analysis, ephemeral credentials that last a given job step, and more. + +This is a snapshot of our automated checks at a glance. + +![Continuous Deployment practices](./media/continuous_deployment_practices.png) diff --git a/mkdocs.yml b/mkdocs.yml index b2de3b79a96..2315152a3c9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -12,6 +12,7 @@ nav: - API reference: api/" target="_blank - Upgrade guide: upgrade.md - We Made This (Community): we_made_this.md + - Security: security.md - Core utilities: - core/tracer.md - core/logger.md