IT Security 1 month ago

Critical Container Escape Vulnerability in Gitea Runner (act_runner) – Detection & Mitigation Guide

A critical vulnerability in Gitea Runner (act_runner) allows malicious workflow authors to bypass privileged mode restrictions, use Docker host namespace options, and escape to the host as root. This advisory outlines the flaw, detection techniques, and immediate mitigation actions for shared A critical vulnerability in Gitea Runner (act_runner) allows malicious workflow authors to bypass privileged mode restrictions, use Docker host namespace options, and escape to the host as root.…

IT Security

TECH OPS > IT Security > Security Advisory


Executive Summary

A critical vulnerability in Gitea Runner (act_runner) allows untrusted workflow authors to escape container isolation and execute arbitrary commands as root on the underlying host system. The flaw stems from incomplete sanitization of Docker container options in workflow YAML files. Even when administrators explicitly disable privileged mode on the runner, dangerous flags such as --pid=host, --ipc=host, and --cap-add=ALL pass through unfiltered, enabling full host compromise via standard utilities like nsenter.

Affected deployments: All versions of act_runner using Docker execution. Severity: Critical for shared runners with untrusted users; High for single-tenant runners where privileged mode was disabled as a security control.

Immediate action required: Audit runner configurations, restrict untrusted workflow execution, and apply code-level fixes or operational workarounds outlined below.



What Is the Vulnerability?

How the Flaw Works: Incomplete Sanitization of Container Options

The vulnerability exists in how act_runner processes the jobs.<job_id>.container.options field from workflow YAML files. When a workflow author specifies custom Docker CLI parameters, act_runner merges these directly into the Docker container’s HostConfig structure without adequate filtering.

The attack chain unfolds as follows:

  1. Input acceptance: Workflow YAML includes arbitrary Docker options under container.options
  2. Parsing: RunContext.options() appends these untrusted parameters to runner-level options
  3. Configuration merge: mergeContainerConfigs() converts Docker CLI flags into HostConfig fields
  4. Incomplete filtering: sanitizeConfig() only removes Binds and Mounts—leaving namespace and capability flags intact
  5. Container creation: Docker daemon receives HostConfig with dangerous flags enabled

The result: a container with Privileged=false but with PidMode=host, IpcMode=host, and CapAdd=[ALL] simultaneously active.

Why Disabling Privileged Mode Doesn’t Protect You

Administrators often assume that setting Privileged: false in runner configuration provides adequate isolation. This assumption is incorrect for this vulnerability.

The Privileged flag in Docker is a single boolean that grants broad capabilities. However, it is not the only mechanism for accessing host resources. The following Docker options independently provide host-level access:

Option Effect Bypasses Privileged=false?
--pid=host Share host PID namespace ✓ Yes
--ipc=host Share host IPC namespace ✓ Yes
--uts=host Share host UTS namespace ✓ Yes
--network=host Use host network stack ✓ Yes
--cap-add=ALL Grant all Linux capabilities ✓ Yes
--security-opt seccomp=unconfined Disable seccomp filtering ✓ Yes

Because act_runner does not strip these options when Privileged=false, an attacker can achieve the same effect as privileged mode through alternative means.

The Attack Path: From Workflow YAML to Host Root Access

Step 1: Malicious Workflow Submission

An attacker with repository write access (or via a pull request on a public repository) submits a workflow file containing:

jobs:
  exploit:
    runs-on: ubuntu-latest
    container:
      image: ubuntu:22.04
      options: >-
        --pid=host
        --ipc=host
        --cap-add=ALL
        --security-opt seccomp=unconfined
        --security-opt apparmor=unconfined
    steps:
      - name: Escape to host
        run: nsenter -t 1 -m -u -i -n -p -- /bin/bash

Step 2: Workflow Execution

act_runner parses this YAML and constructs a Docker container with:

  • Privileged: false (runner default)
  • PidMode: host (from workflow options)
  • IpcMode: host (from workflow options)
  • CapAdd: [ALL] (from workflow options)

Step 3: Container Escape

The nsenter utility, combined with host namespace access and full capabilities, allows the attacker to:

  • Target PID 1 (the host’s init process)
  • Switch to the host’s mount, UTS, IPC, network, and PID namespaces
  • Execute arbitrary commands as root on the underlying system

Step 4: Post-Exploitation

