ci: harden workflow token permissions and pin actions by SHA by myasnikovdaniil · Pull Request #2849 · cozystack/cozystack · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
ci: harden workflow token permissions and pin actions by SHA
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
myasnikovdaniil committed Jun 23, 2026
commit 76ad921fddda7b53f88c5611146a728d119fd560
10 changes: 7 additions & 3 deletions .github/workflows/auto-release.yaml
17 changes: 11 additions & 6 deletions .github/workflows/backport.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ concurrency:
group: backport-${{ 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: write
pull-requests: write
contents: read

jobs:
# Determine which backports are needed
Expand All @@ -24,6 +24,8 @@ jobs:
(github.event.action == 'labeled' && (github.event.label.name == 'backport' || github.event.label.name == 'backport-previous'))
)
runs-on: [self-hosted]
permissions:
contents: read
outputs:
backport_current: ${{ steps.labels.outputs.backport }}
backport_previous: ${{ steps.labels.outputs.backport_previous }}
Expand All @@ -32,7 +34,7 @@ jobs:
steps:
- name: Check which labels are present
id: labels
uses: actions/github-script@v7
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
script: |
const pr = context.payload.pull_request;
Expand All @@ -47,7 +49,7 @@ jobs:

- name: Determine target branches
id: branches
uses: actions/github-script@v7
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
script: |
// Get latest release
Expand Down Expand Up @@ -94,6 +96,9 @@ jobs:
github.event.pull_request.base.ref == 'main' &&
(needs.prepare.outputs.backport_current == 'true' || needs.prepare.outputs.backport_previous == 'true')
runs-on: [self-hosted]
permissions:
contents: write
pull-requests: write
strategy:
matrix:
backport_type: [current, previous]
Expand All @@ -116,13 +121,13 @@ jobs:
# 2. Checkout (required by backport‑action)
- name: Checkout repository
if: steps.target.outcome == 'success'
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

# 3. Create the back‑port pull request
- name: Create back‑port PR
id: backport
if: steps.target.outcome == 'success'
uses: korthout/backport-action@v3.2.1
uses: korthout/backport-action@0193454f0c5947491d348f33a275c119f30eb736 # v3.2.1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
label_pattern: '' # don't read labels for targets
Expand Down
10 changes: 8 additions & 2 deletions .github/workflows/codegen-drift.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,22 @@ concurrency:
group: codegen-drift-${{ 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:
codegen-drift:
name: Verify generated code is up to date
runs-on: ubuntu-22.04
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version-file: go.mod
cache: true
Expand Down
9 changes: 6 additions & 3 deletions .github/workflows/labels.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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")
PY

Repository: 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}")
PY

Repository: cozystack/cozystack

Length of output: 2793


Disable credential persistence in the write-scoped sync job actions/checkout.

In .github/workflows/labels.yaml, the sync job grants issues: write and pull-requests: write but the actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 step at line 83 does not set persist-credentials: false.

Suggested patch
-      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
+      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
+        with:
+          persist-credentials: false
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
🧰 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 Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/labels.yaml at line 83, The checkout step in the sync job
is missing persist-credentials:false which risks leaking write-scoped GitHub
token; update the actions/checkout step (the one using
actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5) in the sync job to
include persist-credentials: false so the write-scoped credentials are not
persisted to the checked-out repository workspace.

Source: Linters/SAST tools

- uses: EndBug/label-sync@52074158190acb45f3077f9099fea818aa43f97a # v2
with:
config-file: .github/labels.yml
delete-other-labels: false
6 changes: 4 additions & 2 deletions .github/workflows/pr-labeler.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@ on:
pull_request_target:
types: [opened, edited, reopened, synchronize]

# Top-level token defaults to read-only; jobs request the minimum extra scopes.
permissions:
contents: read
pull-requests: write

jobs:
label:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Apply labels from PR title
uses: actions/github-script@v7
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
script: |
// Conventional Commits types accepted by Cozystack (per docs/agents/contributing.md):
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/pr-size.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ on:
pull_request_target:
types: [opened, synchronize, reopened]

# Top-level token defaults to read-only; jobs request the minimum extra scopes.
permissions:
contents: read
pull-requests: write

concurrency:
group: pr-size-${{ github.event.pull_request.number }}
Expand All @@ -15,8 +15,10 @@ concurrency:
jobs:
size:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/github-script@v7
- uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
script: |
const pr = context.payload.pull_request;
Expand Down
12 changes: 9 additions & 3 deletions .github/workflows/pre-commit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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/ | sort

Repository: cozystack/cozystack

Length of output: 1976


Align actions/checkout pin in pre-commit.yml (line 22).
.github/workflows/pre-commit.yml pins actions/checkout to the v3 SHA, while most other workflows use the v4 SHA (34e114...)—and a couple use @v4 or v6.0.2. Update to the v4 SHA for consistency unless v3 is required (then document why).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/pre-commit.yml at line 22, The workflow is pinning
actions/checkout to the v3 SHA (uses:
actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744) which is inconsistent
with other workflows; update the uses: entry to the v4 SHA (34e114...) to match
the majority of workflows (replace the current actions/checkout SHA with the v4
SHA), or if v3 is intentionally required keep the current pin but add a short
comment explaining why v3 is needed for this workflow.

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
Expand Down
14 changes: 9 additions & 5 deletions .github/workflows/pull-requests-release.yaml
Loading
Loading