From 426c2cceb0c5e353fa52ecd4f1b36c2762b2a25b Mon Sep 17 00:00:00 2001 From: Musiker15 Date: Sat, 10 Oct 2026 20:22:46 +0200 Subject: [PATCH] Security-Review: Docker abschalten, Checkout nach dem Agenten entfernen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Läufer gehört zur Gruppe docker, und ein Container mit der Wurzel des Hosts wäre ein zweiter Weg zu root. Der Daemon stoppt jetzt vor sudo, und beides wird geprüft. Den Checkout, in dem der Agent schreiben kann, entfernt ein Schritt gleich nach ihm, bevor der Post-Schritt von actions/checkout dort git aufruft. Lässt sich das Review am PR nicht ablegen, steht der Status auf error statt dauerhaft auf "läuft". Das Restrisiko der wieder erlaubten User Namespaces steht als Kommentar im Workflow. --- .github/workflows/security-review.yml | 49 ++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/.github/workflows/security-review.yml b/.github/workflows/security-review.yml index 0fc7e3f..ea5dd59 100644 --- a/.github/workflows/security-review.yml +++ b/.github/workflows/security-review.yml @@ -22,9 +22,11 @@ name: Security-Review # - A pull request from a branch of the repository itself, opened by a member, # is reviewed on pull_request with a shell. The shell runs in the sandbox of # Claude Code: a process namespace of its own, without the variables that -# request an identity token, without the token file, without sudo, writing -# only in the checkout and reaching nothing but the npm registry. Nothing -# of the pull request runs outside it in this job. +# request an identity token, without the token file, without sudo and +# Docker, writing only in the checkout and reaching nothing but the npm +# registry. Nothing of the pull request runs outside it in this job, and +# the checkout, the one place the agent could write, is gone before the +# job runs anything in it again. # - Anything else (a fork, an author outside the organisation, a bot) is # reviewed by reading alone on pull_request_target, whose definition comes # from main: no shell, nothing of the pull request is installed or run. Such @@ -272,6 +274,12 @@ jobs: # 24.04 user namespaces that AppArmor otherwise refuses an unprivileged # process. Done with sudo before anything of the pull request exists # beyond its files. + # + # Allowing them opens the kernel to every process of the job through + # user namespaces again: a flaw there leads to root, and root to the + # token in the memory of the runner. That risk is carried knowingly, for + # pull requests of members only; one from outside gets no shell, and + # this step does not run for it. - name: Sandbox vorbereiten if: ${{ env.TRUSTED == 'true' }} run: | @@ -280,16 +288,24 @@ jobs: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 # A process of the runner user could otherwise become root and read the - # memory of the runner, the identity token among it. From here on there - # is no way back to root in this job. - - name: sudo abschalten + # memory of the runner, the identity token among it. Docker is the + # second way there: the runner user is in its group, and a container + # with the root of the host mounted is root as well. The daemon stops + # first, while sudo still can stop it. From here on there is no way back + # to root in this job, and both are checked, not assumed. + - name: sudo und Docker abschalten run: | + sudo systemctl stop docker.socket docker.service containerd.service || true sudo find /etc/sudoers.d -type f -exec grep -l runner {} + | xargs -r sudo rm -f if sudo -n true 2> /dev/null; then echo "sudo ist noch möglich, das Review läuft so nicht." exit 1 fi - echo "sudo ist abgeschaltet." + if docker info > /dev/null 2>&1; then + echo "Docker antwortet noch, das Review läuft so nicht." + exit 1 + fi + echo "sudo und Docker sind abgeschaltet." - name: Auftrag zusammenstellen env: @@ -557,6 +573,16 @@ jobs: "$RUNNER_TEMP/claude.jsonl" 2> /dev/null | tail -n 5 || true fi + # The agent may have written in the checkout, its .git among it. What + # runs there after it would run outside the sandbox with the environment + # of the job: the post step of actions/checkout calls git in it, git + # submodule foreach among it, and a configuration written for that would + # run. Nothing after the agent needs the checkout, so it goes, whatever + # the agent did or how its step ended. + - name: Checkout entfernen + if: ${{ always() }} + run: rm -rf "$GITHUB_WORKSPACE/pr" + # The report is the last answer of the agent. The block of private # findings comes out of it here and does not go into the artifact, # which in a public repository anyone may download. @@ -732,7 +758,14 @@ jobs: fi jq -n --arg sha "$SHA" --arg event "$event" --rawfile body body.md \ '{commit_id: $sha, event: $event, body: $body}' > review.json - review_url="$(gh api --method POST "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews" --input review.json --jq .html_url)" + # Refused (the setting that lets Actions approve is off, the body too + # long, the API down), the status says so instead of staying on + # "läuft" for good: the step would stop here before setting it. + if ! review_url="$(gh api --method POST "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews" --input review.json --jq .html_url)"; then + set_status error "Review ließ sich am PR nicht ablegen ($event), siehe Lauf." "$run_url" + echo "Review ließ sich nicht posten, Status error gesetzt." + exit 1 + fi case "$result" in clean) set_status success "Keine Befunde." "$review_url" ;;