fix: harden the workflows — spoofable actor check and overly broad permissions - #95
Conversation
Two weaknesses in the workflow added last week, both found by code scanning once the same pattern landed in hugo-template, where zizmor's findings are visible on the pull request. github.actor == 'renovate[bot]' guards a job that has contents: write and pushes. actor is whoever triggered the most recent event, which on a synchronize is whoever pushed last, and zizmor rates comparing it to a bot name as spoofable. Replaced with the pull request's author, which is fixed when the pull request opens and cannot be set to another account. The checkout also left the token in .git/config for the whole job, including while the script downloads release tarballs off the internet. It now checks out with persist-credentials: false and hands the token to the push alone. Both came in from the Stensel8/scripts pattern this was modelled on. Scoped to update-checksums.yml on purpose: zizmor reports further findings in the older workflows here, and those are a separate piece of work. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SwrLVfDhkTVHC945s1kZ2s
zizmor reported thirteen findings across the older workflows here, all of them predating the checksum bot. This clears the lot. Permissions were granted at the top of each workflow, which hands them to every job in it: - hugo.yml gave pages: write and id-token: write to the build job as well as the deploy job. Only deploy-pages needs those; configure-pages in the build job needs contents: read and nothing more. Split accordingly. - pr-checks.yml gave pull-requests: write to all eight jobs. Only the two that write to the pull request need it: image-format, which posts a comment about non-AVIF images, and update-checklist, which rewrites the description. The rest read a checkout. - python-checks.yml declared no permissions at all, so it inherited whatever the repository default happens to be. Ten checkouts kept the token in .git/config for the duration of their job without needing it; none of these push. They now pass persist-credentials: false. The Pages base URL was interpolated straight into a run: block, where an expression is expanded before bash sees it. It goes through env now. Verified against the actual requirements rather than trimmed by guesswork: the documented split is contents: read for configure-pages, pages: write and id-token: write for deploy-pages. zizmor is clean on this branch, and actionlint passes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SwrLVfDhkTVHC945s1kZ2s
…ksum A failed install reported success. When nmcli rejects the certificate configuration, run_nmcli returns False instead of exiting, so the caller can retry without the explicit CA bundle. The return value of that retry was discarded, so when it failed too the script carried on to announce "eduroam profile created successfully" and exited 0 with no profile created. Reproduced with an nmcli stub that fails both attempts; it now reports the failure and exits 1. --silent did not silence the prompts. Only show_message consulted the flag; prompt_input looked at gui_tool alone, so a --silent run on a desktop still opened a zenity box asking for the username. The flag is documented as "Run without GUI". Decided once in __init__ now, so the two cannot disagree. The log sanitiser mangled ordinary sentences. Its password pattern took the separator as optional and then consumed the following word, which turned the message that explains the keyring prompt into "Your password=[REDACTED] now be requested by your desktop keyring". Requiring an = or : fixes the prose while still masking real values; checked against password=, "password: ", "PASSWORD = ", and the username and email patterns, which are unaffected. The docs tell people to verify the download against a published SHA-256, so both the English and Dutch pages carry the new hash. It matches the file in this commit. bandit and pyflakes are clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SwrLVfDhkTVHC945s1kZ2s
|
Audit van 1. Een mislukte installatie meldde succesDe ernstigste. if not success:
print("Note: Using system default trust store (implicit validation).")
self.run_nmcli(cmd) # <- return genegeerdMet beide pogingen falend: Een student krijgt te horen dat het gelukt is, ziet daarna dat eduroam niet werkt, en heeft geen idee waarom. Nu wordt de fout gemeld en is de exitcode 1. 2.
|
The fallback path creates the eduroam profile without an explicit CA bundle, and its comment called that "still secure via domain suffix validation". It is not. With no ca-cert, wpa_supplicant does not verify the server certificate against any trust anchor, so domain-suffix-match only inspects a name inside a certificate nobody vouched for. Anyone can present a self-signed certificate carrying ise.infra.saxion.net, which on eduroam means a rogue access point can collect the MSCHAPv2 exchange from students who followed our own guide. The path stays, because a profile that cannot be created helps nobody, but it now prints what it is doing and how to fix it rather than describing itself as secure. The warning only appears when the fallback is actually taken; the normal path is unchanged and silent. Also: the keyring message named GNOME Keyring only, on a script whose whole point is to work on GNOME and KDE alike. It names KWallet too now. Verified the KDE path end to end with a kdialog stub: detection and both dialog invocations are correct. Checksum in both language pages bumped to match. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SwrLVfDhkTVHC945s1kZ2s
802-1x.ca-cert pointed at the distribution's CA bundle, so any of the roughly 150 public authorities in it was an acceptable signer for a server calling itself ise.infra.saxion.net. domain-suffix-match does not close that gap: the name is exactly what an attacker holding a certificate from any of those CAs gets to choose. The chain Saxion publishes through eduroam CAT is now embedded in the script and written to ~/.config/saxion-eduroam/saxion-eduroam-ca.pem, and ca-cert points there. Trust narrows from every public CA to USERTrust RSA Certification Authority plus GEANT OV RSA CA 4, which is what the official CAT installers configure. Both are shipped, as CAT does, so the chain still builds if GEANT rotates the intermediate under the same root. Embedded rather than shipped alongside, because the guide tells people to download one file and verify one checksum. A second file would mean a second download nobody checks. The unvalidated fallback is gone. It existed for systems with no CA bundle, which cannot happen now that the chain travels with the script, and connecting without validation means handing a Saxion password to whatever access point answered. Both language pages describe the pinning, name the two authorities, and point at the new path. Checksum bumped to match. Verified against the certificates Saxion actually publishes: the embedded block is byte-identical to the CAT export, the file written at runtime matches it, nmcli is called once with the pinned path and never with the system bundle, and a rejected certificate now fails with guidance instead of falling through. Note on wifi.cloned-mac-address: left at permanent deliberately. Saxion does not register devices by MAC, but it does block one temporarily when it looks like it is scanning or flooding, and a randomised address would let that be shrugged off by reconnecting. Documented in the code rather than left to be rediscovered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SwrLVfDhkTVHC945s1kZ2s
Summary
Alle zizmor-bevindingen in deze repo opgeruimd: 13 → 0.
Type of change
fix— bug fix (broken link, incorrect command, layout issue)1. Spoofbare bot-check in de checksum-workflow (high)
Die
ifbewaakt een job metcontents: writedie pusht.github.actoris wie de laatste gebeurtenis veroorzaakte — bij eensynchronizedus wie er als laatste pushte. Vervangen door de auteur van de PR, die vastligt bij het openen:Kwam mee uit het
Stensel8/scripts-patroon; THectic.nl heeft dezelfde fix in een eigen PR.2. Rechten stonden bovenaan, dus gold voor élke job (3× error, 1× warning)
hugo.ymlpages: write+id-token: writevoor build én deploycontents: readvoor build, Pages-rechten alleen voor deploypr-checks.ymlpull-requests: writevoor alle 8 jobsimage-formatenupdate-checklistpython-checks.ymlpermissions→ erft de repo-defaultpermissions: {}+contents: readop de jobDe splitsing van
hugo.ymlheb ik nagezocht in plaats van gegokt:configure-pagesin de build-job heeft alleencontents: readnodig;pages: writeenid-token: writehoren bijdeploy-pages. Als ik dat verkeerd had ingeschat, was je Pages-deploy stuk gegaan.3. Tien checkouts hielden hun token vast (warning)
artipacked: de credentials bleven in.git/configstaan zolang de job liep, terwijl geen van deze jobs pusht. Nu overalpersist-credentials: false— behalve in de checksum-bot, die wél pusht en het token daarom expliciet aan dat ene commando meegeeft.4. Pages-URL ging rechtstreeks een
run:in (note)Een expressie in een
run:wordt geëxpandeerd vóórdat bash hem ziet.Waarom dit nu pas opviel
Code scanning faalt alleen op alerts die een PR nieuw introduceert. Deze stonden er al, dus ze bleven onder de radar. In
hugo-templatewaren de workflows nieuw — daar sloeg het wél aan, en dat bracht ook de spoofbare check in deze repo aan het licht.Getest
zizmor: No findings to report.actionlint: groen.Checklist
hugo server— n.v.t.; actionlint en zizmor lokaal gedraaid, en de Pages-rechten nagezocht in de documentatie🤖 Generated with Claude Code
https://claude.ai/code/session_01SwrLVfDhkTVHC945s1kZ2s