skip to content

What RBAC and security setup does Spring Cloud Kubernetes discovery require, and how do you minimize the permission blast radius?

level: seniorimportance: should knowfreq 35%

answer

  1. ServiceAccount token -> kube-apiserver
  2. get/list/watch on services + endpoints
  3. Role (namespace) vs ClusterRole (all-namespaces)
  4. Read-only, no write verbs
  5. Discovery Server = zero RBAC in apps

basics

~20 s

The pod's ServiceAccount needs read access (get/list/watch) to Kubernetes services and endpoints in the relevant namespaces. Grant it with a Role/RoleBinding (namespace-scoped) — or a ClusterRole for all-namespaces. To avoid giving every app this access, route through a Discovery Server that holds the permissions.

solid answer

~40 s

Discovery reads Service and Endpoints objects from the kube-apiserver, so the pod's ServiceAccount must have RBAC verbs get, list, and watch on resources services and endpoints (and often pods/endpointslices depending on version). Namespace-scoped discovery needs a Role + RoleBinding in that namespace; all-namespaces discovery needs a ClusterRole + ClusterRoleBinding, which is far broader. To limit blast radius, prefer single-namespace discovery so you can use a narrow Role, and grant only read verbs — never write. For many apps, the cleaner pattern is the Spring Cloud Kubernetes Discovery Server: one deployment holds the RBAC and serves discovery over HTTP, so individual app ServiceAccounts need no cluster API permissions at all. Also mount a dedicated ServiceAccount per app rather than reusing 'default', so permissions are auditable and least-privilege.

code

kotlin · 24 lines
kotlin
// Namespace-scoped least-privilege RBAC (YAML shown as a string for reference)
val rbac = """
apiVersion: v1
kind: ServiceAccount
metadata: { name: orders-sa, namespace: shop }
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: { name: discovery-read, namespace: shop }
rules:
  - apiGroups: [""]
    resources: ["services", "endpoints"]
    verbs: ["get", "list", "watch"]   # read-only
  - apiGroups: ["discovery.k8s.io"]
    resources: ["endpointslices"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: { name: discovery-read-bind, namespace: shop }
subjects: [{ kind: ServiceAccount, name: orders-sa, namespace: shop }]
roleRef: { kind: Role, name: discovery-read, apiGroup: rbac.authorization.k8s.io }
"""
// Deployment pod spec: spec.template.spec.serviceAccountName: orders-sa

go deeper

for a junior

Know that the pod needs permission to read services/endpoints via a ServiceAccount.

for a middle

Should name get/list/watch on services and endpoints and Role vs ClusterRole.

for a senior

Should design least-privilege RBAC, explain why watch matters, and know the Discovery Server tradeoff.

for a principal

Should weigh blast radius, apiserver watch load at fleet scale, per-app ServiceAccount auditing, and centralized-vs-embedded discovery topology.

**Why RBAC matters.** Unlike Eureka, where auth is between app and registry server, here the 'registry' is the **kube-apiserver** itself. Every discovery lookup is an authenticated, authorized API call made with the pod's **ServiceAccount** token (mounted at `/var/run/secrets/kubernetes.io/serviceaccount`). If that ServiceAccount lacks permission, discovery returns nothing / fails with a 403. **Exactly what permissions.** Discovery reads: - `services` — to enumerate service names and their port/metadata, - `endpoints` (and on newer clusters `endpointslices` in apiGroup `discovery.k8s.io`) — to get the ready pod addresses, - sometimes `pods` — for richer metadata in some configurations. The verbs needed are the read triad: **get, list, watch** (`watch` because informer-based clients open a watch stream to keep a local cache fresh). **No write verbs** should ever be granted — discovery never mutates cluster state. **Scoping: Role vs ClusterRole.** This is the key security lever: - **Namespace-scoped** (default, `all-namespaces=false`): bind a **Role** + **RoleBinding** in the app's namespace. The permission is confined to that namespace — least privilege. - **All-namespaces** (`spring.cloud.kubernetes.discovery.all-namespaces=true`): you must use a **ClusterRole** + **ClusterRoleBinding**, granting read of services/endpoints across the *entire* cluster. That's a big surface; only enable it if you genuinely need cross-namespace discovery. **Least-privilege practices:** 1. **Dedicated ServiceAccount per app.** Never rely on the namespace `default` ServiceAccount; create `orders-sa` and set `serviceAccountName` in the pod spec so grants are explicit and auditable. 2. **Read-only, minimal resources.** Only `get/list/watch` on `services` and `endpoints`. 3. **Prefer namespace scope.** Keep `all-namespaces=false` unless required. 4. **Discovery Server pattern.** The **Spring Cloud Kubernetes Discovery Server** is a single deployment that holds the (possibly cluster-wide) RBAC and exposes discovery over HTTP. App pods use a thin `DiscoveryClient` pointed at `spring.cloud.kubernetes.discovery.discovery-server-url` and need **zero** Kubernetes API permissions. This shrinks the blast radius (only one workload can read the cluster's services), and reduces the number of watch connections against the apiserver — important at scale. **Transport & token security.** API calls use TLS to the apiserver validated against the cluster CA bundle that's auto-mounted; the bearer token is the ServiceAccount token. You generally don't configure these manually inside the cluster — the Fabric8/official client auto-detects in-cluster config. Outside the cluster (local dev) it falls back to your kubeconfig. **Common failures / gotchas:** - Empty `getServices()`/`getInstances()` with no error often means missing `list` permission or wrong namespace. - Discovery works at startup but goes stale → missing `watch` verb, so the informer can't refresh. - Turning on `all-namespaces` but only binding a namespaced Role → cross-namespace reads silently fail. - Reusing an over-privileged shared ServiceAccount defeats least privilege and complicates audits.

  • Discovery returns the right instances at startup but never sees a newly scaled pod. Which permission is missing?
    The watch verb. Informer-based discovery keeps a live cache via a watch stream; without 'watch' it can only do an initial list and won't observe subsequent changes.
  • How does the Discovery Server change the RBAC story?
    It concentrates the (possibly cluster-wide) get/list/watch permissions into one deployment that serves discovery over HTTP. App pods just call that server, so their ServiceAccounts need no Kubernetes API permissions, shrinking the blast radius and apiserver watch load.

saying these in an interview costs you the question

  • Granting write verbs (create/update/delete) — discovery is read-only
  • Using a ClusterRole for single-namespace discovery
  • Relying on the default ServiceAccount instead of a dedicated one
  • Forgetting the watch verb, causing stale instance lists

context