Once root access is achieved, the attacker can:

  • Extract CI/CD secrets and API tokens from environment variables
  • Access Docker socket to compromise sibling containers
  • Pivot to internal infrastructure networks
  • Persist backdoors or cryptocurrency miners
  • Exfiltrate source code and intellectual property


Who Is Affected?

Shared or Multi-Tenant act_runner Deployments

Organizations running a single act_runner instance to execute workflows from multiple repositories or teams face the highest risk. In these environments:

  • Public repositories automatically execute workflows from any contributor’s pull request
  • Internal repositories may accept pull requests from external contractors or partner organizations
  • Shared runners reduce infrastructure costs but concentrate risk

Risk level: CRITICAL

Runners with Public Repositories or Open Pull Request Triggers

Any act_runner configured to automatically execute workflows triggered by:

  • Pull requests from forks
  • External contributor submissions
  • Webhook events from untrusted sources

These configurations assume workflow authors are trustworthy—an assumption that breaks when the workflow itself is the attack vector.

Risk level: CRITICAL

Single-Tenant Runners Where Privileged Mode Was Explicitly Disabled

Organizations that disabled privileged mode as a deliberate security hardening measure are directly impacted. The vulnerability bypasses this control entirely, making the security investment ineffective.

Risk level: HIGH


Real-World Attack Scenarios

Scenario 1: Container Escape Using nsenter and Host Namespaces

Attacker profile: Malicious external contributor to a public open-source project

Attack execution:

  1. Attacker forks the target repository
  2. Attacker creates a branch with a modified .github/workflows/build.yml file
  3. Attacker submits a pull request with the malicious workflow
  4. Repository maintainer reviews code changes (which appear benign) and merges the PR
  5. act_runner automatically executes the workflow on the shared runner
  6. Workflow container spawns with --pid=host --cap-add=ALL
  7. nsenter -t 1 -m -u -i -n -p -- id executes as root on the host
  8. Attacker gains shell access to the runner host

Impact: Full host compromise, access to all concurrent job containers, credential theft.

Scenario 2: Extracting Secrets and Lateral Movement After Breakout

Attacker profile: Insider threat with repository write access

Attack execution:

  1. Attacker commits a workflow that escapes to host and exfiltrates secrets
  2. Workflow runs and attacker gains root shell on runner
  3. Attacker inspects /proc to find environment variables of sibling containers:
   nsenter -t 1 -m -u -i -n -p -- cat /proc/*/environ | grep -i "token\|secret\|key"
  1. Attacker accesses Docker socket to inspect running containers:
   nsenter -t 1 -m -u -i -n -p -- docker ps
   nsenter -t 1 -m -u -i -n -p -- docker exec <container_id> env
  1. Attacker extracts deployment credentials, Gitea API tokens, and cloud provider keys
  2. Attacker uses stolen credentials to access production infrastructure
  3. Attacker establishes persistence by modifying cron jobs or systemd services on the host

Impact: Lateral movement to production systems, data breach, supply chain compromise.


How to Detect Exploitation Attempts

Static Scanning: Identifying Dangerous Workflow Patterns

Implement automated scanning of workflow files in your repositories to flag suspicious patterns before they execute.

Scanning approach:

Search .gitea/workflows/*.yml and .github/workflows/*.yml files for dangerous container.options patterns:

# Detect host namespace options
grep -r "\-\-pid=host\|\-\-ipc=host\|\-\-uts=host\|\-\-network=host" .gitea/workflows/ .github/workflows/
# Detect capability expansion
grep -r "\-\-cap-add" .gitea/workflows/ .github/workflows/
# Detect security profile disablement
grep -r "seccomp=unconfined\|apparmor=unconfined" .gitea/workflows/ .github/workflows/

Integration: Add this scanning to:

  • Pre-commit hooks (block commits with dangerous patterns)
  • Pull request CI checks (flag PRs with suspicious workflows)
  • Repository scanning tools (GitLab/GitHub security scanning)

Runtime Monitoring: Docker Daemon and Process Execution Alerts

Monitor the Docker daemon and process execution on runner hosts for signs of exploitation.

Docker daemon monitoring:

# Monitor for container creation with host namespaces
docker events --filter 'type=container' --filter 'action=create' | \
  grep -E "PidMode.*host|IpcMode.*host|CapAdd.*ALL"

Alert conditions:

  • Container creation with HostConfig.PidMode=host originating from act_runner process
  • Container creation with HostConfig.CapAdd containing ALL or SYS_ADMIN
  • Container creation with HostConfig.SecurityOpt containing seccomp=unconfined

Process execution monitoring:

# Alert on nsenter execution targeting PID 1
auditctl -w /usr/bin/nsenter -p x -k nsenter_execution
ausearch -k nsenter_execution | grep "nsenter.*-t 1"

Alert conditions:

  • Execution of nsenter from within a container
  • nsenter targeting PID 1 or host namespaces
  • Execution of privileged commands (id, whoami, cat /etc/shadow) from container context

Log Indicators to Watch For

Docker daemon logs (/var/lib/docker/containers/*/*/json.log):

