Update CA cert hash in datastream #254
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Update CA cert hash in datastream | |
| on: | |
| workflow_dispatch: | |
| schedule: | |
| - cron: "0 1 * * *" # Daily at 1 AM UTC | |
| push: | |
| branches: | |
| - main | |
| paths: | |
| - ".github/workflows/update-ca-cert.yaml" | |
| - "ssg-chainguard-gpos-ds.xml" | |
| concurrency: | |
| group: update-ca-cert-${{ github.ref }} | |
| cancel-in-progress: false | |
| permissions: | |
| contents: read | |
| jobs: | |
| update-ca-cert: | |
| runs-on: ubuntu-latest | |
| if: ${{ github.repository_owner == 'chainguard-dev' }} | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| id-token: write | |
| pull-requests: write | |
| issues: write | |
| env: | |
| IMAGE_REF: cgr.dev/chainguard/wolfi-base:latest | |
| DATASTREAM_PATH: gpos/xml/scap/ssg/content/ssg-chainguard-gpos-ds.xml | |
| TESTS_PATH: tests | |
| FIXTURES_GLOB: tests/e2e/fixtures/*/Dockerfile | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| with: | |
| persist-credentials: false | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 | |
| with: | |
| egress-policy: audit | |
| - name: Generate token for PR | |
| uses: octo-sts/action@f603d3be9d8dd9871a265776e625a27b00effe05 # v1.1.1 | |
| id: octo-sts | |
| with: | |
| scope: ${{ github.repository }} | |
| identity: ca-cert-updater | |
| - name: Install cosign | |
| uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 | |
| - name: Setup crane | |
| uses: imjasonh/setup-crane@59c71e96a00b28651f10369ba3359a6d730740a0 # v0.6 | |
| - name: Pull and verify image | |
| id: image | |
| run: | | |
| set -euo pipefail | |
| DIGEST=$(crane digest "${IMAGE_REF}") | |
| FULL_REF="${IMAGE_REF%:*}@${DIGEST}" | |
| echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT" | |
| echo "full_ref=${FULL_REF}" >> "$GITHUB_OUTPUT" | |
| # Verify signature | |
| cosign verify \ | |
| --certificate-oidc-issuer https://token.actions.githubusercontent.com \ | |
| --certificate-identity-regexp "https://github.com/chainguard-images/images/.*" \ | |
| "${FULL_REF}" || { | |
| echo "::error::Image signature verification failed" | |
| exit 1 | |
| } | |
| - name: Extract CA certificate SHA | |
| id: ca | |
| env: | |
| STEPS_IMAGE_OUTPUTS_FULL_REF: ${{ steps.image.outputs.full_ref }} | |
| STEPS_IMAGE_OUTPUTS_DIGEST: ${{ steps.image.outputs.digest }} | |
| run: | | |
| set -euo pipefail | |
| SHA=$(crane export "${STEPS_IMAGE_OUTPUTS_FULL_REF}" - | \ | |
| tar -xO etc/ssl/certs/ca-certificates.crt | \ | |
| sha256sum | cut -d' ' -f1) | |
| echo "sha=${SHA}" >> "$GITHUB_OUTPUT" | |
| cat >> "$GITHUB_STEP_SUMMARY" <<EOF | |
| ### CA Certificate Update | |
| - **Image**: \`${IMAGE_REF}\` | |
| - **Digest**: \`${STEPS_IMAGE_OUTPUTS_DIGEST}\` | |
| - **CA SHA256**: \`${SHA}\` | |
| EOF | |
| - name: Update datastream with new SHA | |
| id: update | |
| env: | |
| CA_CERT_SHA: ${{ steps.ca.outputs.sha }} | |
| run: | | |
| set -euo pipefail | |
| # Check if datastream exists | |
| if [ ! -f "${DATASTREAM_PATH}" ]; then | |
| echo "::error::Datastream file not found: ${DATASTREAM_PATH}" | |
| exit 1 | |
| fi | |
| # Create backup | |
| cp "${DATASTREAM_PATH}" "${DATASTREAM_PATH}.bak" | |
| # Update SHA in datastream | |
| sed -i -E "s|(<[^>]*hash>)[^<]*(</[^>]*hash>)|\1${CA_CERT_SHA}\2|g" "${DATASTREAM_PATH}" | |
| # Check if file changed | |
| ds_changed=false | |
| if diff -q "${DATASTREAM_PATH}.bak" "${DATASTREAM_PATH}" > /dev/null; then | |
| echo "No datastream changes needed - SHA already up to date" | |
| echo "datastream_changed=false" >> "$GITHUB_OUTPUT" | |
| rm "${DATASTREAM_PATH}.bak" | |
| else | |
| ds_changed=true | |
| echo "Updated datastream with new SHA: ${CA_CERT_SHA}" | |
| echo "datastream_changed=true" >> "$GITHUB_OUTPUT" | |
| rm "${DATASTREAM_PATH}.bak" | |
| fi | |
| # Show what was updated for the summary | |
| echo "### Datastream Update Summary" >> "$GITHUB_STEP_SUMMARY" | |
| echo "- **File**: \`${DATASTREAM_PATH}\`" >> "$GITHUB_STEP_SUMMARY" | |
| echo "- **New SHA**: \`${CA_CERT_SHA}\`" >> "$GITHUB_STEP_SUMMARY" | |
| if [ "${ds_changed}" == "true" ]; then | |
| echo "- **Status**: Updated" >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| echo "- **Status**: Already up-to-date" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| - name: Re-pin E2E fixture base-image digests | |
| id: fixtures | |
| env: | |
| STEPS_IMAGE_OUTPUTS_DIGEST: ${{ steps.image.outputs.digest }} | |
| run: | | |
| set -euo pipefail | |
| # Re-pin every `FROM <image>@sha256:<old>` in tests/e2e/fixtures/*/Dockerfile | |
| # to the digest that matches the CA bundle hash computed above. This keeps | |
| # the datastream hash and the fixture base images atomically in sync, so | |
| # the CertificateAudit assertion in e.g. baseline-clean can't flake due to | |
| # drift between the two values. | |
| # | |
| # For each Dockerfile: | |
| # - Parse the existing FROM line's `image` (everything before `@sha256:`). | |
| # - Re-resolve that image's current digest with `crane digest <image>`. | |
| # - Substitute the new digest in place, preserving the rest of the line. | |
| # | |
| # This generalizes beyond wolfi-base: any fixture using a digest-pinned | |
| # cgr.dev image gets re-pinned to its own current digest. The wolfi-base | |
| # digest is expected to match ${STEPS_IMAGE_OUTPUTS_DIGEST} (we already | |
| # resolved and verified it), so we reuse that to avoid a redundant lookup. | |
| fixtures_changed=false | |
| updated_files=() | |
| # shellcheck disable=SC2086 # intentional glob expansion | |
| shopt -s nullglob | |
| for dockerfile in ${FIXTURES_GLOB}; do | |
| # Extract FROM line (first match); skip if no digest-pinned FROM present. | |
| from_line=$(grep -E '^FROM [^ ]+@sha256:[0-9a-f]{64}' "${dockerfile}" | head -n1 || true) | |
| if [ -z "${from_line}" ]; then | |
| echo "No digest-pinned FROM in ${dockerfile}; skipping" | |
| continue | |
| fi | |
| # image = token after FROM, up to and including the tag (everything before @sha256:) | |
| image_with_tag=$(echo "${from_line}" | awk '{print $2}' | sed -E 's/@sha256:[0-9a-f]{64}$//') | |
| # Reuse already-resolved digest for the canonical IMAGE_REF; otherwise query. | |
| if [ "${image_with_tag}" = "${IMAGE_REF}" ]; then | |
| new_digest="${STEPS_IMAGE_OUTPUTS_DIGEST}" | |
| else | |
| new_digest=$(crane digest "${image_with_tag}") | |
| fi | |
| cp "${dockerfile}" "${dockerfile}.bak" | |
| # Surgical replace: only the @sha256:<hex> suffix on lines starting with FROM <image_with_tag>@ | |
| # Use a safe sed delimiter (|) since image refs may contain /. | |
| sed -i -E "s|^(FROM ${image_with_tag}@sha256:)[0-9a-f]{64}|\1${new_digest#sha256:}|" "${dockerfile}" | |
| if ! diff -q "${dockerfile}.bak" "${dockerfile}" > /dev/null; then | |
| fixtures_changed=true | |
| updated_files+=("${dockerfile}") | |
| echo "Updated ${dockerfile} -> ${new_digest}" | |
| fi | |
| rm "${dockerfile}.bak" | |
| done | |
| echo "fixtures_changed=${fixtures_changed}" >> "$GITHUB_OUTPUT" | |
| echo "### Fixture Dockerfile Update Summary" >> "$GITHUB_STEP_SUMMARY" | |
| if [ "${fixtures_changed}" = "true" ]; then | |
| echo "- **Status**: Updated" >> "$GITHUB_STEP_SUMMARY" | |
| for f in "${updated_files[@]}"; do | |
| echo " - \`${f}\`" >> "$GITHUB_STEP_SUMMARY" | |
| done | |
| else | |
| echo "- **Status**: Already up-to-date" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| - name: Aggregate change status | |
| id: changed | |
| env: | |
| DS_CHANGED: ${{ steps.update.outputs.datastream_changed }} | |
| FX_CHANGED: ${{ steps.fixtures.outputs.fixtures_changed }} | |
| run: | | |
| set -euo pipefail | |
| if [ "${DS_CHANGED}" = "true" ] || [ "${FX_CHANGED}" = "true" ]; then | |
| echo "changed=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "changed=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Setup gitsign | |
| if: steps.changed.outputs.changed == 'true' | |
| uses: chainguard-dev/actions/setup-gitsign@05fbd381f7c158bd33c9bbf3a28f67852269fdf8 # v1.6.21 | |
| - name: Create Pull Request | |
| if: steps.changed.outputs.changed == 'true' | |
| uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 | |
| id: cpr | |
| with: | |
| token: ${{ steps.octo-sts.outputs.token }} | |
| commit-message: | | |
| chore(oscap): re-pin CA bundle hash and fixture base-image digests | |
| Atomically updates the CA bundle SHA in the OSCAP datastream and the | |
| digest-pinned FROM lines in tests/e2e/fixtures/*/Dockerfile so the | |
| two values can never drift out of sync (which would flake the | |
| CertificateAudit E2E assertions). | |
| Image: ${{ env.IMAGE_REF }} | |
| Digest: ${{ steps.image.outputs.digest }} | |
| CA SHA: ${{ steps.ca.outputs.sha }} | |
| Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> | |
| branch: update-ca-cert-${{ steps.ca.outputs.sha }} | |
| delete-branch: true | |
| title: "chore(oscap): re-pin CA bundle hash and fixture base-image digests" | |
| body: | | |
| ## CA Certificate + Fixture Base-Image Update | |
| Atomically re-pins two values that must stay in lockstep: | |
| 1. The `<ind:hash>` under `oval:org.CABundleHash:ste:1` in the OSCAP | |
| datastream (`${{ env.DATASTREAM_PATH }}`). | |
| 2. The `FROM cgr.dev/chainguard/wolfi-base:latest@sha256:...` line in | |
| every `tests/e2e/fixtures/*/Dockerfile`. | |
| If these drift (e.g. Dependabot bumps the fixture digest before this | |
| workflow refreshes the datastream hash, or vice versa), the | |
| `baseline-clean` / `cabundle-tampered` E2E CertificateAudit check | |
| fails because the fixture's CA bundle no longer matches the hash the | |
| datastream asserts. This workflow is now the authoritative update | |
| point for both values together. | |
| - **Image**: `${{ env.IMAGE_REF }}` | |
| - **Digest**: `${{ steps.image.outputs.digest }}` | |
| - **New CA SHA256**: `${{ steps.ca.outputs.sha }}` | |
| - **Datastream changed**: `${{ steps.update.outputs.datastream_changed }}` | |
| - **Fixtures changed**: `${{ steps.fixtures.outputs.fixtures_changed }}` | |
| labels: | | |
| automated pr | |
| #- name: Enable auto-merge | |
| # if: steps.cpr.outputs.pull-request-number != '' | |
| # run: | | |
| # gh pr merge --auto --squash \ | |
| # "https://github.com/${{ github.repository }}/pull/${{ steps.cpr.outputs.pull-request-number }}" | |
| # env: | |
| # GITHUB_TOKEN: ${{ steps.octo-sts.outputs.token }} |