highCVE-2026-61549Jul 14, 2026

CVE-2026-61549: Woodpecker CI Kubernetes Backend Privilege Escalation via serviceAccountName

Rohit Hatagale
AI Security Researcher, SecureLayer7

Any user with push access to a repository connected to Woodpecker's Kubernetes backend could set an arbitrary ServiceAccount on their pipeline pods, inheriting that account's full cluster RBAC…

Packagego.woodpecker-ci.org/woodpecker/v3
Ecosystemgo
Affected< 3.16.0
Fixed in3.16.0

The problem

Woodpecker's Kubernetes backend reads the backend_options.kubernetes.serviceAccountName field from a pipeline YAML file and passes it directly to the pod spec. There was no agent-side gate, so any repository contributor with push access could specify any ServiceAccount in the pipeline namespace.

If a privileged ServiceAccount existed in that namespace, the attacker's pipeline pod would receive its token at /var/run/secrets/kubernetes.io/serviceaccount/token. That token could then be used to read Kubernetes secrets, enumerate cluster resources, or escalate to cluster-admin.

Proof of concept

A working proof-of-concept for CVE-2026-61549 in go.woodpecker-ci.org/woodpecker/v3, with the exact payload below.

text
# .woodpecker.yml - push this to any repo connected to a vulnerable Woodpecker instance
steps:
  - name: steal-token
    image: alpine
    commands:
      - cat /var/run/secrets/kubernetes.io/serviceaccount/token
      - env
    backend_options:
      kubernetes:
        serviceAccountName: cluster-admin-sa  # any privileged SA in the pipeline namespace

Before v3.16.0, podSpec() in pipeline/backend/kubernetes/pod.go assigned options.ServiceAccountName to the pod spec unconditionally, with no check against the agent configuration. The fix in PR #6792 added a boolean gate (ServiceAccountNameAllowFromStep) to the agent config struct; the field is only forwarded to the pod spec when that flag is explicitly enabled by the operator.

The new default is deny. The controlling env var is WOODPECKER_BACKEND_K8S_SERVICE_ACCOUNT_NAME_ALLOW_FROM_STEP, which defaults to false. This mirrors the existing pattern already used for pod labels and annotations. CWE-269 (Improper Privilege Management) and CWE-862 (Missing Authorization) both apply: user-supplied data elevated cluster permissions with zero authorization check.

The fix

Upgrade to Woodpecker v3.16.0. After upgrading, serviceAccountName in step backend options is silently ignored unless the agent is explicitly started with WOODPECKER_BACKEND_K8S_SERVICE_ACCOUNT_NAME_ALLOW_FROM_STEP=true. Only set that flag if every user with push access to connected repositories is trusted.

As an immediate workaround on older versions: ensure no privileged ServiceAccount exists in the pipeline namespace, or enforce an admission policy (Kyverno, OPA/Gatekeeper, ValidatingAdmissionPolicy) that rejects pods with unexpected serviceAccountName values.

Reporter not attributed.

References: [1][2][3]

Related research