"HostConfig":{"PidMode":"host","IpcMode":"host","CapAdd":["ALL"]}

Kernel audit logs (auditctl):

type=EXECVE msg=audit(...): argc=5 a0="nsenter" a1="-t" a2="1" a3="-m" a4="-u"

Container runtime logs (containerd/cri-o):

container_create request with host_pid=true and cap_add=[ALL]

act_runner logs:

"Creating container with options: --pid=host --cap-add=ALL"

Immediate Mitigation Actions

Code-Level Fix: Sanitizing Container Options in act_runner

The definitive fix requires modifying act_runner source code to treat container.options as untrusted input and strip dangerous flags.

Implementation approach:

Modify the sanitizeConfig() function (or create a new sanitizeContainerOptions() function) to reject or strip the following when Privileged=false:

// Pseudo-code for sanitization logic
func sanitizeContainerOptions(options string, privilegedMode bool) string {
    if !privilegedMode {
        // Block host namespace options
        options = stripOption(options, "--pid=host")
        options = stripOption(options, "--ipc=host")
        options = stripOption(options, "--uts=host")
        options = stripOption(options, "--network=host")
        // Block capability expansion
        options = stripOption(options, "--cap-add=ALL")
        options = stripOption(options, "--cap-add=SYS_ADMIN")
        options = stripOption(options, "--cap-add=SYS_PTRACE")
        // Block security profile disablement
        options = stripOption(options, "--security-opt=seccomp=unconfined")
        options = stripOption(options, "--security-opt=apparmor=unconfined")
        // Block device access
        options = stripOption(options, "--device")
        options = stripOption(options, "--device-cgroup-rule")
        // Block volume mounts from other containers
        options = stripOption(options, "--volumes-from")
    }
    return options
}

Deployment: Apply this fix by:

  1. Updating act_runner to the patched version
  2. Restarting all runner instances
  3. Validating that existing workflows still function (non-malicious workflows should not use these options)

Operational Workaround: Ephemeral VM-Based Runners

If code-level patching is delayed, isolate job execution using ephemeral virtual machines instead of persistent container hosts.

Implementation:

  • Deploy act_runner on short-lived VM instances (e.g., AWS EC2 spot instances, Google Cloud Preemptible VMs)
  • Configure the runner to execute a single job, then terminate the VM
  • Use infrastructure-as-code (Terraform, CloudFormation) to provision fresh VMs for each job

Benefits:

  • Even if a container escape occurs, the attacker gains access only to an ephemeral VM with no persistent data
  • Secrets and credentials are not stored on the VM; they are injected at runtime
  • Lateral movement to other jobs is impossible (each job runs on a separate VM)

Trade-offs:

  • Increased infrastructure costs (VM provisioning overhead)
  • Longer job execution time (VM startup latency)
  • Complexity in managing VM lifecycle and cleanup

Access Control: Restricting Untrusted Workflow Execution

Implement repository and runner policies to prevent untrusted users from triggering workflows.

Gitea-level controls:

  1. Disable workflow execution for external pull requests:
  • In repository settings, disable “Allow workflows to run on pull requests from forks”
  • Require manual approval for workflows from external contributors
  1. Restrict runner access:
  • Create separate runner groups for trusted and untrusted workflows
  • Assign public repositories to isolated runners with minimal permissions
  • Assign internal repositories to runners with broader capabilities
  1. Require workflow approval:
  • Implement a policy requiring code review and approval before workflows execute
  • Use branch protection rules to enforce this

Runner-level controls:

  1. Disable public repository execution:
  • Configure runners to only accept jobs from private repositories
  • Use runner labels to segregate workloads
  1. Implement workflow validation:
  • Pre-execute workflow validation to reject files with dangerous patterns
  • Log all workflow executions for audit purposes

Technical Deep Dive: The Vulnerability Mechanism

Source-to-Sink Flow: Input Parsing Through Container Creation

