Ein Workflow, der beim ersten echten Lauf failt, kostet dich zehn Minuten und einen Blick ins Log. Ein Workflow, der beim ersten Lauf durchläuft, aber eine Rechteausweitung durchreicht, kostet dich mehr, und du merkst es oft erst, wenn es zu spät ist. Beide Fehlerarten stecken an derselben Stelle: in der YAML-Datei, bevor überhaupt ein Runner startet. Genau dort kannst du sie abfangen, mit zwei Werkzeugen, die unterschiedliche Fragen stellen.

actionlint prüft, ob dein Workflow syntaktisch und semantisch stimmt. Existiert der Kontext, den du referenzierst. Ist die Shell-Syntax im run-Block korrekt. Passt der matrix-Wert zum Job. zizmor stellt eine andere Frage: ob der Workflow, so wie er geschrieben ist, ausnutzbar ist. Ein Workflow kann bei actionlint sauber durchlaufen und bei zizmor trotzdem ein Sicherheitsproblem melden, weil beide auf unterschiedliche Fehlerklassen zielen. Für die Pipeline brauchst du beide, nicht eins von beiden.

actionlint: Syntax und Best Practice vor dem ersten Lauf

actionlint ist in Go geschrieben und läuft ohne Runtime-Abhängigkeiten. Installation über das offizielle Skript oder als Binary:

bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
./actionlint

Ohne Argumente findet actionlint automatisch alle Dateien unter .github/workflows/. Ein typischer Fund sieht so aus:

.github/workflows/deploy.yml:34:9: shellcheck reported issue: SC2086:info:1:14: Double quote to prevent globbing and word splitting [shellcheck]
.github/workflows/deploy.yml:41:17: property "commit_message" is not defined in object type {actor: string; base_ref: string; ...} [expression]
.github/workflows/deploy.yml:52:5: could not parse as YAML: mapping value not allowed here

Die erste Zeile kommt aus der eingebauten shellcheck-Integration, actionlint reicht jeden run-Block an shellcheck weiter und meldet Shell-Fehler, die dir sonst erst zur Laufzeit als kryptischer Exit-Code begegnen. Die zweite Zeile ist ein Kontext-Fehler: ein Feld, das im referenzierten Objekt schlicht nicht existiert, was ohne statische Prüfung erst beim Ausführen als leerer String auffällt, oft unbemerkt.

zizmor: Sicherheitslücken in der YAML selbst

zizmor ist in Rust geschrieben und über pip, uvx oder cargo verfügbar:

uvx zizmor .github/workflows/
pip install zizmor

zizmor sucht gezielt nach Mustern, die aus einem Workflow ein Einfallstor machen:

error[dangerous-triggers]: use of fundamentally insecure workflow trigger
  --> .github/workflows/comment.yml:1:1
1 | on: pull_request_target
  = note: erlaubt Checkout von Fork-Code mit Zugriff auf Repo-Secrets

warning[template-injection]: code injection via template expansion
  --> .github/workflows/comment.yml:22:11
22 |     run: echo "Titel: ${{ github.event.issue.title }}"
  = note: der Issue-Titel ist Nutzereingabe und landet unquotiert in der Shell

warning[excessive-permissions]: overly broad permissions
  --> .github/workflows/build.yml:1:1
1 | permissions: write-all

Alle drei Muster sind real und häufig: pull_request_target kombiniert mit einem Checkout des PR-Codes gibt fremdem Code Zugriff auf deine Secrets. Eine Nutzereingabe wie ein Issue-Titel, direkt in einen run-Block interpoliert, ist Script-Injection, kein Edge-Case. Und ein Workflow ohne explizites permissions-Feld läuft in vielen Repos mit weiterreichenden Rechten, als der Job je braucht.

Beide in die Pipeline einbauen

name: lint-workflows
on:
  pull_request:
    paths:
      - '.github/workflows/**'
permissions:
  contents: read
jobs:
  static-checks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: actionlint
        run: |
          bash <(curl -s https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
          ./actionlint -color
      - name: zizmor
        run: |
          pip install --quiet zizmor
          zizmor --format sarif .github/workflows > zizmor.sarif
      - name: Ergebnis hochladen
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: zizmor.sarif

Der paths-Filter sorgt dafür, dass der Job nur läuft, wenn sich tatsächlich etwas unter .github/workflows/ ändert, nicht bei jedem Code-Commit. Der SARIF-Export macht die zizmor-Funde in der GitHub-Code-Scanning-Übersicht sichtbar, statt sie im Job-Log zu vergraben.

Lokal, vor dem Push, gehört das in pre-commit:

repos:
  - repo: https://github.com/rhysd/actionlint
    rev: v1.7.11  # aktuelle Version vor Einsatz prüfen: github.com/rhysd/actionlint/releases
    hooks:
      - id: actionlint
  - repo: local
    hooks:
      - id: zizmor
        name: zizmor
        entry: zizmor
        language: system
        files: ^\.github/workflows/.*\.ya?ml$

So sieht der Autor eines Workflows den Fund vor dem Push, nicht erst der Reviewer im PR oder, schlimmer, niemand.

Grenzen

Drei Dinge, auf die du dich nicht verlassen solltest.

actionlint braucht shellcheck im Pfad, um run-Blöcke wirklich zu prüfen. Ohne shellcheck läuft actionlint trotzdem durch, meldet aber nur die eigenen strukturellen Prüfungen, die Shell-Fehler fallen dann komplett hinten runter, ohne dass das Tool das laut sagt.

zizmor prüft den Workflow, den du ihm gibst, nicht automatisch die composite Actions oder reusable Workflows, die er per uses: nachlädt. Ruft dein Workflow eine Action aus einem anderen Repo auf, ist das ein eigener Scan-Gegenstand, keine automatische Mitprüfung.

Und pull_request_target wird nicht immer zurecht als Fund markiert. Manche Workflows nutzen den Trigger bewusst und ohne Checkout des PR-Codes, rein um mit Repo-Rechten zu kommentieren. Ein Beispiel dafür: ein Workflow, der bei einem Issue-Kommentar automatisch ein Label setzt, braucht dafür Schreibrechte auf Issues, aber keinen Checkout von fremdem Code. zizmor markiert den Trigger trotzdem, weil das Werkzeug den Kontext des einzelnen Workflows nicht kennt, nur das Muster. Die richtige Reaktion ist eine kommentierte Ausnahme im Code, keine pauschale Unterdrückung der Regel für das ganze Repo.

Beide Tools ersetzen kein Runtime-Monitoring und keine Review-Disziplin. Sie verschieben nur den Zeitpunkt, zu dem ein Fehler auffällt, von der Produktion auf den Pull Request.

Wie sich das auf Prozessebene übersetzt, wenn Governance nicht mehr mit der Zahl der Teams und Stages mithält, liest du auf digital-business.blog. Wie du Composite Actions von Reusable Workflows abgrenzt, liest du im Post vom 17.08.. Wie du Secrets aus Build-Layern heraushältst, im Post vom 14.08.. Wie ephemere Runner die Isolation zwischen Läufen sauber halten, im Post vom 12.08.

Hinterlasse einen Kommentar

Diese Seite verwendet Akismet, um Spam zu reduzieren. Erfahre, wie deine Kommentardaten verarbeitet werden..