GHSA-5587-2X54-JJ6H
Vulnerability from github – Published: 2026-07-17 21:46 – Updated: 2026-07-17 21:46Description
The routesrv component exposes the full cluster route topology (Ingress/RouteGroup configurations, backend URLs, filter chains, OAuth/OIDC callback paths) and cache-cluster topology (Redis/Valkey shard addresses) over plain HTTP with zero authentication. Any pod in the Kubernetes cluster can reach routesrv via its predictable DNS name and retrieve sensitive cluster-wide routing and cache infrastructure data.
Vulnerable Code
routesrv/routesrv.go:87-99,114-137 — all handler registrations on the main mux:
mux.Handle("/routes", b) // eskipBytes.ServeHTTP — all route data
mux.Handle("/routes/{zone}", b) // zone-scoped route data
mux.Handle("/swarm/redis/shards", rh) // Redis cluster addresses
mux.Handle("/swarm/valkey/shards", vh) // Valkey cluster addresses
routesrv/eskipbytes.go:134-196 — eskipBytes.ServeHTTP:
func (e *eskipBytes) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
// ... only checks GET/HEAD method, NO auth check
if r.Method != "GET" && r.Method != "HEAD" {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
// ... serves all route data immediately
}
routesrv/redishandler.go:28-41 — RedisHandler.ServeHTTP:
func (rh *RedisHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
// ... serves Redis cluster addresses immediately, NO auth check
}
routesrv/valkeyhandler.go:28-41 — ValkeyHandler.ServeHTTP:
func (vh *ValkeyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
// ... serves Valkey cluster addresses immediately, NO auth check
}
Attack Path
- Initial Compromise: Attacker compromises any pod in the Kubernetes cluster (via application CVE, supply-chain attack, malicious container image, etc.)
- Discovery: Attacker discovers routesrv via predictable Kubernetes DNS name:
skipper-ingress-routesrv.kube-system.svc.cluster.local:9090(documented atdocs/tutorials/operations.md:108,docs/tutorials/ratelimit.md:137,197) - Data Extraction without Auth:
GET http://<routesrv>:9090/routes→ All Ingress/RouteGroup configurations across ALL namespacesGET http://<routesrv>:9090/swarm/redis/shards→ Redis cache cluster node addressesGET http://<routesrv>:9090/swarm/valkey/shards→ Valkey cache cluster node addresses- Subsequent Attacks: With cache cluster topology, attacker can perform direct cache-level attacks (ratelimit data manipulation, session data exfiltration)
Permission Boundary Analysis
The routesrv uses a ServiceAccount with cluster-wide RBAC to list Ingress (networking.k8s.io), RouteGroup (zalando.org), Endpoints, and Services across all namespaces (see clusterclient.go:648-653 fetchClusterState). The kube-apiserver requires proper ServiceAccount token + RBAC authorization for the Kubernetes API itself, but routesrv exposes the aggregated data over HTTP with zero authentication.
A compromised pod with limited RBAC (restricted to its own namespace) can bypass Kubernetes RBAC entirely by reading routesrv. This crosses the boundary from "namespace-scoped Kubernetes workload with restricted RBAC" to "full cluster route topology across all namespaces".
No NetworkPolicy manifests exist in the deploy/ directory. The default Kubernetes flat network model allows any pod to reach any service, further widening the attack surface.
Exposed Data
| Endpoint | Data Exposed | Impact |
|---|---|---|
GET /routes |
All ingress/routegroup backends: internal service URLs, filter chains (auth, rate limiting, OAuth, JWT, OPA policies), load balancer group membership | Cluster-wide reconnaissance, targeted backend attacks |
GET /routes/{zone} |
Zone-scoped subset of above route data | Same, scoped |
GET /swarm/redis/shards |
Redis cluster internal IP:port pairs | Direct cache-level attacks, ratelimit data manipulation |
GET /swarm/valkey/shards |
Valkey cluster internal IP:port pairs | Same |
Additionally, the data-plane client (eskipfile/remote.go:190-219) also performs plain HTTP GET with no credentials — only an ETag header is sent — confirming that no auth capability exists in the architecture at all.
Mitigation
- Add authentication to all routesrv HTTP endpoints (basic auth, bearer token, mTLS, or shared secret) via flag
-route-server-filters="" - Deploy Kubernetes NetworkPolicies restricting ingress to routesrv to only the data-plane skipper pod selectors
- Consider using mutual TLS authentication between data-plane and control-plane components
NetworkPolicy does not remove the missing-auth condition
Restrictive NetworkPolicies are a valid mitigation, but they are not an application-layer authentication mechanism. The security-relevant defect remains that routesrv serves control-plane-derived data to unauthenticated callers whenever network reachability exists.
Impact framing
This report does not rely on claiming direct integrity or availability impact. The verified issue is a confidentiality-focused control-plane exposure: route definitions, backend topology, filter-chain details, and Redis/Valkey shard addresses become readable to any reachable in-cluster client.
Resources
routesrv/routesrv.go:87-99— handler registration (zero auth)routesrv/eskipbytes.go:134-196— route data handler (no auth)routesrv/redishandler.go:28-41— Redis shard handler (no auth)routesrv/valkeyhandler.go:28-41— Valkey shard handler (no auth)dataclients/kubernetes/clusterclient.go:648-653—fetchClusterState()— shows cluster-wide RBACeskipfile/remote.go:190-219— data-plane client also has no auth capabilitydocs/tutorials/operations.md:108,docs/tutorials/ratelimit.md:137,197— documented routesrv DNS name
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/zalando/skipper"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "0.27.13"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-54246"
],
"database_specific": {
"cwe_ids": [
"CWE-306"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-17T21:46:18Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "## Description\n\nThe `routesrv` component exposes the full cluster route topology (Ingress/RouteGroup configurations, backend URLs, filter chains, OAuth/OIDC callback paths) and cache-cluster topology (Redis/Valkey shard addresses) over plain HTTP with **zero authentication**. Any pod in the Kubernetes cluster can reach routesrv via its predictable DNS name and retrieve sensitive cluster-wide routing and cache infrastructure data.\n\n## Vulnerable Code\n\n**routesrv/routesrv.go:87-99,114-137** \u2014 all handler registrations on the main mux:\n```go\nmux.Handle(\"/routes\", b) // eskipBytes.ServeHTTP \u2014 all route data\nmux.Handle(\"/routes/{zone}\", b) // zone-scoped route data\nmux.Handle(\"/swarm/redis/shards\", rh) // Redis cluster addresses\nmux.Handle(\"/swarm/valkey/shards\", vh) // Valkey cluster addresses\n```\n\n**routesrv/eskipbytes.go:134-196** \u2014 `eskipBytes.ServeHTTP`:\n```go\nfunc (e *eskipBytes) ServeHTTP(rw http.ResponseWriter, r *http.Request) {\n // ... only checks GET/HEAD method, NO auth check\n if r.Method != \"GET\" \u0026\u0026 r.Method != \"HEAD\" {\n w.WriteHeader(http.StatusMethodNotAllowed)\n return\n }\n // ... serves all route data immediately\n}\n```\n\n**routesrv/redishandler.go:28-41** \u2014 `RedisHandler.ServeHTTP`:\n```go\nfunc (rh *RedisHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n if r.Method != \"GET\" {\n w.WriteHeader(http.StatusMethodNotAllowed)\n return\n }\n // ... serves Redis cluster addresses immediately, NO auth check\n}\n```\n\n**routesrv/valkeyhandler.go:28-41** \u2014 `ValkeyHandler.ServeHTTP`:\n```go\nfunc (vh *ValkeyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n if r.Method != \"GET\" {\n w.WriteHeader(http.StatusMethodNotAllowed)\n return\n }\n // ... serves Valkey cluster addresses immediately, NO auth check\n}\n```\n\n## Attack Path\n\n1. **Initial Compromise**: Attacker compromises any pod in the Kubernetes cluster (via application CVE, supply-chain attack, malicious container image, etc.)\n2. **Discovery**: Attacker discovers routesrv via predictable Kubernetes DNS name: `skipper-ingress-routesrv.kube-system.svc.cluster.local:9090` (documented at `docs/tutorials/operations.md:108`, `docs/tutorials/ratelimit.md:137,197`)\n3. **Data Extraction without Auth**: \n - `GET http://\u003croutesrv\u003e:9090/routes` \u2192 All Ingress/RouteGroup configurations across ALL namespaces\n - `GET http://\u003croutesrv\u003e:9090/swarm/redis/shards` \u2192 Redis cache cluster node addresses\n - `GET http://\u003croutesrv\u003e:9090/swarm/valkey/shards` \u2192 Valkey cache cluster node addresses\n4. **Subsequent Attacks**: With cache cluster topology, attacker can perform direct cache-level attacks (ratelimit data manipulation, session data exfiltration)\n\n## Permission Boundary Analysis\n\nThe routesrv uses a ServiceAccount with **cluster-wide RBAC** to list Ingress (networking.k8s.io), RouteGroup (zalando.org), Endpoints, and Services across all namespaces (see `clusterclient.go:648-653` `fetchClusterState`). The kube-apiserver requires proper ServiceAccount token + RBAC authorization for the Kubernetes API itself, but **routesrv exposes the aggregated data over HTTP with zero authentication**.\n\nA compromised pod with limited RBAC (restricted to its own namespace) can bypass Kubernetes RBAC entirely by reading routesrv. This crosses the boundary from *\"namespace-scoped Kubernetes workload with restricted RBAC\"* to *\"full cluster route topology across all namespaces\"*.\n\nNo NetworkPolicy manifests exist in the `deploy/` directory. The default Kubernetes flat network model allows any pod to reach any service, further widening the attack surface.\n\n## Exposed Data\n\n| Endpoint | Data Exposed | Impact |\n|----------|-------------|--------|\n| `GET /routes` | All ingress/routegroup backends: internal service URLs, filter chains (auth, rate limiting, OAuth, JWT, OPA policies), load balancer group membership | Cluster-wide reconnaissance, targeted backend attacks |\n| `GET /routes/{zone}` | Zone-scoped subset of above route data | Same, scoped |\n| `GET /swarm/redis/shards` | Redis cluster internal IP:port pairs | Direct cache-level attacks, ratelimit data manipulation |\n| `GET /swarm/valkey/shards` | Valkey cluster internal IP:port pairs | Same |\n\nAdditionally, the data-plane client (`eskipfile/remote.go:190-219`) also performs plain HTTP GET with no credentials \u2014 only an ETag header is sent \u2014 confirming that no auth capability exists in the architecture at all.\n\n\n## Mitigation\n\n1. Add authentication to all routesrv HTTP endpoints (basic auth, bearer token, mTLS, or shared secret) via flag `-route-server-filters=\"\"`\n2. Deploy Kubernetes NetworkPolicies restricting ingress to routesrv to only the data-plane skipper pod selectors\n3. Consider using mutual TLS authentication between data-plane and control-plane components\n\n\n### NetworkPolicy does not remove the missing-auth condition\nRestrictive NetworkPolicies are a valid mitigation, but they are not an application-layer authentication mechanism. The security-relevant defect remains that routesrv serves control-plane-derived data to unauthenticated callers whenever network reachability exists.\n\n### Impact framing\nThis report does **not** rely on claiming direct integrity or availability impact. The verified issue is a confidentiality-focused control-plane exposure: route definitions, backend topology, filter-chain details, and Redis/Valkey shard addresses become readable to any reachable in-cluster client.\n\n## Resources\n\n- `routesrv/routesrv.go:87-99` \u2014 handler registration (zero auth)\n- `routesrv/eskipbytes.go:134-196` \u2014 route data handler (no auth)\n- `routesrv/redishandler.go:28-41` \u2014 Redis shard handler (no auth)\n- `routesrv/valkeyhandler.go:28-41` \u2014 Valkey shard handler (no auth)\n- `dataclients/kubernetes/clusterclient.go:648-653` \u2014 `fetchClusterState()` \u2014 shows cluster-wide RBAC\n- `eskipfile/remote.go:190-219` \u2014 data-plane client also has no auth capability\n- `docs/tutorials/operations.md:108`, `docs/tutorials/ratelimit.md:137,197` \u2014 documented routesrv DNS name",
"id": "GHSA-5587-2x54-jj6h",
"modified": "2026-07-17T21:46:18Z",
"published": "2026-07-17T21:46:18Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/zalando/skipper/security/advisories/GHSA-5587-2x54-jj6h"
},
{
"type": "PACKAGE",
"url": "https://github.com/zalando/skipper"
},
{
"type": "WEB",
"url": "https://github.com/zalando/skipper/releases/tag/v0.27.13"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:A/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "Skipper\u0027s routesrv-no-auth component: All routesrv API Endpoints Lack Authentication"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.