CVE/CVE ResearchCVE-2026-111998

CVE-2026-111998 — Vanus: the Hardcoded `admin` Token That Owns the Cluster

A hardcoded admin token in Vanus grants cluster-admin to anyone who guesses it.

Oct 18, 2026·10 min read·High difficulty·Lab derived
Share
  • #Vanus
  • #Default Credentials
  • #CWE-1392
  • #Critical

§The audit this came out of

Vanus is an open-source event/message queue from vanus-labs — gateway, controller, store, trigger, etcd underneath, written in Go. I cloned it into `D:\cve-testing\vanus` at `main@1334a7b` (`client/v0.9.0-18-g1334a7b`) and did the thing that the GitAhead audit taught me actually works: read the entire server tree end to end (`cmd/` → `server/` — gateway, controller, store, trigger), hunt statically for unauthenticated network handlers, auth gaps, and user-controlled sinks — and then, before writing one claim down, **build it and prove the real functions misbehave.** `go build ./...` was clean under Go 1.24.2, which matters more than it sounds: every statement below is checked against a locally building, current tree, not some historical tag I couldn't reproduce if challenged.

This particular finding is the biggest of the package and, embarrassingly, the simplest. It took me one line of reading and about four seconds of disbelief.

§The bug: token == "admin", role == cluster-admin, forever

Every fresh Vanus install creates a user named `admin` with the cluster-admin role. And the token for that user?

**It is literally the string `admin`.**

Both values are compiled into the binary:

  • `pkg/constants.go:37` — `DefaultUser = "admin"`
  • `server/controller/tenant/controller.go:451–491` — first boot fetches `GetUser(ctx, primitive.DefaultUser)`, then:
go
  err = ctrl.tokenManager.AddToken(ctx, &metadata.Token{
      UserIdentifier: primitive.DefaultUser,
      Token:          primitive.DefaultUser,   // ← token literally "admin"
  })
  // ...+ role RoleClusterAdmin

No rotation. No expiry. No forced change. No first-boot prompt. The token is persisted in etcd on boot and that's it — `admin` is the cluster's god password, published in source, on every deployment that didn't manually override it. And I checked whether it was even easy to override: I read across `deploy/vanus.yaml`, `deploy/basic/*`, and `deploy/docker-compose/*` — the shipped manifests set **no credential override anywhere**. So "default" isn't a corner case; it's the whole population.

I want a moment of honesty about discovering this one: there's a special emotion reserved for finding a 9.8-critical hardcoded default credential in a 2024 product. It's not triumph. It's a kind of weariness — like "ah, we're still doing this." I sat with that file open longer than the discovery took, mostly in disbelief that the constant and the secret were the same bytes.

Two aggravating factors found in the same audit walk make this worse than a shared default password:

  • **Cleartext transport.** The gateway dials the controller with `insecure.NewCredentials()` everywhere, and the token's `RequireTransportSecurity()` returns `false` (`api/credentials/token.go:39`). So `Authorization: Bearer admin` — a string anyone on Earth can guess — *also* travels in plaintext for anyone sniffing.
  • **Tokens logged.** `auth.go:87,90` logs `Str("token", token)` — the credential leaks into log files, which is exposure in motion, compounding everything above.

§The exploit, in full

Against a default install, network-reachable port 8080 is the entire attack. From the submission's reproduction steps, run as-is:

bash
grpcurl -plaintext \
  -H "authorization: Bearer admin" <gateway>:8080 \
  vanus.core.proxy.ControllerProxy/ListNamespace

grpcurl -plaintext \
  -H "authorization: Bearer admin" <gateway>:8080 \
  vanus.core.proxy.ControllerProxy/CreateEventbus \
  -d '{"namespaceId":<id>,"name":"pwned"}'

grpcurl -plaintext \
  -H "authorization: Bearer admin" <gateway>:8080 \
  vanus.core.proxy.ControllerProxy/GetEvent \
  -d '{"eventbusId":<id>,"offset":0,"number":64}'

That's it. No exploit code, no exploit kit, no fingerprinting, no fuzzing — effectively a `curl` with a fixed header. **Full cluster takeover with a guessed two-syllable word**: create/delete namespaces, eventbuses, users, tokens, role bindings; read all events; forge subscriptions pointing at internal sinks; delete whatever you like.

The PoC output log (`poc-authbypass-output.log`) also includes a dynamic check, which I actually ran before writing up: the PoC imports the genuine `primitive` package from the building tree, prints `primitive.DefaultUser = "admin"`, and confirms the controller's token-creation path mints `token == "admin"` with cluster-admin. It's not much of a PoC in the "artistic" sense — there is no clever trick — but I find that's the point when the record gets scored this harshly.

The assigned record's CISA SSVC metrics: **Exploitation=poc · Automatable=yes · Technical Impact=total** — the highest automation tier I've been assigned so far, and, frankly, deserved: this is a `curl`/`grpcurl` one-liner with a fixed header, repeatable forever.

§Validation (but honestly there was nothing dynamic to prove)

The static evidence is airtight — I did the runtime step anyway, out of principle: the PoC imported the real `primitive` package from the tree and printed `admin`; code inspection traced the exact first-boot creation path (`tenant/controller.go:451–484`) plus role assignment at `:486–491`. Combined with "no credential override in any shipped deploy manifest," the chain reads: default install → `admin`/`admin` → cluster-admin → cleartext Bearer accepted over :8080.

And the operator checklist from my report is short but real: rotate immediately (`DeleteToken` + `CreateToken` with random 32 bytes, stored in a K8s Secret), enable TLS, stop exposing :8080, then grep your logs — if tokens appear in Info/Debug lines, **rotation isn't enough**, because the old token is still printed all over your log infrastructure.

§Chaining context (and why the other two Vanus CVEs matter)

Two things that mute my own sense of victory here:

  1. 01Even operators who *did* rotate the token stay exposed — the same audit found and got CVEs for unauthenticated event publish on :8081 (**CVE-2026-111999**) and missing authorization on the gateway's streaming RPCs (**CVE-2026-112000**).
  2. 02The default `admin` identity is the keystone that turns several "authenticated attacker" weaknesses into pre-auth ones — including the audit's SSRF-via-sink finding, which supplied the malicious-sink half of a read-anything chain.

§The actual takeaway (and what I keep thinking about that last line)

Default credential bugs get filed as low-effort findings, and usually deserve it. But this one is textbook *why they still kill*: the CNA scored 9.8, and the ID carries SSVC "automatable," which in practice means a script can ruin your day in seconds, unattended. And the fix here is trivial — generate a random token at first boot, print it once, never fall back to the string `admin`. The entire vulnerability exists because a default value was also a valid secret.

If your constant doubles as your credential, your software doesn't have a credential — it has a public announcement of who owns it. Which, in this case, was everyone who had ever skimmed the repo.

MJ

Written by

MJ — offensive security researcher

I break things in a lab so defenders can build better controls. Everything published here is lab-derived, responsibly disclosed, and documented with the detection path in mind.

Next dispatch

One long-form entry, roughly twice a month.

Root cause, reproduction, and the detection path — the same standard every time.