
Kubernetes RBAC and Service Account Security
An examination of Kubernetes RBAC and service account security strategies to enhance cluster protection using Kyverno and pod security policies.
The Mechanism of Machine Identity in Kubernetes
Kubernetes distinguishes sharply between human users and the processes running within its cluster. While users authenticate via OIDC or certificates, pods operate as "machine identities" defined by ServiceAccounts. In a standard configuration, this identity is a bearer token automatically mounted into the pod's filesystem at /var/run/secrets/kubernetes.io/serviceaccount/token. When code executes inside a pod, it reads this token to authenticate against the API server. If the ServiceAccount is bound to a ClusterRole with excessive permissions, a compromised application effectively becomes a root-level attacker. This mechanism allows privilege escalation simply by reading a file on the local disk.
Part 11 of the Machine Identity & DevSecOps series explores how to sever this link between default configurations and cluster-wide access. We will examine the lifecycle of ServiceAccounts, the enforcement of least privilege via Kyverno, and the final defense layer provided by Pod Security Standards (PSS).
The ServiceAccount Lifecycle and the automountServiceAccountToken Risk
The first line of defense lies in the automountServiceAccountToken field. Historically, prior to Kubernetes 1.24, this defaulted to true, meaning every pod received a bearer token regardless of need. However, starting with Kubernetes 1.24, the default behavior shifted to false, significantly reducing the attack surface for modern clusters. Despite this native improvement, modern security postures still require explicitly configuring this field to ensure consistency across legacy namespaces or older cluster versions. Without this check, the principle of least privilege collapses because the default state can become "maximum trust" in environments that haven't fully migrated.
Consider a developer deploying a microservice named payment-service into the finance namespace. They create a ServiceAccount named payment-sa but omit the token configuration. They then bind this account to a Role allowing only ConfigMap reads.
apiVersion: v1
kind: ServiceAccount
metadata:
name: payment-sa
namespace: finance
# In K8s < 1.24, automountServiceAccountToken defaults to true if omitted.
# In K8s >= 1.24, it defaults to false.
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: payment-binding
namespace: finance
subjects:
- kind: ServiceAccount
name: payment-sa
namespace: finance
roleRef:
kind: Role
name: read-configmaps
apiGroup: rbac.authorization.k8s.ioWhile the RoleBinding restricts the action to reading ConfigMaps, if automountServiceAccountToken remains true (on older versions or misconfigured workloads), the mounted token allows an attacker exploiting a vulnerability (e.g., command injection) to enumerate resources. A malicious payload could execute:
curl -H "Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT/apis/v1/namespaces/default/podsIf the RoleBinding was accidentally misconfigured to a ClusterRole, the attacker gains full cluster access. The mechanism of protection must be enforced at the admission level to prevent this configuration error from ever reaching the API server, particularly in legacy namespaces where the default might still be true.
Enforcing Least Privilege with Kyverno Admission Control
Kyverno acts as a policy engine that intercepts requests before they reach the API server. It does not merely check permissions; it mutates admission requests to ensure compliance. The core mechanism here is the "Validate" rule, which inspects incoming Pod definitions against a set of constraints.
A Kyverno policy can enforce stricter postures than the native defaults, ensuring automountServiceAccountToken is explicitly set to false for all pods, or handling legacy namespaces where the native default might differ.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: enforce-service-account-token
spec:
validationFailureAction: Enforce
background: true
rules:
- name: check-automount-token
match:
any:
- resources:
kinds:
- Pod
validate:
message: "automountServiceAccountToken must be set to false for all pods."
pattern:
spec:
automountServiceAccountToken: falseWhen a deployment violates this policy, Kyverno intercepts the request. The API server never sees the Pod object, and the deployment fails immediately. This shifts the security model from "detect after deployment" to "prevent at creation," ensuring that the "least privilege" principle is baked into the deployment workflow rather than relied upon as a manual configuration step.
Pod Security Standards as the Final Defense Layer
RBAC and ServiceAccount hardening are insufficient on their own. Even if a token is hidden, a pod might request elevated privileges to bypass the API server entirely. Pod Security Standards (PSS) operate at a different layer: while RBAC controls what the API server allows, the Pod Security Admission controller (a separate admission controller) controls what the kubelet allows the container to do.
PSS mechanisms involve validating the securityContext of the pod. If a pod requests privileged: true, it runs with the same privileges as the host user, bypassing container isolation. If an attacker escapes the container, they are now on the host node. RBAC cannot stop this because the request is valid from the API server's perspective; the API server does not inherently know the pod will run as root on the host. The Pod Security Admission controller, however, blocks such requests based on the configured profile (Baseline, Restricted, or Privileged).
Kyverno can also enforce PSS by validating securityContext fields to ensure a restricted profile. This profile mandates:
privilegedis false.runAsNonRootis true.allowPrivilegeEscalationis false.- Specific capabilities (like
NET_ADMIN) are dropped.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: enforce-pod-security
spec:
validationFailureAction: Enforce
rules:
- name: check-privileged
match:
any:
- resources:
kinds:
- Pod
validate:
message: "Privileged containers are not allowed."
pattern:
spec:
containers:
- securityContext:
privileged: falseNote: For robust enforcement across multi-container pods, the pattern should use wildcard syntax (e.g., containers[*].securityContext.privileged: false) or allOf logic to ensure all containers adhere to the restriction.
Synthesis: The Multi-Layered Attack Workflow
Combining these mechanisms creates a comprehensive defense-in-depth strategy. Consider a threat actor gaining access to a CI/CD pipeline and attempting to deploy a malicious image that tries to mount the host filesystem.
- RBAC Check: The CI/CD user has a
ServiceAccountwithcreatepermissions onpods. The API server allows the request initially. - Kyverno Admission: Kyverno inspects the Pod spec. It sees
securityContext.privileged: true. The policy rejects the request immediately. The deployment fails. - Fallback Scenario: If Kyverno was misconfigured but
automountServiceAccountTokenwas disabled, the pod starts but has no token. The attacker cannot escalate to the API server. - Final Failure: If the attacker bypasses both layers and attempts to use
hostPathmounts, the native Pod Security Admission controller (built into Kubernetes 1.25+) would block thehostPathvolume type or therunAsUser: 0requirement. Alternatively, a Kyverno policy can enforce similar restrictions, but do not imply Kyverno PSS policies are the native admission controller itself.
This layered approach ensures that even if an attacker compromises a container, they are confined to a sandbox with no network access to the API server and no ability to escalate privileges to the host. The distinction between "user identity" and "machine identity" is the core of this security model. Confusing these two leads to the most common vulnerabilities. A ServiceAccount should never be bound to a ClusterRole with wildcard permissions unless it is a system component like the kube-proxy.
Many organizations treat RBAC as a static configuration file, which is a fundamental error. RBAC is a dynamic interface. The moment a new Role is created, the threat landscape changes. Using Kyverno to mutate and validate these changes in real-time is not optional for advanced clusters; it is the only way to maintain a "zero-trust" posture. Relying solely on manual audits of YAML files is insufficient because human error in defining apiGroups: ["*"] is inevitable.
The workflow for securing a cluster involves three distinct steps:
- Default Deny: Configure namespaces to reject all pods unless explicitly allowed.
- Identity Minimization: Ensure every ServiceAccount has
automountServiceAccountToken: falseby default and only grant specificRolebindings. - Runtime Hardening: Enforce PSS
restrictedprofiles to prevent privilege escalation within the container.
Conclusion
This approach ensures that even if an attacker compromises a container, they are confined to a sandbox with no network access to the API server and no ability to escalate privileges to the host. The mechanism is not about preventing the initial breach, but about limiting the blast radius to a single container with no identity.
For further reading on the specific API structures of ServiceAccounts and the evolution of Pod Security Standards, refer to the official Kubernetes documentation and the Kyverno policy library. The shift from "permissive" to "restrictive" defaults is the defining characteristic of modern K8s security.
Common Pitfalls
When implementing these security measures, organizations frequently stumble into specific traps:
- Over-reliance on RoleBindings without limiting scope: Creating a
Rolewith broad permissions and binding it to aServiceAccountwithout restricting thenamespacescope often leads to accidental privilege escalation if the binding is reused or misapplied. - Neglecting to disable automountServiceAccountToken in legacy namespaces: Assuming the Kubernetes 1.24+ default applies universally can leave older namespaces vulnerable if the cluster version varies or if specific workloads were not updated.
- Confusing Pod Security Standards with RBAC: Treating PSS as a substitute for RBAC is a critical error. RBAC controls API access, while PSS controls container runtime capabilities; both are required for a complete security posture.
Practical Takeaways
To effectively secure Kubernetes clusters, adopt these mental models:
- Zero Trust for Workloads: Treat every pod as potentially compromised. Assume that if a pod can talk to the API server, it should have the minimum possible permissions.
- Explicit Over Implicit: Never rely on defaults, even the "improved" defaults of newer Kubernetes versions. Explicitly set
automountServiceAccountToken: falsein your manifests. - Defense in Depth: Do not rely on a single control. Combine RBAC, admission controllers (Kyverno), and Pod Security Standards to ensure that if one layer fails, others remain intact.
FAQ
Q: Can I enable automountServiceAccountToken for specific pods?
A: Yes, you can set automountServiceAccountToken: true in the spec of a specific Pod or Deployment if the workload requires API access. However, this should be the exception, not the rule, and requires strict auditing.
Q: Does Kyverno replace the need for the Pod Security Admission controller? A: No. Kyverno provides flexible policy enforcement and mutation capabilities, while the Pod Security Admission controller is a native Kubernetes feature that enforces PSS profiles. Using both provides the strongest defense.
Q: How do I verify if a ServiceAccount has a mounted token?
A: You can inspect the Pod spec directly. If automountServiceAccountToken is not set to false, the token will be mounted by default (depending on your cluster version). Kyverno policies can automatically flag or reject pods that do not explicitly disable this.
Related posts
Identity-Driven Kubernetes Access: Beyond RBAC with Gatekeeper and Kyverno
An examination of identity-driven Kubernetes access management using OPA Gatekeeper and Kyverno for enhanced policy-as-code security.
Machine Identity Management: The Hidden Attack Surface
An examination of machine identity management covering service account security, certificate management, and API key management as a critical attack surface.
Certificate Lifecycle Automation with cert-manager and Vault PKI
Explore certificate lifecycle automation using cert-manager and Vault PKI for TLS management, rotation, and Let's Encrypt integration.