Skip to content

Commit 4fab19e

Browse files
committed
📝 Add ‘Securing the release workflow’
* Git tags replaced by commit SHA values
1 parent b48f34d commit 4fab19e

File tree

6 files changed

+135
-51
lines changed

6 files changed

+135
-51
lines changed

.github/workflows/ci.yml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,29 @@ jobs:
99
pre-commit:
1010
runs-on: ubuntu-latest
1111
steps:
12-
- uses: actions/checkout@v6
13-
- uses: actions/setup-python@v6
14-
- uses: actions/cache@v5
12+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
13+
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
14+
- uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
1515
with:
1616
path: ~/.cache/pre-commit
1717
key: pre-commit|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }}
18-
- uses: pre-commit/action@v3.0.1
18+
- uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
1919

2020
docs:
2121
name: Build docs and check links
2222
runs-on: ubuntu-latest
2323
steps:
24-
- uses: actions/checkout@v6
25-
- uses: pandoc/actions/setup@v1
26-
- uses: actions/setup-python@v6
24+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
25+
- uses: pandoc/actions/setup@86321b6dd4675f5014c611e05088e10d4939e09e # v1.1.1
26+
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
2727
with:
2828
# Keep in sync with .readthedocs.yaml
2929
python-version-file: .python-version
3030
- name: Install plantuml
3131
run: |
3232
sudo apt install plantuml
3333
- name: Setup cached uv
34-
uses: hynek/setup-cached-uv@v2
34+
uses: hynek/setup-cached-uv@4300ec2180bc77d705e626a34e381b81a4772c51 # v2.5.0
3535
- name: Create venv and install docs dependencies
3636
run: |
3737
uv venv

docs/document/sphinx/test.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,13 @@ You can then define the following jobs for GitHub, for example:
121121
runs-on: ubuntu-latest
122122
steps:
123123
- name: Download pre-built packages
124-
uses: actions/download-artifact@v4
124+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
125125
with:
126126
name: Packages
127127
path: dist
128128
- run: tar xf dist/*.tar.gz --strip-components=1
129129
130-
- uses: actions/setup-python@v5
130+
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
131131
with:
132132
# Keep in sync with tox.ini/docs and .readthedocs.yaml
133133
python-version: "3.12"

docs/packs/.github/workflows/build_wheels.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@ jobs:
1616
os: [ubuntu-latest, windows-latest, macos-13, macos-14]
1717

1818
steps:
19-
- uses: actions/checkout@v4
19+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
2020

2121
- name: Build wheels
22-
uses: pypa/cibuildwheel@v2.21.3
22+
uses: pypa/cibuildwheel@8d2b08b68458a16aeb24b64e68a09ab1c8e82084 # v3.4.1
2323

24-
- uses: actions/upload-artifact@v4
24+
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
2525
with:
2626
name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }}
2727
path: ./wheelhouse/*.whl

docs/packs/publish.rst

Lines changed: 105 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -183,16 +183,16 @@ PyPI at every time a release is created. Such a
183183
needs: [test]
184184
steps:
185185
- name: Checkout
186-
uses: actions/checkout@v4
186+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
187187
with:
188188
fetch-depth: 0
189189
- name: Set up Python
190-
uses: actions/setup-python@v5
190+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
191191
with:
192192
python-version-file: .python-version
193193
cache-dependency-path: '**/pyproject.toml'
194194
- name: Setup cached uv
195-
uses: hynek/setup-cached-uv@v2
195+
uses: hynek/setup-cached-uv@4300ec2180bc77d705e626a34e381b81a4772c51 # v2.5.0
196196
- name: Create venv
197197
run: |
198198
uv venv
@@ -203,9 +203,9 @@ PyPI at every time a release is created. Such a
203203
- name: Retrieve and publish
204204
steps:
205205
- name: Retrieve release distributions
206-
uses: actions/download-artifact@v4
206+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
207207
- name: Publish package distributions to PyPI
208-
uses: pypa/gh-action-pypi-publish@release/v1
208+
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
209209
with:
210210
username: __token__
211211
password: ${{ secrets.PYPI_TOKEN }}
@@ -227,17 +227,52 @@ Lines 38–41
227227
.. seealso::
228228