The vulnerability follows a clear data flow from untrusted input (workflow YAML) to dangerous output (Docker API call):

Workflow YAML
    ↓
jobs.<job_id>.container.options (untrusted string)
    ↓
ContainerSpec.Options field
    ↓
RunContext.options() method (appends to runner options)
    ↓
mergeContainerConfigs() function (parses Docker CLI flags)
    ↓
Docker HostConfig structure
    ↓
Docker daemon API call (CreateContainer)
    ↓
Container spawned with dangerous flags

Each step in this chain assumes the previous step has validated the input. None of them do.

Incomplete Filtering: What sanitizeConfig() Actually Does

The current sanitizeConfig() function in act_runner performs minimal filtering:

// Current implementation (vulnerable)
func sanitizeConfig(config *container.HostConfig) {
    // Only removes Binds and Mounts
    config.Binds = nil
    config.Mounts = nil
    // Everything else passes through unchanged
}

This function was designed to prevent container escape via volume mounts, not via namespace sharing or capability expansion. It is insufficient for the threat model of untrusted workflow authors.

Preserved HostConfig Flags That Enable Escape

The following HostConfig fields remain unfiltered and enable container escape:

Field Dangerous Value Escape Mechanism
PidMode "host" nsenter -t 1 targets host PID 1
IpcMode "host" Shared IPC allows cross-container communication
UTSMode "host" Shared hostname/domainname
NetworkMode "host" Direct access to host network stack
CapAdd ["ALL"] or ["SYS_ADMIN"] Grants CAP_SYS_PTRACE, CAP_SYS_ADMIN for namespace manipulation
SecurityOpt ["seccomp=unconfined"] Disables syscall filtering
SecurityOpt ["apparmor=unconfined"] Disables AppArmor confinement

When any of these are set to permissive values, a container can escape to the host using standard utilities.


Technical flow diagram showing how workflow YAML options bypass sanitization to reach Docker HostConfig with dangerous flags intact.
Technical flow diagram showing how workflow YAML options bypass sanitization to reach Docker HostConfig with dangerous flags intact.

Threat Assessment and Risk Levels

Critical Severity: Shared Runners with Untrusted Users

Conditions:

  • Single act_runner instance serving multiple repositories
  • Public repositories or repositories accepting external pull requests
  • Workflows execute automatically on PR submission
  • No manual approval required

Risk factors:

  • Exploitability: Trivial (standard YAML syntax, no zero-day required)
  • Impact: Total host compromise (root access)
  • Likelihood: High (attackers actively target CI/CD systems)
  • Blast radius: All concurrent jobs, all secrets, all infrastructure access

CVSS v3.1 Score: 9.8 (Critical)

High Severity: Single-Tenant Runners with Disabled Privileged Mode

Conditions:

  • act_runner serves a single organization or team
  • Privileged mode explicitly disabled as security control
  • Internal users have repository write access
  • Insider threat or compromised developer account possible

Risk factors:

  • Exploitability: Trivial (same as above)
  • Impact: Total host compromise (privileged mode bypass)
  • Likelihood: Medium (requires insider or account compromise)
  • Blast radius: All secrets, all infrastructure access

CVSS v3.1 Score: 8.2 (High)

Exploitability: Why This Requires No Zero-Day Exploits

This vulnerability is trivially exploitable because:

  1. No kernel exploits required: nsenter is a standard Linux utility included in most distributions
  2. No container runtime vulnerabilities needed: Docker correctly implements the requested configuration
  3. No privilege escalation required: The attacker already has the necessary capabilities within the container
  4. Standard YAML syntax: Workflow files use documented Docker CLI options
  5. No authentication bypass: The attacker only needs repository write access (which they may already have)

An attacker with basic Linux knowledge can exploit this vulnerability in minutes.


Affected Versions and Products

All Releases of act_runner Using Docker Execution

Affected product: Gitea Runner (act_runner)

Affected versions: All versions that support the container.options field in workflow YAML and use Docker as the execution backend.

Unaffected configurations:

  • act_runner using Kubernetes backend (different execution model)
  • act_runner using VM-based execution (no container escape possible)
  • Workflows that do not use container.options field (though this is not a reliable mitigation)

Scope of Impact Across Gitea Deployments

Estimated impact:

  • All public Gitea instances with act_runner enabled
  • All private Gitea instances accepting external pull requests
  • All organizations using shared act_runner instances

