Skip to content

Commit 429ed5c

Browse files
ci: restore security hardening to GitHub Actions workflows
Restore security measures that were accidentally removed: - Add workflow-level `permissions: contents: read` to restrict default token access - Hash-pin all GitHub Actions to prevent supply chain attacks - Restore `python -I` isolation flag on Python invocations to prevent code execution - Use `ubuntu-slim` runner on gate/aggregation and release jobs - Add `permissions: {}` to jobs that only aggregate matrix results - Implement safe bash array pattern for `$ALIASES` to prevent shell injection - Use proper quoting and array expansion for shell variables and options - Restore `python -Im` module invocations for frequenz.repo.config CLI Signed-off-by: Phillip Wenig <phillip.wenig@frequenz.com>
1 parent f19d6f3 commit 429ed5c

3 files changed

Lines changed: 91 additions & 57 deletions

File tree

.github/workflows/ci-pr.yaml

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ name: Test PR
33
on:
44
pull_request:
55

6+
permissions:
7+
# Read repository contents for checkout and dependency resolution only.
8+
contents: read
9+
610
env:
711
# Please make sure this version is included in the `matrix`, as the
812
# `matrix` section can't use `env`, so it must be entered manually
@@ -17,7 +21,7 @@ jobs:
1721

1822
steps:
1923
- name: Run nox
20-
uses: frequenz-floss/gh-action-nox@v1.1.1
24+
uses: frequenz-floss/gh-action-nox@80a9845a59ffc71d27b9c41099eb6cb55bc7b671 # v1.1.1
2125
with:
2226
python-version: "3.11"
2327
nox-session: ci_checks_max
@@ -27,15 +31,15 @@ jobs:
2731
runs-on: ubuntu-24.04
2832
steps:
2933
- name: Setup Git
30-
uses: frequenz-floss/gh-action-setup-git@v1.0.0
34+
uses: frequenz-floss/gh-action-setup-git@16952aac3ccc01d27412fe0dea3ea946530dcace # v1.0.0
3135

3236
- name: Fetch sources
33-
uses: actions/checkout@v6
37+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
3438
with:
3539
submodules: true
3640

3741
- name: Setup Python
38-
uses: frequenz-floss/gh-action-setup-python-with-deps@v1.0.4
42+
uses: frequenz-floss/gh-action-setup-python-with-deps@0d0d77eac3b54799f31f25a1060ef2c6ebdf9299 # v1.0.2
3943
with:
4044
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
4145
dependencies: .[dev-mkdocs]
@@ -44,11 +48,14 @@ jobs:
4448
env:
4549
MIKE_VERSION: gh-${{ github.job }}
4650
run: |
47-
mike deploy $MIKE_VERSION
48-
mike set-default $MIKE_VERSION
51+
# mike is installed as a console script, not a runnable module.
52+
# Run the installed script under isolated mode to avoid importing from
53+
# the workspace when building docs from checked-out code.
54+
python -I "$(command -v mike)" deploy "$MIKE_VERSION"
55+
python -I "$(command -v mike)" set-default "$MIKE_VERSION"
4956
5057
- name: Upload site
51-
uses: actions/upload-artifact@v7
58+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
5259
with:
5360
name: docs-site
5461
path: site/

.github/workflows/ci.yaml

Lines changed: 70 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ on:
1515
- "dependabot/**"
1616
workflow_dispatch:
1717

18+
permissions:
19+
# Read repository contents for checkout and dependency resolution only.
20+
contents: read
21+
1822
env:
1923
# Please make sure this version is included in the `matrix`, as the
2024
# `matrix` section can't use `env`, so it must be entered manually
@@ -28,7 +32,7 @@ jobs:
2832
strategy:
2933
fail-fast: false
3034
matrix:
31-
target:
35+
platform:
3236
- ubuntu-24.04
3337
- ubuntu-24.04-arm
3438
- windows-latest
@@ -44,7 +48,7 @@ jobs:
4448
# that uses the same venv to run multiple linting sessions
4549
- "ci_checks_max"
4650
- "pytest_min"
47-
runs-on: ${{ matrix.target }}
51+
runs-on: ${{ matrix.platform }}
4852

