Skip to content

Commit b975a83

Browse files
authored
CI: install gh from a pinned keyring checked into the repo (#3449)
CI: install gh from a keyring checked into the repo (#3449) Add a .github/actions/install-gh-cli composite action. It installs gh from the cli.github.com apt repository with signed-by= pointing at the armored public key in the action directory, instead of a keyring downloaded at job time. The file holds only the current key, so apt trusts exactly one key. The README gives the rationale and the key-rotation steps. build-wheel.yml and test-wheel-linux.yml use the action in place of their inline installs. test-wheel-linux.yml loads it with $/ because that job checks out inputs.sha, which can predate the action.
1 parent 9bbfb5c commit b975a83

6 files changed

Lines changed: 154 additions & 22 deletions

File tree

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# install-gh-cli
2+
3+
Installs the GitHub CLI (`gh`) from the `cli.github.com` apt repository on
4+
Linux jobs that don't already have it. If `gh` is already on `PATH` the action
5+
exits immediately.
6+
7+
## Why the key is checked in
8+
9+
The standard upstream install instructions download the repository signing key
10+
over HTTPS and trust whatever bytes come back. The security of every `gh`
11+
package installed in CI then rests solely on the TLS trust store of the runner
12+
or container and the integrity of `cli.github.com`. Several workflows supply
13+
`gh` with a `GITHUB_TOKEN`, so a substituted key could place a malicious
14+
binary into those jobs.
15+
16+
Checking the public key into the repository means apt will only accept packages
17+
signed by a key that was reviewed in a pull request. The file contains exactly
18+
one key, because apt trusts every key listed in a `signed-by=` keyring; a
19+
multi-key file would widen trust unnecessarily.
20+
21+
Packages from Ubuntu's own repositories don't need this: their signing keys
22+
ship with the OS image in the `ubuntu-keyring` package, so apt checks them
23+
against keys that are already on disk. `cli.github.com` is the only
24+
third-party apt repository in our CI, and this action gives it the same
25+
property: a key present before the job runs, not one fetched during it.
26+
27+
## Updating the key
28+
29+
GitHub occasionally rotates the signing key. The previous key (fingerprint
30+
`2C61 0620 1985 B60E 6C7A C873 23F3 D4EA 7571 6059`) expired on 2026-09-05.
31+
The current key (fingerprint `7F38 BBB5 9D06 4DBC B3D8 4D72 5612 B364 6231
32+
3325`) was created on 2026-04-07 and has no expiry.
33+
34+
A key rotation shows up in CI as `apt-get update` failing with a `NO_PUBKEY`
35+
or `EXPKEYSIG` error for the `cli.github.com` repository.
36+
37+
To update the key:
38+
39+
1. Find the new key's fingerprint in the [GitHub CLI Linux install docs](https://github.com/cli/cli/blob/trunk/docs/install_linux.md); the active fingerprints are listed near the top of the page. The docs are the reference; the key server is not an independent check since step 2 also fetches from the same host.
40+
41+
2. Download the armored key for the new fingerprint directly from GitHub CLI's
42+
published ASC file and extract only that key:
43+
44+
```bash
45+
tmp=$(mktemp -d)
46+
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.asc \
47+
| gpg --homedir "$tmp" --batch --import
48+
gpg --homedir "$tmp" --batch --armor --export <FINGERPRINT> \
49+
> .github/actions/install-gh-cli/githubcli-archive-keyring.asc
50+
```
51+
52+
3. Confirm the exported key signs the current repository metadata:
53+
54+
```bash
55+
gpg --dearmor < .github/actions/install-gh-cli/githubcli-archive-keyring.asc \
56+
> "$tmp/key.gpg"
57+
curl -fsSL https://cli.github.com/packages/dists/stable/InRelease \
58+
| gpgv --keyring "$tmp/key.gpg" -
59+
```
60+
61+
The output must say `Good signature from "GitHub CLI <...>"`.
62+
63+
4. Update this README: replace the old fingerprint and creation/expiry dates
64+
above with the new key's details.
65+
66+
5. Open a pull request. Include the new fingerprint, the creation and expiry
67+
dates from `gpg --show-keys`, and the `gpgv` output from step 3 in the PR
68+
description.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
name: Install GitHub CLI
5+
description: Install gh from the cli.github.com apt repo, trusting only the pinned signing key checked in next to this action
6+
7+
runs:
8+
using: composite
9+
steps:
10+
- name: Install gh
11+
shell: bash --noprofile --norc -xeuo pipefail {0}
12+
run: |
13+
if command -v gh >/dev/null 2>&1; then
14+
exit 0
15+
fi
16+
# See .github/actions/install-gh-cli/README.md for key update instructions.
17+
install -D -m 644 "$GITHUB_ACTION_PATH/githubcli-archive-keyring.asc" \
18+
/etc/apt/keyrings/githubcli-archive-keyring.asc
19+
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.asc] https://cli.github.com/packages stable main" \
20+
> /etc/apt/sources.list.d/github-cli.list
21+
apt-get update -q
22+
apt-get install -y gh
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
-----BEGIN PGP PUBLIC KEY BLOCK-----
2+
3+
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
50+
LxU=
51+
=VbVd
52+
-----END PGP PUBLIC KEY BLOCK-----

‎.github/workflows/build-wheel.yml‎

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -555,24 +555,16 @@ jobs:
555555
${{ env.CUDA_CORE_TEST_BINARIES_DIR }}/*.lib
556556
if-no-files-found: error
557557

558+
- name: Install GitHub CLI
559+
if: ${{ !inputs.single-cuda-major && env.BUILD_CORE == 'true' }}
560+
uses: ./.github/actions/install-gh-cli
561+
558562
- name: Look up prior-branch cuda.bindings run ID
559563
id: prior-bindings
560564
if: ${{ !inputs.single-cuda-major && env.BUILD_CORE == 'true' }}
561565
env:
562566
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
563567
run: |
564-
if ! (command -v gh 2>&1 >/dev/null); then
565-
# See https://github.com/cli/cli/blob/trunk/docs/install_linux.md#debian-ubuntu-linux-raspberry-pi-os-apt.
566-
# gh is needed for lookup-run-id, which calls gh api.
567-
mkdir -p -m 755 /etc/apt/keyrings \
568-
&& out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \
569-
&& cat $out | tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
570-
&& chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
571-
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
572-
&& apt update \
573-
&& apt install gh -y
574-
fi
575-
576568
OLD_BRANCH=$(yq '.backport_branch' ci/versions.yml)
577569
OLD_ARTIFACT_PATTERN="cuda-bindings-python${PYTHON_VERSION_FORMATTED}-cuda*-${{ inputs.host-platform }}-*[0-9a-f]"
578570
LATEST_PRIOR_RUN_ID=$(./ci/tools/lookup-run-id \

‎.github/workflows/test-wheel-linux.yml‎

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -216,23 +216,20 @@ jobs:
216216
run-id: ${{ inputs.run-id || github.run_id }}
217217
github-token: ${{ secrets.GITHUB_TOKEN }}
218218

219+
- name: Install GitHub CLI
220+
if: ${{ (env.TEST_BINDINGS == 'true' || env.TEST_CORE == 'true' || env.TEST_PYTHON == 'true') &&
221+
env.BINDINGS_SOURCE == 'backport' }}
222+
# $/ resolves at the workflow's commit. ./ would resolve against the
223+
# inputs.sha checkout, which can predate this action.
224+
uses: $/.github/actions/install-gh-cli
225+
219226
- name: Look up prior-branch artifacts run ID
220227
id: prior
221228
if: ${{ (env.TEST_BINDINGS == 'true' || env.TEST_CORE == 'true' || env.TEST_PYTHON == 'true') &&
222229
env.BINDINGS_SOURCE == 'backport' }}
223230
env:
224231
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
225232
run: |
226-
# See https://github.com/cli/cli/blob/trunk/docs/install_linux.md#debian-ubuntu-linux-raspberry-pi-os-apt.
227-
# gh is needed for lookup-run-id, which calls gh api.
228-
mkdir -p -m 755 /etc/apt/keyrings \
229-
&& out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \
230-
&& cat $out | tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
231-
&& chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
232-
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
233-
&& apt update \
234-
&& apt install gh -y
235-
236233
OLD_BRANCH=${{ needs.compute-matrix.outputs.OLD_BRANCH }}
237234
OLD_ARTIFACT_PATTERN="cuda-bindings-python${PYTHON_VERSION_FORMATTED}-cuda*-${{ inputs.host-platform }}-*[0-9a-f]"
238235
LOOKUP_ARGS=(

‎.spdx-ignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
LICENSE*
2+
*.asc
23
*.html
34
*.json
45
*.md

0 commit comments

Comments
 (0)