Mitigation status:

  • No official patch released at time of writing
  • Workarounds available (ephemeral VMs, access control)
  • Code-level fix requires act_runner source modification

FAQ

Why does setting Privileged: false on the runner fail to block container breakouts?

Setting Privileged: false only controls Docker’s single Privileged boolean flag. This flag grants a broad set of capabilities and host access, but it is not the only way to access host resources.

The vulnerability exploits alternative Docker options (--pid=host, --cap-add=ALL, etc.) that independently provide host-level access. Because act_runner does not strip these options when Privileged=false, an attacker can achieve the same effect through a different mechanism.

Think of it like a security door with a broken lock: even if you disable the main lock mechanism, the door can still be opened through an unguarded side entrance.

What fields does act_runner currently sanitize in container configurations?

In affected versions, sanitizeConfig() only filters:

  • Binds (volume mount paths)
  • Mounts (volume mount objects)

All other HostConfig fields pass through unfiltered, including:

  • Namespace sharing options (PidMode, IpcMode, UTSMode, NetworkMode)
  • Capability expansion (CapAdd)
  • Security profile disablement (SecurityOpt)
  • Device access (Devices, DeviceCgroupRules)
  • Volume inheritance (VolumesFrom)

Who is at highest risk from this vulnerability?

Highest risk:

  1. Organizations running shared act_runner instances for multiple teams/repositories
  2. Open-source projects accepting pull requests from untrusted contributors
  3. Public repositories with automatic workflow execution enabled

Secondary risk:

  1. Organizations with disabled privileged mode as a security control
  2. Environments with insider threat concerns
  3. Deployments with weak access controls on repository write permissions

Can I safely use act_runner with public repositories?

Current answer: No. Public repositories with automatic workflow execution are vulnerable to this attack.

Safe alternatives:

  1. Disable automatic workflow execution for external pull requests
  2. Require manual approval before running workflows from forks
  3. Use ephemeral VM-based runners instead of persistent container hosts
  4. Implement strict workflow validation to reject dangerous patterns
  5. Upgrade to a patched version of act_runner once available

How do I know if my runner has been compromised?

Indicators of compromise:

  1. Log evidence:
  • Docker daemon logs showing container creation with PidMode=host or CapAdd=ALL
  • Kernel audit logs showing nsenter execution from container context
  • act_runner logs showing unusual workflow execution patterns
  1. Process evidence:
  • nsenter process spawned from within a container
  • Privileged commands executed from container context
  • Unexpected network connections from runner host
  1. Filesystem evidence:
  • Modified cron jobs or systemd services on runner host
  • Unexpected files in /tmp or home directories
  • Modified SSH keys or authorized_hosts files
  1. Behavioral evidence:
  • Unusual resource consumption (CPU, memory, network)
  • Unexpected outbound connections to external IPs
  • Failed login attempts or privilege escalation attempts in logs

Investigation steps:

  1. Isolate the runner host from the network immediately
  2. Preserve logs and filesystem for forensic analysis
  3. Rotate all secrets and credentials used by the runner
  4. Audit all jobs executed on the runner in the past 30 days
  5. Check for lateral movement to other infrastructure

What is the difference between blocking –cap-add and setting Privileged: false?

Aspect Privileged: false --cap-add blocking
Scope Controls Docker’s privileged mode flag Controls individual Linux capabilities
Granularity All-or-nothing (true/false) Per-capability control
Escape vector Blocks privileged mode but not alternatives Blocks specific dangerous capabilities
Effectiveness Insufficient alone (this vulnerability) Necessary but requires comprehensive list
Bypass difficulty Easy (use --pid=host instead) Hard (requires multiple capabilities)

Best practice: Use both together. Set Privileged: false AND block dangerous capabilities like SYS_ADMIN, SYS_PTRACE, and ALL.


References

  • Gitea Runner Repository: https://gitea.io/en-us/
  • Docker Security Best Practices: https://docs.docker.com/engine/security/
  • Linux Namespaces Documentation: https://man7.org/linux/man-pages/man7/namespaces.7.html
  • nsenter Utility Manual: https://man7.org/linux/man-pages/man1/nsenter.1.html
  • NIST Container Security Guide: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-190.pdf
  • CIS Docker Benchmark: https://www.cisecurity.org/benchmark/docker

Document Version: 1.0
Last Updated: 2024
Classification: Security Advisory
Distribution: Internal Use / Affected Organizations

← Back to IT Security