4953
steps:
5054
- name: Run nox
@@ -63,7 +67,9 @@ jobs:
6367
needs: ["nox"]
6468
# We skip this job only if nox was also skipped
6569
if: always() && needs.nox.result != 'skipped'
66-
runs-on: ubuntu-24.04
70+
runs-on: ubuntu-slim
71+
# Drop token permissions: this job only checks matrix status from `needs`.
72+
permissions: {}
6773
env:
6874
DEPS_RESULT: ${{ needs.nox.result }}
6975
steps:
@@ -80,19 +86,19 @@ jobs:
8086
- runner: ubuntu-22.04
8187
target: aarch64
8288
steps:
83-
- uses: actions/checkout@v6
84-
- uses: actions/setup-python@v6
89+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
90+
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
8591
with:
8692
python-version: 3.x
8793
- name: Build wheels
88-
uses: PyO3/maturin-action@v1
94+
uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
8995
with:
9096
target: ${{ matrix.platform.target }}
9197
args: --release --out dist --find-interpreter
9298
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
9399
manylinux: auto
94100
- name: Upload wheels
95-
uses: actions/upload-artifact@v7
101+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
96102
with:
97103
name: wheels-linux-${{ matrix.platform.target }}
98104
path: dist
@@ -107,19 +113,19 @@ jobs:
107113
- runner: ubuntu-22.04
108114
target: aarch64
109115
steps:
110-
- uses: actions/checkout@v6
111-
- uses: actions/setup-python@v6
116+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
117+
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
112118
with:
113119
python-version: 3.x
114120
- name: Build wheels
115-
uses: PyO3/maturin-action@v1
121+
uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
116122
with:
117123
target: ${{ matrix.platform.target }}
118124
args: --release --out dist --find-interpreter
119125
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
120126
manylinux: musllinux_1_2
121127
- name: Upload wheels
122-
uses: actions/upload-artifact@v7
128+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
123129
with:
124130
name: wheels-musllinux-${{ matrix.platform.target }}
125131
path: dist
@@ -132,19 +138,19 @@ jobs:
132138
- runner: windows-latest
133139
target: x64
134140
steps:
135-
- uses: actions/checkout@v6
136-
- uses: actions/setup-python@v6
141+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
142+
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
137143
with:
138144
python-version: 3.x
139145
architecture: ${{ matrix.platform.target }}
140146
- name: Build wheels
141-
uses: PyO3/maturin-action@v1
147+
uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
142148
with:
143149
target: ${{ matrix.platform.target }}
144150
args: --release --out dist --find-interpreter
145151
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
146152
- name: Upload wheels
147-
uses: actions/upload-artifact@v7
153+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
148154
with:
149155
name: wheels-windows-${{ matrix.platform.target }}
150156
path: dist
@@ -159,18 +165,18 @@ jobs:
159165
- runner: macos-15
160166
target: aarch64
161167
steps:
162-
- uses: actions/checkout@v6
163-
- uses: actions/setup-python@v6
168+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
169+
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
164170
with:
165171
python-version: 3.x
166172
- name: Build wheels
167-
uses: PyO3/maturin-action@v1
173+
uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
168174
with:
169175
target: ${{ matrix.platform.target }}
170176
args: --release --out dist --find-interpreter
171177
sccache: ${{ !startsWith(github.ref, 'refs/tags/') }}
172178
- name: Upload wheels
173-
uses: actions/upload-artifact@v7
179+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
174180
with:
175181
name: wheels-macos-${{ matrix.platform.target }}
176182
path: dist
@@ -179,14 +185,14 @@ jobs:
179185
name: Build source distribution packages
180186
runs-on: ubuntu-24.04
181187
steps:
182-
- uses: actions/checkout@v6
188+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
183189
- name: Build sdist
184-
uses: PyO3/maturin-action@v1
190+
uses: PyO3/maturin-action@e83996d129638aa358a18fbd1dfb82f0b0fb5d3b # v1.51.0
185191
with:
186192
command: sdist
187193
args: --out dist
188194
- name: Upload sdist
189-
uses: actions/upload-artifact@v7
195+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
190196
with:
191197
name: wheels-sdist
192198
path: dist/*.tar.gz
@@ -236,13 +242,13 @@ jobs:
236242

237243
steps:
238244
- name: Setup Git
239-
uses: frequenz-floss/gh-action-setup-git@v1.0.0
245+
uses: frequenz-floss/gh-action-setup-git@16952aac3ccc01d27412fe0dea3ea946530dcace # v1.0.0
240246

241247
- name: Print environment (debug)
242248
run: env
243249

244250
- name: Download package
245-
uses: actions/download-artifact@v8
251+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
246252
with:
247253
name: wheels-${{ matrix.platform.image }}-${{ matrix.platform.target }}
248254
path: dist
@@ -263,13 +269,13 @@ jobs:
263269
> pyproject.toml
264270
265271
- name: Setup Python
266-
uses: frequenz-floss/gh-action-setup-python-with-deps@v1.0.4
272+
uses: frequenz-floss/gh-action-setup-python-with-deps@0d0d77eac3b54799f31f25a1060ef2c6ebdf9299 # v1.0.2
267273
with:
268274
python-version: ${{ matrix.python.semver }}
269275
dependencies: dist/*${{ matrix.python.wheelver }}-${{ matrix.platform.tag }}*.whl
270276

271277
- name: Print installed packages (debug)
272-
run: python -m pip freeze
278+
run: python -Im pip freeze
273279

274280
# This job runs if all the `test-installation` matrix jobs ran and succeeded.
275281
# It is only used to have a single job that we can require in branch
@@ -281,7 +287,9 @@ jobs:
281287
needs: ["test-installation"]
282288
# We skip this job only if test-installation was also skipped
283289
if: always() && needs.test-installation.result != 'skipped'
284-
runs-on: ubuntu-24.04
290+
runs-on: ubuntu-slim
291+
# Drop token permissions: this job only checks matrix status from `needs`.
292+
permissions: {}
285293
env:
286294
DEPS_RESULT: ${{ needs.test-installation.result }}
287295
steps:
@@ -294,15 +302,15 @@ jobs:
294302
runs-on: ubuntu-24.04
295303
steps:
296304
- name: Setup Git
297-
uses: frequenz-floss/gh-action-setup-git@v1.0.0
305+
uses: frequenz-floss/gh-action-setup-git@16952aac3ccc01d27412fe0dea3ea946530dcace # v1.0.0
298306

299307
- name: Fetch sources
300-
uses: actions/checkout@v6
308+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
301309
with:
302310
submodules: true
303311

304312
- name: Setup Python
305-
uses: frequenz-floss/gh-action-setup-python-with-deps@v1.0.4
313+
uses: frequenz-floss/gh-action-setup-python-with-deps@0d0d77eac3b54799f31f25a1060ef2c6ebdf9299 # v1.0.2
306314
with:
307315
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
308316
dependencies: .[dev-mkdocs]
@@ -311,11 +319,14 @@ jobs:
311319
env:
312320
MIKE_VERSION: gh-${{ github.job }}
313321
run: |
314-
mike deploy $MIKE_VERSION
315-
mike set-default $MIKE_VERSION
322+
# mike is installed as a console script, not a runnable module.
323+
# Run the installed script under isolated mode to avoid importing from
324+
# the workspace when building docs from checked-out code.
325+
python -I "$(command -v mike)" deploy "$MIKE_VERSION"
326+
python -I "$(command -v mike)" set-default "$MIKE_VERSION"
316327
317328
- name: Upload site
318-
uses: actions/upload-artifact@v7
329+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
319330
with:
320331
name: docs-site
321332
path: site/
@@ -327,18 +338,19 @@ jobs:
327338
if: github.event_name == 'push'
328339
runs-on: ubuntu-24.04
329340
permissions:
341+
# Push generated documentation updates to the `gh-pages` branch.
330342
contents: write
331343
steps:
332344
- name: Setup Git
333-
uses: frequenz-floss/gh-action-setup-git@v1.0.0
345+
uses: frequenz-floss/gh-action-setup-git@16952aac3ccc01d27412fe0dea3ea946530dcace # v1.0.0
334346

335347
- name: Fetch sources
336-
uses: actions/checkout@v6
348+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
337349
with:
338350
submodules: true
339351

340352
- name: Setup Python
341-
uses: frequenz-floss/gh-action-setup-python-with-deps@v1.0.4
353+
uses: frequenz-floss/gh-action-setup-python-with-deps@0d0d77eac3b54799f31f25a1060ef2c6ebdf9299 # v1.0.2
342354
with:
343355
python-version: ${{ env.DEFAULT_PYTHON_VERSION }}
344356
dependencies: .[dev-mkdocs]
@@ -351,7 +363,7 @@ jobs:
351363
GIT_REF: ${{ github.ref }}
352364
GIT_SHA: ${{ github.sha }}
353365
run: |
354-
python -m frequenz.repo.config.cli.version.mike.info
366+
python -Im frequenz.repo.config.cli.version.mike.info
355367
356368
- name: Fetch the gh-pages branch
357369
if: steps.mike-version.outputs.version
@@ -372,13 +384,23 @@ jobs:
372384
GIT_REF: ${{ github.ref }}
373385
GIT_SHA: ${{ github.sha }}
374386
run: |
375-
mike deploy --update-aliases --title "$TITLE" "$VERSION" $ALIASES
387+
# Collect aliases into an array to avoid accidental (or malicious)
388+
# shell injection when passing them to mike.
389+
aliases=()
390+
if test -n "$ALIASES"; then
391+
read -r -a aliases <<<"$ALIASES"
392+
fi
393+
# mike is installed as a console script, not a runnable module.
394+
# Run the installed script under isolated mode to avoid importing from
395+
# the workspace when building docs from checked-out code.
396+
python -I "$(command -v mike)" \
397+
deploy --update-aliases --title "$TITLE" "$VERSION" "${aliases[@]}"
376398
377399
- name: Sort site versions
378400
if: steps.mike-version.outputs.version
379401
run: |
380402
git checkout gh-pages
381-
python -m frequenz.repo.config.cli.version.mike.sort versions.json
403+
python -Im frequenz.repo.config.cli.version.mike.sort versions.json
382404
git commit -a -m "Sort versions.json"
383405
384406
- name: Publish site
@@ -392,14 +414,12 @@ jobs:
392414
# Create a release only on tags creation
393415
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
394416
permissions:
395-
# We need write permissions on contents to create GitHub releases and on
396-
# discussions to create the release announcement in the discussion forums
417+
# Create GitHub releases and upload distribution artifacts.
397418
contents: write
398-
discussions: write
399-
runs-on: ubuntu-24.04
419+
runs-on: ubuntu-slim
400420
steps:
401421
- name: Download distribution files
402-
uses: actions/download-artifact@v8
422+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
403423
with:
404424
path: dist
405425

@@ -420,14 +440,14 @@ jobs:
420440
- name: Create GitHub release
421441
run: |
422442
set -ux
423-
extra_opts=
424-
if echo "$REF_NAME" | grep -- -; then extra_opts=" --prerelease"; fi
443+
extra_opts=()
444+
if echo "$REF_NAME" | grep -- -; then extra_opts+=(--prerelease); fi
425445
gh release create \
426446
-R "$REPOSITORY" \
427447
--notes-file RELEASE_NOTES.md \
428448
--generate-notes \
429-
$extra_opts \
430-
$REF_NAME \
449+
"${extra_opts[@]}" \
450+
"$REF_NAME" \
431451
dist/wheels-*/*.whl dist/wheels-sdist/*.tar.gz
432452
env:
433453
REF_NAME: ${{ github.ref_name }}
@@ -444,10 +464,10 @@ jobs:
444464
id-token: write
445465
steps:
446466
- name: Download distribution files
447-
uses: actions/download-artifact@v8
467+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
448468
with:
449469
path: dist
450470
merge-multiple: true
451471

452472
- name: Publish the Python distribution to PyPI
453-
uses: pypa/gh-action-pypi-publish@release/v1
473+
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0

0 commit comments

Comments
 (0)