From 98535f82f291d8525a85079c7ca359c3b34457fd Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 11 Aug 2026 14:44:00 +0000 Subject: [PATCH] SRE-904: Extract the secret handoff into shared actions --- .github/actions/decrypt-secret/action.yml | 43 +++++++++++++++++++ .github/actions/encrypt-secret/action.yml | 32 ++++++++++++++ .../workflows/housekeeping-dependencies.yml | 32 ++++++-------- 3 files changed, 88 insertions(+), 19 deletions(-) create mode 100644 .github/actions/decrypt-secret/action.yml create mode 100644 .github/actions/encrypt-secret/action.yml diff --git a/.github/actions/decrypt-secret/action.yml b/.github/actions/decrypt-secret/action.yml new file mode 100644 index 0000000..c15240d --- /dev/null +++ b/.github/actions/decrypt-secret/action.yml @@ -0,0 +1,43 @@ +name: Decrypt secret +description: >- + Decrypt a value produced by encrypt-secret, masking the plaintext before + anything else can read it. Run this before any step that executes code you + do not control. + +inputs: + encrypted_value: + description: Base64 ciphertext from encrypt-secret. + required: true + encryption_key: + description: Symmetric passphrase shared with the encrypting job. + required: true + +outputs: + value: + description: Decrypted plaintext, masked in logs. + value: ${{ steps.decrypt.outputs.value }} + +runs: + using: composite + steps: + - name: Decrypt + id: decrypt + shell: bash + env: + ENCRYPTED_VALUE: ${{ inputs.encrypted_value }} + ENC_KEY: ${{ inputs.encryption_key }} + run: | + : "${ENCRYPTED_VALUE:?}" "${ENC_KEY:?}" + value=$(printf '%s' "${ENCRYPTED_VALUE}" | openssl enc -d -aes-256-cbc -pbkdf2 -pass env:ENC_KEY -base64 -A) + # Masking is per line, so a multi-line value needs each line masked. + while IFS= read -r line; do + if [ -n "${line}" ]; then + echo "::add-mask::${line}" + fi + done <<<"${value}" + delimiter=$(openssl rand -hex 16) + { + echo "value<<${delimiter}" + echo "${value}" + echo "${delimiter}" + } >>"${GITHUB_OUTPUT}" diff --git a/.github/actions/encrypt-secret/action.yml b/.github/actions/encrypt-secret/action.yml new file mode 100644 index 0000000..0ca1482 --- /dev/null +++ b/.github/actions/encrypt-secret/action.yml @@ -0,0 +1,32 @@ +name: Encrypt secret +description: >- + Encrypt a value so it can cross a job boundary. GitHub silently drops masked + plaintext from job outputs; ciphertext survives the trip and is useless + without the key. + +inputs: + value: + description: Plaintext to encrypt. Must never be echoed. + required: true + encryption_key: + description: Symmetric passphrase shared with the decrypting job. + required: true + +outputs: + encrypted_value: + description: Base64 ciphertext, safe to expose as a job output. + value: ${{ steps.encrypt.outputs.encrypted_value }} + +runs: + using: composite + steps: + - name: Encrypt + id: encrypt + shell: bash + env: + VALUE: ${{ inputs.value }} + ENC_KEY: ${{ inputs.encryption_key }} + run: | + : "${VALUE:?}" "${ENC_KEY:?}" + encrypted=$(printf '%s' "${VALUE}" | openssl enc -aes-256-cbc -pbkdf2 -salt -pass env:ENC_KEY -base64 -A) + echo "encrypted_value=${encrypted}" >>"${GITHUB_OUTPUT}" diff --git a/.github/workflows/housekeeping-dependencies.yml b/.github/workflows/housekeeping-dependencies.yml index d87b90a..6ec15f2 100644 --- a/.github/workflows/housekeeping-dependencies.yml +++ b/.github/workflows/housekeeping-dependencies.yml @@ -17,6 +17,8 @@ on: - ".github/workflows/housekeeping-dependencies.yml" - ".github/actions/install-renovate/**" - ".github/actions/github-app-token/**" + - ".github/actions/encrypt-secret/**" + - ".github/actions/decrypt-secret/**" schedule: - cron: "0 */2 * * *" workflow_dispatch: @@ -99,7 +101,7 @@ jobs: cancel-in-progress: false group: renovate outputs: - token-ciphertext: ${{ steps.encrypt.outputs.token-ciphertext }} + token-ciphertext: ${{ steps.encrypt.outputs.encrypted_value }} steps: - name: Get token @@ -115,17 +117,13 @@ jobs: cf-access-client-secret: ${{ secrets.CF_ACCESS_STAGE_CLIENT_SECRET }} # The token is already masked, and GitHub silently drops masked values - # from job outputs, so only ciphertext can cross the job boundary. The - # plaintext must never be echoed here. + # from job outputs, so only ciphertext can cross the job boundary. - name: Encrypt token id: encrypt - env: - TOKEN: ${{ steps.app-token.outputs.token }} - ENC_KEY: ${{ secrets.RENOVATE_TOKEN_ENC_KEY }} - run: | - : "${TOKEN:?}" "${ENC_KEY:?}" - ciphertext=$(printf '%s' "${TOKEN}" | openssl enc -aes-256-cbc -pbkdf2 -salt -pass env:ENC_KEY -base64 -A) - echo "token-ciphertext=${ciphertext}" >>"${GITHUB_OUTPUT}" + uses: $/.github/actions/encrypt-secret + with: + value: ${{ steps.app-token.outputs.token }} + encryption_key: ${{ secrets.RENOVATE_TOKEN_ENC_KEY }} renovate: name: Renovate @@ -148,14 +146,10 @@ jobs: # First step, so the plaintext is masked before anything else runs. - name: Decrypt token id: app-token - env: - TOKEN_CIPHERTEXT: ${{ needs.mint-token.outputs.token-ciphertext }} - ENC_KEY: ${{ secrets.RENOVATE_TOKEN_ENC_KEY }} - run: | - : "${TOKEN_CIPHERTEXT:?}" "${ENC_KEY:?}" - token=$(printf '%s' "${TOKEN_CIPHERTEXT}" | openssl enc -d -aes-256-cbc -pbkdf2 -pass env:ENC_KEY -base64 -A) - echo "::add-mask::${token}" - echo "token=${token}" >>"${GITHUB_OUTPUT}" + uses: $/.github/actions/decrypt-secret + with: + encrypted_value: ${{ needs.mint-token.outputs.token-ciphertext }} + encryption_key: ${{ secrets.RENOVATE_TOKEN_ENC_KEY }} - name: Install Renovate uses: $/.github/actions/install-renovate @@ -182,7 +176,7 @@ jobs: - name: Run Renovate env: LOG_LEVEL: ${{ inputs.logLevel || 'info' }} - RENOVATE_TOKEN: ${{ steps.app-token.outputs.token }} + RENOVATE_TOKEN: ${{ steps.app-token.outputs.value }} RENOVATE_FORCE: ${{ inputs.overrideSchedule && '{"schedule":null}' || '' }} RENOVATE_DRY_RUN: ${{ env.dry_run == 'disabled' && 'null' || env.dry_run }} RENOVATE_PLATFORM_COMMIT: enabled