Navigation Menu
-
Notifications
You must be signed in to change notification settings - Fork 176
ci: harden workflow token permissions and pin actions by SHA #2849
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
Raise the OpenSSF Scorecard Token-Permissions and Pinned-Dependencies checks (both currently 0/10) ahead of CNCF Incubation due diligence: - Declare a top-level read-only default (permissions: contents: read) in every workflow, plus an explicit per-job permissions: block that grants only the writes that job actually needs. Workflows that previously held write scopes at the workflow level (backport, pr-labeler, pr-size, stale) now hold them at job level instead. - Pin every GitHub-owned and third-party action to a full commit SHA with a version comment. scorecard.yml was already pinned in #2721. No triggers, job graph, or step logic change; only token scopes and action refs. Self-hosted release jobs keep the exact write scopes they already declared. Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com> Assisted-By: Claude <noreply@anthropic.com>
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -24,8 +24,10 @@ concurrency: | |||||||||
| jobs: | ||||||||||
| validate: | ||||||||||
| runs-on: ubuntu-latest | ||||||||||
| permissions: | ||||||||||
| contents: read | ||||||||||
| steps: | ||||||||||
| - uses: actions/checkout@v4 | ||||||||||
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | ||||||||||
|
|
||||||||||
| - name: Validate labels.yml schema | ||||||||||
| run: | | ||||||||||
|
|
@@ -74,11 +76,12 @@ jobs: | |||||||||
| if: github.event_name != 'pull_request' | ||||||||||
| runs-on: ubuntu-latest | ||||||||||
| permissions: | ||||||||||
| contents: read | ||||||||||
| issues: write | ||||||||||
| pull-requests: write | ||||||||||
| steps: | ||||||||||
| - uses: actions/checkout@v4 | ||||||||||
| - uses: EndBug/label-sync@v2 | ||||||||||
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE=".github/workflows/labels.yaml"
echo "== File = $FILE =="
ls -la "$FILE" || true
echo
echo "== Lines around 70-95 =="
nl -ba "$FILE" | sed -n '70,100p'
echo
echo "== Find all checkout steps in this workflow =="
rg -n "uses:\s*actions/checkout@" "$FILE" || true
echo
echo "== Show surrounding block for each checkout step =="
rg -n "uses:\s*actions/checkout@" "$FILE" | cut -d: -f1 | while read -r ln; do
start=$((ln-8))
end=$((ln+12))
echo
echo "--- checkout context at line $ln ($start-$end) ---"
nl -ba "$FILE" | sed -n "${start},${end}p"
done
echo
echo "== Check sync job permissions and checkout persist-credentials settings =="
python3 - <<'PY'
import re, pathlib
p = pathlib.Path(".github/workflows/labels.yaml")
txt = p.read_text()
# Very lightweight parse: find "sync:" job block region by indentation
lines = txt.splitlines()
# locate "sync:" line
for i,l in enumerate(lines, start=1):
if re.match(r'^\s*sync:\s*$', l):
start=i
# block ends when indentation <= that of 'sync:' (2 spaces typically for jobs: children)
indent=len(l)-len(l.lstrip(' '))
j=i
while j < len(lines):
nl=len(lines[j])-len(lines[j].lstrip(' '))
if nl<=indent and re.match(r'^\s*\w', lines[j]) and not re.match(r'^\s*sync:\s*$', lines[j]):
break
j+=1
block="\n".join(lines[start-1:j])
print("== sync job block (approx lines %d-%d) =="%(start,j))
print(block)
break
else:
print("sync job not found by exact 'sync:' match")
PYRepository: cozystack/cozystack Length of output: 273 🏁 Script executed: #!/bin/bash
set -euo pipefail
FILE=".github/workflows/labels.yaml"
python3 - <<'PY'
import pathlib, re
p = pathlib.Path(".github/workflows/labels.yaml")
lines = p.read_text().splitlines()
def show(start, end):
for i in range(start, min(end, len(lines)) + 1):
print(f"{i:>4}: {lines[i-1]}")
print("== Lines 75-90 ==")
show(75, 90)
print("\n== All checkout steps ==");
pat = re.compile(r"^\s*-\s*uses:\s*actions/checkout@([0-9a-fA-F]{40})")
checkout_lines = [i for i,l in enumerate(lines, start=1) if pat.search(l)]
print("checkout line numbers:", checkout_lines)
for ln in checkout_lines:
start = max(ln-8, 1)
end = min(ln+12, len(lines))
print(f"\n--- checkout context at line {ln} (SHA={pat.search(lines[ln-1]).group(1)}) ---")
show(start, end)
print("\n== Attempt to extract `sync` job block & its permissions/checkout config ==");
# Simple indentation-based extraction:
sync_start = None
for i,l in enumerate(lines, start=1):
if re.match(r"^\s*sync:\s*$", l):
sync_start = i
sync_indent = len(l) - len(l.lstrip(' '))
break
if sync_start is None:
print("No `sync:` job found.")
raise SystemExit(0)
# Find end of block: first line with indentation <= sync_indent and starting a new top-level key
j = sync_start
while j <= len(lines):
l = lines[j-1]
indent = len(l) - len(l.lstrip(' '))
if j > sync_start and indent <= sync_indent and re.match(r"^\s*\w", l):
break
j += 1
block = lines[sync_start-1:j-1]
print(f"sync job approx lines {sync_start}-{j-1}")
# Extract permissions section lines within sync block
perm_block = []
in_perm = False
perm_indent = None
for idx, l in enumerate(block, start=sync_start):
if re.match(r"^\s*permissions:\s*$", l):
in_perm = True
perm_indent = len(l) - len(l.lstrip(' '))
perm_block.append((idx, l))
continue
if in_perm:
indent = len(l) - len(l.lstrip(' '))
if indent <= perm_indent and re.match(r"^\s*\w", l):
in_perm = False
continue
perm_block.append((idx,l))
if perm_block:
print("\n-- sync permissions block --")
for idx,l in perm_block:
print(f"{idx:>4}: {l}")
else:
print("\nNo `sync` permissions block found.")
# Find checkout inside sync block
sync_checkout = []
for idx in range(sync_start, j):
l = lines[idx-1]
if re.search(r"uses:\s*actions/checkout@", l):
sync_checkout.append(idx)
print("\ncheckout steps inside sync job (line numbers):", sync_checkout)
for ln in sync_checkout:
start=max(ln-6,1); end=min(ln+14,len(lines))
print(f"\n--- sync checkout context at line {ln} ---")
show(start,end)
# Check persist-credentials anywhere near checkout lines
for ln in sync_checkout:
window = lines[max(ln-2,1)-1:min(ln+8,len(lines))]
txt = "\n".join(window)
if "persist-credentials:" in txt:
print(f"persist-credentials present near line {ln}")
# show exact line(s)
for k in range(max(ln-2,1), min(ln+8,len(lines))+1):
if "persist-credentials" in lines[k-1]:
print(f" {k:>4}: {lines[k-1]}")
else:
print(f"persist-credentials NOT present near line {ln}")
PYRepository: cozystack/cozystack Length of output: 2793 Disable credential persistence in the write-scoped In Suggested patch- - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
+ with:
+ persist-credentials: false📝 Committable suggestion
Suggested change
🧰 Tools🪛 zizmor (1.25.2)[warning] 83-83: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false (artipacked) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||||||||||
| - uses: EndBug/label-sync@52074158190acb45f3077f9099fea818aa43f97a # v2 | ||||||||||
| with: | ||||||||||
| config-file: .github/labels.yml | ||||||||||
| delete-other-labels: false | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,26 +8,32 @@ concurrency: | |
| group: pre-commit-${{ github.workflow }}-${{ github.event.pull_request.number }} | ||
| cancel-in-progress: true | ||
|
|
||
| # Top-level token defaults to read-only; jobs request the minimum extra scopes. | ||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| pre-commit: | ||
| runs-on: ubuntu-22.04 | ||
| permissions: | ||
| contents: read | ||
| steps: | ||
| - name: Checkout code | ||
| uses: actions/checkout@v3 | ||
| uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check all checkout SHAs used across workflows
echo "=== actions/checkout versions in workflows ==="
rg -n "uses:\s*actions/checkout@" .github/workflows/ | sortRepository: cozystack/cozystack Length of output: 1976 Align 🤖 Prompt for AI Agents |
||
| with: | ||
| fetch-depth: 0 | ||
| fetch-tags: true | ||
|
|
||
| - name: Set up Python | ||
| uses: actions/setup-python@v4 | ||
| uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c # v4 | ||
| with: | ||
| python-version: '3.11' | ||
|
|
||
| - name: Install pre-commit | ||
| run: pip install pre-commit | ||
|
|
||
| - name: Set up Go | ||
| uses: actions/setup-go@v5 | ||
| uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 | ||
| with: | ||
| go-version-file: go.mod | ||
| cache: true | ||
|
|
||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: cozystack/cozystack
Length of output: 209
🌐 Web query:
actions/create-github-app-token repositories input scope behavior💡 Result:
If you’re using the actions/create-github-app-token GitHub Action, the token’s repository access (“scope”) depends on whether you set the owner and repositories inputs: 1) Neither owner nor repositories set - The token is scoped to only the current repository. [1] 2) owner set, repositories empty - The token is scoped to all repositories in the provided owner’s GitHub App installation. [1] 3) owner not set, repositories set - The token is scoped to the specified repositories under the current repository owner. [1] 4) owner set, repositories set - The token is scoped to the specified repositories under the provided owner. [1] Additionally: - The repositories input accepts a comma-separated or newline-separated list of repository names. [1][2] - The token inherits all permissions granted to the GitHub App installation (you can still reduce permissions via the action’s permission-* inputs, but by default it mirrors the installation’s permissions). [2] - Under the hood, it creates a GitHub App installation access token via the POST /app/installations/{installation_id}/access_tokens endpoint; the REST API documentation notes you can specify individual repositories with repositories/repository_ids, and if you don’t specify repositories, the installation access token has access to all repositories the installation was granted. [3] Sources: [1][2][3]
Citations:
🏁 Script executed:
Repository: cozystack/cozystack
Length of output: 6248
🏁 Script executed:
Repository: cozystack/cozystack
Length of output: 6248
🌐 Web query:
actions/create-github-app-token input repositories format github.event.repository.name💡 Result:
The repositories input for the actions/create-github-app-token action accepts a list of repository names provided as a comma-separated or newline-separated string [1][2]. Regarding the format and use of github.event.repository.name (or similar expressions like github.repository): 1. Format Options: You can provide repository names as bare names (e.g., repo1) or as full owner/repository names (e.g., owner/repo1) [3][1]. 2. Owner Consistency: If you use the full owner/repository format, the owner portion must match the owner input (if provided) or the current repository owner (if the owner input is unset) [1][2]. 3. Using github.repository: It is valid to include the current repository using the context variable ${{ github.repository }} within your list [1][2]. The action is designed to normalize these inputs by parsing them and stripping the owner portion where necessary for token scoping, while still validating that the owner matches the expected target [3]. Example usage with a newline-separated list: repositories: | ${{ github.repository }} another-repo The action automatically handles trimming and parsing the entries, making it robust for both comma-separated and multi-line YAML formats [4].
Citations:
repositoriesinput actions/create-github-app-token#372Scope the GitHub App token to the current repository
With
owner: cozystackbut norepositoriesinput,actions/create-github-app-tokencan mint an installation token covering all repositories granted to the GitHub App installation under that owner. Constrain it to the current repo.Suggested patch
- name: Generate GitHub App token id: app-token uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1 with: app-id: ${{ secrets.COZYSTACK_CI_APP_ID }} private-key: ${{ secrets.COZYSTACK_CI_PRIVATE_KEY }} owner: cozystack + repositories: ${{ github.event.repository.name }}📝 Committable suggestion
🧰 Tools
🪛 zizmor (1.25.2)
[error] 33-33: dangerous use of GitHub App tokens (github-app): token granted access to all repositories for this owner's app installation
(github-app)
[error] 29-29: dangerous use of GitHub App tokens (github-app): app token inherits blanket installation permissions
(github-app)
🤖 Prompt for AI Agents
Source: Linters/SAST tools