229229
* `GitHub Actions <https://docs.github.com/en/actions>`_
230+
* :doc:`cibuildwheel`
231+
232+
Securing the release workflow
233+
-----------------------------
234+
235+
Continuous deployment systems used to publish Python packages are a popular
236+
target for attacks. You can avoid many of these risks by following a few
237+
security recommendations:
238+
239+
Avoid insecure triggers
240+
Workflows that can be triggered by an attacker, particularly those that rely
241+
on inputs controlled by the attacker (such as :ref:`pull request
242+
<merge-pull-requests>` or :doc:`branch
243+
<Python4DataScience:productive/git/branch>` titles), have been used in the
244+
past to inject commands. In particular, the ``pull_request_target`` trigger
245+
in :ref:`github-actions` should be avoided.
246+
Sanitise parameters and inputs
247+
Any workflow parameter or input that can be expanded into an executable
248+
command has the potential to be exploited in attacks. Sanitise values by
249+
passing them to commands as environment variables to prevent :abbr:`SSTI
250+
(Server Side Template Injection)` attacks.
251+
Avoid mutable references
252+
Fix your dependencies in workflows.
253+
254+
* Prefer Git commit `SHA
255+
<https://en.wikipedia.org/wiki/Secure_Hash_Algorithms>`_ values over
256+
:doc:`Git tags <Python4DataScience:productive/git/tag>`, as tags are
257+
mutable.
258+
* Use a :ref:`uv_lock` file for PyPI dependencies used in workflows.
259+
260+
Use verifiable deployments
261+
With :ref:`trusted_publishers`, you can use verifiable GitHub environments
262+
to build your Python packages. If you use GitHub Actions for continuous
263+
delivery, you should use :ref:`zizmorcore` to detect and fix insecure
264+
workflows.
230265

231266
.. _trusted_publishers:
232267

233268
Trusted Publishers
234-
------------------
269+
~~~~~~~~~~~~~~~~~~
235270

236271
`Trusted Publishers <https://docs.pypi.org/trusted-publishers/>`_ is a procedure
237272
for publishing packages on the :term:`PyPI`. It is based on OpenID Connect and
238273
requires neither a password nor a token. Only the following steps are required:
239274

240-
#. Add a *Trusted Publishers* on PyPI
275+
#. Add a *Trusted Publisher* on PyPI
241276

242277
Depending on whether you want to publish a new package or update an existing
243278
one, the process is slightly different:
@@ -276,7 +311,7 @@ requires neither a password nor a token. Only the following steps are required:
276311
.. code-block:: diff
277312
:caption: .github/workflows/pypi.yml
278313
:lineno-start: 10
279-
:emphasize-lines: 3, 4-5
314+
:emphasize-lines: 3-5
280315
281316
package-and-deploy:
282317
runs-on: ubuntu-latest
@@ -292,24 +327,19 @@ requires neither a password nor a token. Only the following steps are required:
292327
Lines 13–14
293328
The ``write`` authorisation is required for *Trusted Publishing*.
294329

295-
Zeilen 42–44
330+
Zeilen 40–44
296331
``username`` and ``password`` are no longer required for the GitHub
297332
action ``pypa/gh-action-pypi-publish``.
298333

299-
.. code-block:: diff
334+
.. code-block:: yaml
300335
:lineno-start: 40
301336
:emphasize-lines: 3-
302337
303-
- name: Publish package distributions to PyPI
304-
uses: pypa/gh-action-pypi-publish@release/v1
305-
- with:
306-
- username: __token__
307-
- password: ${{ secrets.PYPI_TOKEN }}
308-
309-
.. _digital-attestations:
310-
311-
Digital Attestations
312-
--------------------
338+
- name: Publish package distributions to PyPI
339+
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
340+
with:
341+
username: __token__
342+
password: ${{ secrets.PYPI_TOKEN }}
313343
314344
Since 14 November 2024, :term:`PyPI` also supports :pep:`740` with `Digital
315345
Attestations <https://docs.pypi.org/attestations/>`_. PyPI uses the
@@ -337,7 +367,7 @@ are used for publishing:
337367
id-token: write
338368
steps:
339369
- name: Publish package distributions to PyPI
340-
uses: pypa/gh-action-pypi-publish@release/v1
370+
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
341371
342372
.. note::
343373
Support for the automatic creation of digital attestations and publishing
@@ -346,3 +376,57 @@ are used for publishing:
346376
.. seealso::
347377
`PyPI now supports digital attestations
348378
<https://blog.pypi.org/posts/2024-11-14-pypi-now-supports-digital-attestations/>`_
379+
380+
.. _zizmorcore:
381+
382+
zizmor
383+
~~~~~~
384+
385+
`zizmor <https://docs.zizmor.sh>`_ can detect and resolve many security issues
386+
in typical GitHub Actions CI/CD configurations. zizmor is designed to integrate
387+
with GitHub Actions. A typical GitHub Action we use for zizmor looks like this:
388+
389+
.. code-block:: yaml
390+
:caption: .github/workflows/zizmor.yml
391+
392+
# https://github.com/woodruffw/zizmor
393+
name: Zizmor
394+
395+
on:
396+
push:
397+
branches: ["main"]
398+
pull_request:
399+
branches: ["**"]
400+
401+
concurrency:
402+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
403+
cancel-in-progress: true
404+
405+
permissions: {}
406+
407+
jobs:
408+
zizmor:
409+
name: Run zizmor
410+
runs-on: ubuntu-latest
411+
permissions:
412+
security-events: write # Required for upload-sarif (used by zizmor-action) to upload SARIF files.
413+
steps:
414+
- name: Checkout repository
415+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
416+
with:
417+
persist-credentials: false
418+
- name: Run zizmor
419+
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
420+
with:
421+
persona: pedantic
422+
423+
.. _add_2fa:
424+
425+
2FA for all development accounts
426+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
427+
428+
You should use two-factor authentication for all your accounts related to
429+
development – not just for :term:`PyPI`. Remember your version control accounts
430+
(`GitHub <https://github.com/>`_, `GitLab <https://about.gitlab.com/>`_,
431+
`Codeberg <https://codeberg.org/>`_, `Forgejo <https://forgejo.org/>`_) and
432+
email.

