A múlt héten egy teljes szombatot azzal töltöttem, hogy végigauditáltam minden GitHub Actions workflowt az összes repónkban. Nem azért, mert akartam, hanem azért, mert a Trivy elleni supply chain támadás ráébresztett, milyen vékony jégen táncolok.

Ha lemaradtál volna: valaki sikeresen becsempészett egy rosszindulatú commitot az actions/checkout actionbe, kihasználva a GitHub fork commit elérhetőségi mechanizmusát. Kicseréltek egy SHA pint a Trivy release workflowjában úgy, hogy egy fork-ban lévő árva commitra mutasson. A commit legitimnek tűnt, a komment azt mondta # v6.0.2, a szerző egy valódi maintainer nevével volt meghamisítva. A tényleges payload Go fájlokat töltött le egy elgépelt domainről és kicserélte a Trivy forráskódját a build során.

Két sor változott. Összesen tizennégy sor a diffben, a legtöbb kozmetikai zaj, mint egyes idézőjelek cseréje kettősre. Az a fajta PR, amit azért hagynak jóvá, mert “nincs mit nézni rajta.”

Miért nem elég a SHA pinning

Évek óta mondom mindenkinek, hogy tag helyett SHA-val pineljék az actionöket. Ez továbbra is jó tanács. De a Trivy támadás megmutatta, hogy a SHA pinningnek van egy holtfoltja, amire a legtöbben nem gondoltunk.

A GitHub architektúrája lehetővé teszi, hogy a fork commitok SHA alapján elérhetők legyenek a szülő repóból. Tehát ha valaki forkol egy actions/checkout-ot, push-ol egy rosszindulatú commitot, és te ezt a SHA-t hivatkozod a szülő repóból, a GitHub boldogan feloldja. A commitnak nem kell branchon lennie. Nem kell mergelve lennie. Csak léteznie kell valahol a fork hálózatban.

Ez teljesen megváltoztatja a fenyegetettségi modellt. Nem csak az action karbantartóiban bízol. Abban is bízol, hogy a teljes fork gráfban senki nem push-olt valami rosszindulatút, ami épp megfelel egy általad hivatkozott SHA-nak.

Mit változtattam konkrétan

Így nézett ki az auditom és mit javítottam.

1. Action allowlist az org szintjén

A GitHub lehetővé teszi, hogy korlátozd, mely actionök futhatnak az orgodban. Az “összes engedélyezése” beállításról átálltam explicit allowlistre:

# .github/settings.yml (vagy org settings UI)
actions_permissions:
  allowed_actions: selected
  github_owned_allowed: true
  verified_creators_allowed: true
  patterns_allowed:
    - "aquasecurity/*"
    - "docker/*"
    - "hashicorp/*"

Ez nem fog mindent megállítani, de korlátozza a robbanási sugarat.

2. Pinelés ÉS ellenőrzés

A SHA pinning marad, de mostantól ellenőrzöm, mi van a másik végén. Írtam egy kis scriptet, ami feloldja az egyes pinelt SHA-kat és megnézi, hogy tényleg egy tages release branchen élnek-e:

#!/bin/bash
# verify-action-pins.sh
set -euo pipefail

grep -rh "uses:" .github/workflows/*.yml | \
  grep "@" | \
  sed 's/.*uses: //' | \
  sort -u | \
while read -r action; do
  repo=$(echo "$action" | cut -d@ -f1)
  sha=$(echo "$action" | cut -d@ -f2)

  tags=$(gh api "repos/${repo}/git/ref/tags" --paginate -q '.[].ref' 2>/dev/null || echo "")
  found=false
  for tag_ref in $tags; do
    tag_sha=$(gh api "repos/${repo}/git/${tag_ref}" -q '.object.sha' 2>/dev/null || echo "")
    if [[ "$tag_sha" == "$sha" ]]; then
      found=true
      break
    fi
  done

  if [[ "$found" == "false" ]]; then
    echo "FIGYELEM: $action - SHA nem található egyetlen tagen sem"
  fi
done

Nem szép, de elkapja az árva commitokat.

3. Read-only tokenek alapértelmezésben

Minden workflow mostantól explicit jogosultságokat használ:

permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write  # csak ha tényleg kell

Ha a workflowod nem deklarál jogosultságokat, a GitHub egy alapértelmezett tokent ad neki, amivel írhat a repódba. Ez a különbség a “a támadó elolvasta a forrásodat” és a “a támadó pusholt a main branchedre” között.

4. Hálózati kimenő forgalom figyelése

A Trivy payload a scan.aquasecurtiy[.]org címre hívott ki (figyeld az elgépelést a “security”-ben). Ha self-hosted runnereket használsz, blokkolhatod a váratlan kimenő forgalmat. GitHub-hosted runnereknél korlátozottabbak a lehetőségeid, de legalább hozzáadhatsz egy lépést, ami logolja a DNS lekérdezéseket:

- name: DNS baseline rögzítése
  run: |
    cat /etc/resolv.conf
    resolvectl query github.com || true    

Nem igazi védelem, de forensic adatot ad egy incidens után.

5. Review bot a workflow változásokhoz

Minden PR, ami .github/workflows/ fájlokat érint, mostantól a security csapat jóváhagyását igényli. Ezt CODEOWNERS-szel kényszerítjük ki:

# .github/CODEOWNERS
.github/workflows/ @org/security-team

Egyszerű, hatékony, és elkapta volna a Trivy commitot, ha PR-on ment volna keresztül (nem ment, direct push volt, ami egy külön probléma).

A kényelmetlen igazság

Az igazi tanulság itt nem technikai. Hanem az, hogy a CI/CD pipeline-ok tetszőleges kódot futtatnak az internetről, és ezt teljesen normálisnak tartjuk. Productiönben nem csinálnánk curl | bash-t, de a GitHub Actions workflowjainkban pontosan ennek az ekvivalensét csináljuk.

Éveket töltöttem Kubernetes clusterek keményítésével, network policy-k beállításával, admission controllerek futtatásával, container image-ek aláírásával. Közben a build pipeline-om ellenőrizetlen kódot húzott fork hálózatokból és futtatta írási joggal a repóimhoz.

A Trivy incidenst viszonylag gyorsan elkapták. A következőt lehet, hogy nem fogják. Ha mostanában nem auditáltad a workflowjaidat, foglalj le egy szombatot. Valószínűleg fogsz találni dolgokat, amik nem fognak tetszeni.

Gyors ellenőrzőlista

  • Org szintű action allowlist bekapcsolva
  • Minden action SHA-val pinelve, kommentben a verzióval
  • Workflow jogosultságok explicitek és minimálisak
  • CODEOWNERS védi a .github/workflows/ könyvtárat
  • Self-hosted runnereken hálózati kimenő forgalom kontroll
  • Script vagy tool ellenőrzi a pinelt SHA-kat tages release-ek ellen
  • --skip=validate és hasonló flagek tiltva a CI konfigokban

Semmi sem tökéletes ebből. A supply chain biztonság egy spektrum, nem egy checkbox. De a múlt hét után kicsit nyugodtabban alszom, tudva, hogy a pipeline-jaim nem csak abban bíznak, hogy az internet kedves lesz.