[{"content":"persist-credentials: false + git push = 💥 and how to fix it\nIn the light of the recent Tanstack supply-chain attack, pipeline security has become the priority of several projects.\nMistakes in github actions are tricky to see and can have devastating consequences, through PR-controlled variable injection 1.\nLuckily there are powerful tools to catch them:\nZizmor Actionlint One common suggestion from Zizmor is to set persist-credentials: false when using actions/checkout 2\nThe problem: persist-credentials: false + https git commands = boom If you combine these two ingredients you will be greeted with the error\nfatal: could not read Username for \u0026#39;https://github.com/\u0026#39;: No such device or address Workaround 1: Let gh provide credentials - name: Commit and push + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | + gh auth setup-git git commit -m \u0026#34;your message\u0026#34; git push gh auth setup-git configures git to fetch credentials from the gh command, which in turn reads the token from GH_TOKEN.\nThis avoids storing it in .git/config (which is what persist-credentials: false protects us from).\n⚠️ If you self-host your runner, or use a job with a custom container:, you need to install and set up the gh cli yourself. https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-github-cli Workaround 2: Inject the token into the remote URL (and clean it after) - name: Commit and push env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | git commit -m \u0026#34;your message\u0026#34; + git remote set-url origin \u0026#34;https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}\u0026#34; git push + # Embedding the token in the remote URL writes it to `.git/config`, + # so we reset the URL after pushing. + git remote set-url origin \u0026#34;https://github.com/${{ github.repository }}\u0026#34; ⚠️ If git push fails, the credentials stay in .git/config. This is an unlikely attack vector but workaround 1 is safer. https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://docs.zizmor.sh/audits/#artipacked\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://yann.md/posts/2025-05-recipe-persist-credentials-git/","summary":"\u003cp\u003e\u003ccode\u003epersist-credentials: false\u003c/code\u003e + \u003ccode\u003egit push\u003c/code\u003e = 💥 and how to fix it\u003c/p\u003e","title":"Github actions credentials security without breaking your workflows"},{"content":"From scratch or from a base image of your choice.\nPrerequisite: if you start from a base image, is it based on alpine or debian? The syntax is not the same because debian uses cron 1 while alpine uses crond 2.\nCheck with:\n$ docker run --rm ghcr.io/borgmatic-collective/borgmatic:latest cat /etc/os-release Output: NAME=\u0026#34;Alpine Linux\u0026#34; 1. From a fresh Debian/Ubuntu base (crontab) Dockerfile\nFROM debian:bookworm-slim RUN apt-get update \u0026amp;\u0026amp; apt-get install -y cron \u0026amp;\u0026amp; rm -rf /var/lib/apt/lists/* COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh ENTRYPOINT [\u0026#34;/entrypoint.sh\u0026#34;] entrypoint.sh\n#!/bin/sh set -e (printenv | grep -v \u0026#34;^_=\u0026#34;; echo \u0026#34;0 2 * * * /my/script.sh \u0026gt;\u0026gt; /var/log/cron.log 2\u0026gt;\u0026amp;1\u0026#34;) | crontab - exec cron -f Unlike with alpine, the environment variables are not available to the script if you miss the printenv part 3\n2. From an existing image, based on Alpine: example of borgmatic (crond) compose.yaml\nservices: borgmatic: build: ./borgmatic borgmatic/Dockerfile\nFROM ghcr.io/borgmatic-collective/borgmatic:latest COPY entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh ENTRYPOINT [\u0026#34;/entrypoint.sh\u0026#34;] CMD [] borgmatic/entrypoint.sh\n#!/bin/sh set -e borgmatic init ... # Because we override the entrypoint, we need to insert the commands that it would have ran # 2 AM echo \u0026#34;0 2 * * * borgmatic 2\u0026gt;\u0026amp;1 | tee -a /var/log/cron.log\u0026#34; \u0026gt; /etc/crontabs/root exec crond -f Unlike with Debian, environment variables are inherited. No tricks needed 4\nTroubleshooting Does the script work inside the container?\nTrigger the script manually: docker exec \u0026lt;container\u0026gt; /my/script.sh Does the cron trigger?\nCheck if the cron is firing: run it every minute using * * * * * and add a log: echo \u0026quot;$(date) cron fired\u0026quot; \u0026gt;\u0026gt; /var/log/cron.log; /my/script.sh ⚠️ Note: output goes to /var/log/cron.log only, and will not show up in docker logs \u0026lt;container\u0026gt;. Instead use docker exec \u0026lt;container\u0026gt; tail -n 500 -f /var/log/cron.log https://wiki.debian.org/cron\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://wiki.alpinelinux.org/wiki/Cron\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://gist.github.com/Yann-P/43b8775c3660474867d5bc2a8b9250f3\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nhttps://gist.github.com/Yann-P/a819051856a3cb647fd7b1c10b7ed341\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","permalink":"https://yann.md/posts/2025-05-recipe-docker-cron/","summary":"\u003cp\u003eFrom scratch or from a base image of your choice.\u003c/p\u003e","title":"Recipe: docker + crontab"}]