docs/test/pytest/ci.yaml

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,18 @@ jobs:
1919
- macos-latest
2020
- windows-latest
2121
python-version:
22-
- "3.8"
23-
- "3.9"
2422
- "3.10"
2523
- "3.11"
2624
- "3.12"
25+
- "3.13"
26+
- "3.14"
2727

2828
steps:
2929
- name: Check out the repo
30-
uses: actions/checkout@v4
30+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
3131

3232
- name: Set up Python
33-
uses: actions/setup-python@v5
33+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
3434
with:
3535
python-version: ${{ matrix.python-version }}
3636

@@ -43,7 +43,7 @@ jobs:
4343
python -m tox
4444
4545
- name: Upload coverage data
46-
uses: actions/upload-artifact@v4
46+
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
4747
with:
4848
name: coverage-data
4949
path: .coverage.*
@@ -56,10 +56,10 @@ jobs:
5656
runs-on: ubuntu-latest
5757
steps:
5858
- name: Check out the repo
59-
uses: actions/checkout@v4
59+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
6060

6161
- name: Set up Python
62-
uses: actions/setup-python@v5
62+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
6363
with:
6464
python-version: 3.12
6565

@@ -68,7 +68,7 @@ jobs:
6868
python -m pip install --upgrade coverage[toml]
6969
7070
- name: Download coverage data
71-
uses: actions/download-artifact@v4
71+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
7272
with:
7373
name: coverage-data
7474

@@ -84,14 +84,14 @@ jobs:
8484
python -Im coverage report --fail-under=100
8585
8686
- name: Upload HTML report if check failed
87-
uses: actions/upload-artifact@v4
87+
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
8888
with:
8989
name: html-report
9090
path: htmlcov
9191
if: ${{ failure() }}
9292

9393
- name: Create badge
94-
uses: schneegans/dynamic-badges-action@v1.7.0
94+
uses: schneegans/dynamic-badges-action@0e50b8bad39e7e1afd3e4e9c2b7dd145fad07501 # v1.8.0
9595
with:
9696
auth: ${{ secrets.GIST_TOKEN }}
9797
gistID: YOUR_GIST_ID

docs/test/tox.rst

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -442,16 +442,16 @@ of environments are available for GitHub actions:
442442
if: always()
443443
444444
steps:
445-
- uses: actions/checkout@v6
445+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
446446
with:
447447
persist-credentials: false
448-
- uses: actions/setup-python@v6
448+
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
449449
with:
450450
python-version-file: .python-version
451-
- uses: hynek/setup-cached-uv@v2
451+
- uses: hynek/setup-cached-uv@4300ec2180bc77d705e626a34e381b81a4772c51 # v2.5.0
452452
453453
- name: Download coverage data
454-
uses: actions/download-artifact@v7
454+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
455455
with:
456456
pattern: coverage-data-*
457457
merge-multiple: true
@@ -474,16 +474,16 @@ of environments are available for GitHub actions:
474474
``steps``
475475
is a list of steps. The name of each step can be arbitrary and is
476476
optional.
477-
``uses: actions/checkout@v4``
477+
``uses: actions/checkout``
478478
is a GitHub actions tool that checks out our repository so that the rest
479479
of the workflow can access it.
480-
``uses: actions/setup-python@v5``
480+
``uses: actions/setup-python``
481481
is a GitHub actions tool that configures Python and installs it in a
482482
build environment.
483483
``with: python-version: ${{ matrix.python }}``
484484
says that an environment should be created for each of the Python
485485
versions listed in ``matrix.python``.
486-
``uses: hynek/setup-cached-uv@v2``
486+
``uses: hynek/setup-cached-uv``
487487
uses :term:`uv` in GitHub Actions.
488488

489489
.. seealso::

0 commit comments

Comments
 (0)