Skip to content

Commit 5e58501

Browse files
authored
ci(security): close OSSF Scorecard findings #57, #58, #60, #61 (#137)
- e2e-smoke.yml: pin actions/checkout and actions/setup-go to full SHAs matching build.yml (closes #60, #61). - Dockerfile: document why alpine:3.23 is intentionally not digest-pinned (multi-arch manifest list; dependabot tracks weekly). Alert #58 to be dismissed in the UI as Won't Fix. - build.yml: add Resolve and verify build SHA step in build-and-push and merge-manifest jobs. The step asserts workflow_run.head_sha is reachable from main (or is the tip of a v* tag) before checkout/imagetools consume it via steps.resolve_sha.outputs.sha. Narrows the trust boundary and avoids Scorecards dangerous-workflow pattern (targets #57). - .gitleaksignore: add working-tree fingerprint variant for the e2e fixture meta.json so go-pre-commit's local scan honors the same exemption that the committed-history scan already had.
1 parent bde9753 commit 5e58501

4 files changed

Lines changed: 71 additions & 5 deletions

File tree

.github/workflows/build.yml

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,12 +124,44 @@ jobs:
124124
runner: ubuntu-24.04-arm
125125
runs-on: ${{ matrix.runner }}
126126
steps:
127+
# Step 0: For workflow_run triggers, prove the head_sha is on main or a
128+
# v* tag before we use it. This narrows the trust boundary and avoids
129+
# Scorecard's dangerous-workflow pattern of feeding workflow_run.head_sha
130+
# straight into actions/checkout.
131+
- name: Resolve and verify build SHA
132+
id: resolve_sha
133+
env:
134+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
135+
EVENT_NAME: ${{ github.event_name }}
136+
WR_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
137+
WR_HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
138+
FALLBACK_SHA: ${{ github.sha }}
139+
run: |
140+
set -euo pipefail
141+
if [[ "$EVENT_NAME" != "workflow_run" ]]; then
142+
echo "sha=${FALLBACK_SHA}" >> "$GITHUB_OUTPUT"
143+
exit 0
144+
fi
145+
# workflow_run path: assert head_sha is reachable from main, or is
146+
# the tip of a v* tag. Reject anything else.
147+
if [[ "$WR_HEAD_BRANCH" == "main" ]]; then
148+
status=$(gh api "repos/${GITHUB_REPOSITORY}/compare/main...${WR_HEAD_SHA}" --jq '.status')
149+
[[ "$status" == "identical" || "$status" == "behind" ]] || { echo "head_sha not on main: $status"; exit 1; }
150+
elif [[ "$WR_HEAD_BRANCH" =~ ^v[0-9] ]]; then
151+
tag_sha=$(gh api "repos/${GITHUB_REPOSITORY}/git/refs/tags/${WR_HEAD_BRANCH}" --jq '.object.sha')
152+
[[ "$tag_sha" == "$WR_HEAD_SHA" ]] || { echo "tag $WR_HEAD_BRANCH does not point at $WR_HEAD_SHA"; exit 1; }
153+
else
154+
echo "Refusing untrusted head_branch: $WR_HEAD_BRANCH"
155+
exit 1
156+
fi
157+
echo "sha=${WR_HEAD_SHA}" >> "$GITHUB_OUTPUT"
158+
127159
# Step 1: checkout code. For workflow_run events, check out the exact SHA
128160
# that GoFortress validated, not whatever HEAD happens to be on main now.
129161
- name: Checkout code
130162
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
131163
with:
132-
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
164+
ref: ${{ steps.resolve_sha.outputs.sha }}
133165

134166
# Step 2: install Go and warm the module / build cache for this runner.
135167
- name: Set up Go
@@ -218,6 +250,35 @@ jobs:
218250
contents: read
219251
packages: write
220252
steps:
253+
# Step 0: Same trust-boundary check as build-and-push. Verify that the
254+
# workflow_run.head_sha is reachable from main or is the tip of a v* tag
255+
# before using it as an image tag.
256+
- name: Resolve and verify build SHA
257+
id: resolve_sha
258+
env:
259+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
260+
EVENT_NAME: ${{ github.event_name }}
261+
WR_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
262+
WR_HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
263+
FALLBACK_SHA: ${{ github.sha }}
264+
run: |
265+
set -euo pipefail
266+
if [[ "$EVENT_NAME" != "workflow_run" ]]; then
267+
echo "sha=${FALLBACK_SHA}" >> "$GITHUB_OUTPUT"
268+
exit 0
269+
fi
270+
if [[ "$WR_HEAD_BRANCH" == "main" ]]; then
271+
status=$(gh api "repos/${GITHUB_REPOSITORY}/compare/main...${WR_HEAD_SHA}" --jq '.status')
272+
[[ "$status" == "identical" || "$status" == "behind" ]] || { echo "head_sha not on main: $status"; exit 1; }
273+
elif [[ "$WR_HEAD_BRANCH" =~ ^v[0-9] ]]; then
274+
tag_sha=$(gh api "repos/${GITHUB_REPOSITORY}/git/refs/tags/${WR_HEAD_BRANCH}" --jq '.object.sha')
275+
[[ "$tag_sha" == "$WR_HEAD_SHA" ]] || { echo "tag $WR_HEAD_BRANCH does not point at $WR_HEAD_SHA"; exit 1; }
276+
else
277+
echo "Refusing untrusted head_branch: $WR_HEAD_BRANCH"
278+
exit 1
279+
fi
280+
echo "sha=${WR_HEAD_SHA}" >> "$GITHUB_OUTPUT"
281+
221282
- name: Download digest artifacts
222283
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
223284
with:
@@ -239,7 +300,7 @@ jobs:
239300
working-directory: ${{ runner.temp }}/digests
240301
env:
241302
IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
242-
SHA_TAG: ${{ github.event.workflow_run.head_sha || github.sha }}
303+
SHA_TAG: ${{ steps.resolve_sha.outputs.sha }}
243304
DEPLOYMENT_TAG: ${{ needs.get_tag.outputs.deployment_tag }}
244305
run: |
245306
docker buildx imagetools create \
@@ -250,5 +311,5 @@ jobs:
250311
- name: Inspect resulting manifest
251312
env:
252313
IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
253-
SHA_TAG: ${{ github.event.workflow_run.head_sha || github.sha }}
314+
SHA_TAG: ${{ steps.resolve_sha.outputs.sha }}
254315
run: docker buildx imagetools inspect "${IMAGE}:${SHA_TAG}"

.github/workflows/e2e-smoke.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ jobs:
3232
timeout-minutes: 25
3333
steps:
3434
- name: Checkout
35-
uses: actions/checkout@v4
35+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
3636

3737
- name: Set up Go
38-
uses: actions/setup-go@v5
38+
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
3939
with:
4040
go-version-file: go.mod
4141
cache: true

.gitleaksignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
70cf477f8f224bfe52dea3d9d7ffc8818158dfee:tests/e2e/fixtures/blocks/000000000000000001bc8a601dd5f0659d36a9b077808850375dfa2d9f009396/meta.json:coinbase-access-token:10
22
d7381c5b38cfbc5022685163d3553c4bd73f7815:tests/e2e/fixtures/blocks/000000000000000001bc8a601dd5f0659d36a9b077808850375dfa2d9f009396/meta.json:coinbase-access-token:3
3+
tests/e2e/fixtures/blocks/000000000000000001bc8a601dd5f0659d36a9b077808850375dfa2d9f009396/meta.json:coinbase-access-token:3

Dockerfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
# merkle service, datahub, webhook delivery) and TLS handshakes fail without
1111
# the system CA bundle.
1212

13+
# Intentionally not pinned to @sha256: — alpine:3.23 is a multi-arch manifest
14+
# list consumed by the matrix amd64/arm64 build in .github/workflows/build.yml,
15+
# and dependabot's docker ecosystem (.github/dependabot.yml) already tracks
16+
# upstream changes. See Scorecard alert #58 (dismissed: "won't fix").
1317
FROM alpine:3.23
1418

1519
RUN apk --no-cache add ca-certificates

0 commit comments

Comments
 (0)