Your App Pods Can Talk to the Kubernetes API (And You Probably Didn't Know)
You just realized your app pods can talk to the Kubernetes API.
I mean it. Open a shell inside any running pod and try this:
curl -k https://kubernetes.default.svc/api/v1/namespaces/default/pods \
-H "Authorization: Bearer *** /var/run/secrets/kubernetes.io/serviceaccount/token)"
If you haven’t touched RBAC, this returns a list of pods. Your application code, running inside a pod, can query (and depending on the default setup, even modify) Kubernetes objects. That’s a problem if that pod ever gets compromised, has a dependency with a vulnerability, or is running code from a supply chain attack.
The fix is RBAC: Role-Based Access Control. It’s been stable since Kubernetes 1.8. There’s no excuse not to use it.
How pod-to-API communication works
Every pod gets a service account. By default, this is the default service account in the pod’s namespace. Kubernetes automatically mounts a token for this service account at /var/run/secrets/kubernetes.io/serviceaccount/token inside every container.
The API server’s address and CA cert are also mounted:
cat /var/run/secrets/kubernetes.io/serviceaccount/token # the bearer token
cat /var/run/secrets/kubernetes.io/serviceaccount/ca.crt # the API server's CA
cat /var/run/secrets/kubernetes.io/serviceaccount/namespace # the pod's namespace
Any code in the pod can use these to authenticate to the API server. What it can do depends on the RBAC rules bound to that service account.
Here’s the problem: in many clusters, the default service account has been granted permissions (sometimes cluster-admin!) because someone needed their app to talk to the API and couldn’t be bothered to create a proper role. Or a Helm chart shipped with overly permissive RBAC rules. Or nobody thought about it at all.
RBAC objects: the four pieces
RBAC in Kubernetes involves four resource types, all under rbac.authorization.k8s.io/v1:
Role: Defines what actions are allowed on which resources within a single namespace.
ClusterRole: Same as a Role but cluster-wide (or for cluster-scoped resources like Nodes).
RoleBinding: Grants a Role (or ClusterRole) to a user, group, or service account within a namespace.
ClusterRoleBinding: Grants a ClusterRole to a user, group, or service account across the entire cluster.
The flow is: define what’s allowed (Role/ClusterRole), then attach it to who needs it (RoleBinding/ClusterRoleBinding).
Example: let a pod read ConfigMaps and nothing else
This is a common real-world need. An application needs to read ConfigMaps (maybe to watch for configuration changes) but shouldn’t be able to read Secrets, delete Deployments, or do anything else.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: configmap-reader
namespace: default
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list", "watch"]
This Role allows reading ConfigMaps in the default namespace. The empty string "" in apiGroups means the core API group (Pods, Services, ConfigMaps, Secrets, etc. all live there).
Now create a service account for the app:
apiVersion: v1
kind: ServiceAccount
metadata:
name: myapp-sa
namespace: default
Bind the Role to the service account:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: myapp-configmap-reader
namespace: default
subjects:
- kind: ServiceAccount
name: myapp-sa
namespace: default
roleRef:
kind: Role
name: configmap-reader
apiGroup: rbac.authorization.k8s.io
Then use the service account in your Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: default
spec:
replicas: 2
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
serviceAccountName: myapp-sa
containers:
- name: myapp
image: busybox:1.33
command: ['sleep', '3600']
Now verify from inside the pod. The pod can read ConfigMaps:
curl -k https://kubernetes.default.svc/api/v1/namespaces/default/configmaps \
-H "Authorization: Bearer *** /var/run/secrets/kubernetes.io/serviceaccount/token)"
But trying to read Secrets will fail:
curl -k https://kubernetes.default.svc/api/v1/namespaces/default/secrets \
-H "Authorization: Bearer *** /var/run/secrets/kubernetes.io/serviceaccount/token)"
# Returns 403 Forbidden
That’s least privilege in action.
Verifying what a service account can do
The kubectl auth can-i command is your friend:
kubectl auth can-i list configmaps --as=system:serviceaccount:default:myapp-sa
# yes
kubectl auth can-i get secrets --as=system:serviceaccount:default:myapp-sa
# no
kubectl auth can-i delete pods --as=system:serviceaccount:default:myapp-sa
# no
Use this to verify your RBAC setup before deploying. It’s faster than testing from inside a pod.
ClusterRole vs Role: when you need cluster scope
Use a ClusterRole when:
- The service account needs to read cluster-scoped resources (Nodes, PersistentVolumes, Namespaces)
- You want to define permissions once and bind them across multiple namespaces
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: node-reader
rules:
- apiGroups: [""]
resources: ["nodes"]
verbs: ["get", "list"]
Bind it with a ClusterRoleBinding for cluster-wide access, or with a RoleBinding in a specific namespace (yes, you can bind a ClusterRole with a RoleBinding to scope it to a single namespace).
The common mistakes
Using cluster-admin for app pods. I’ve seen Helm charts that create a ClusterRoleBinding to cluster-admin for a service account. This gives the pod full control over the entire cluster. If that pod is compromised, the attacker owns your cluster. Never do this for application workloads.
Not setting automountServiceAccountToken: false when the pod doesn’t need API access. Most application pods don’t need to talk to the Kubernetes API at all. Disable token auto-mounting:
apiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
automountServiceAccountToken: false
containers:
- name: myapp
image: busybox:1.33
command: ['sleep', '3600']
This prevents the token from being mounted entirely. No token, no API access.
Overly broad verbs. Don’t grant "*" (all verbs) when you only need "get". The list and watch verbs can return data for all resources of a type. The create, update, delete verbs can modify cluster state. Be specific.
Forgetting about escalate and bind. If you grant a service account the ability to create or modify Roles and RoleBindings, it can grant itself more permissions. This is privilege escalation. Don’t give app pods RBAC write access.
Auditing what’s currently in place
If you’re inheriting a cluster and want to see what RBAC rules exist:
# List all Roles in a namespace
kubectl get roles -n default
# List all ClusterRoles
kubectl get clusterroles
# Describe a specific role to see its rules
kubectl describe clusterrole cluster-admin
# List all RoleBindings
kubectl get rolebindings -n default
# List all ClusterRoleBindings
kubectl get clusterrolebindings
Look for any binding that targets system:serviceaccount:*:default (the default service account in any namespace). That’s usually a sign that someone needed API access and took the easy route.
A practical checklist
For every new namespace and workload:
- Create a dedicated service account (don’t use
default) - Set
automountServiceAccountToken: falseunless the pod needs API access - If it needs API access, create a Role with minimum permissions
- Bind the Role to the specific service account
- Verify with
kubectl auth can-i - Test from inside the pod
RBAC isn’t hard. The concepts are simple: define permissions, bind them to identities. The hard part is organizational: getting teams to actually use it instead of granting cluster-admin and moving on. Start by auditing what you have. Fix the worst offenders. Enforce the pattern for new workloads. The security improvement is disproportionate to the effort.
Related Posts
Passing the CKA exam after years of Kubernetes
How I prepared for and passed the Certified Kubernetes Administrator exam, and what I'd do differently knowing what I know now.
Choosing a Kubernetes dashboard for your team
Headlamp, OpenShift Console, or the deprecated Kubernetes Dashboard. Here's how they compare and which one fits your team.
Namespace Is the New Cluster Boundary
Namespaces aren't just for organizing resources. They're your first line of defense against a single team eating the whole cluster.