>samit_hota
Back to research
CLOUD SECURITY

Hardening Kubernetes RBAC: Auditing and Downsizing ClusterRoles Safely

Samit Hota·
#kubernetes#cloud-security#rbac#devsecops

Most production Kubernetes clusters carry a heavy authorization debt: broad ClusterRoleBindings assigned to service accounts during initial deployment that no one ever goes back to clean up. Security teams often leave over-privileged roles untouched out of fear that restricting permissions will break critical applications in production.

Relying on generic compliance benchmarks won’t solve this. Checking off a box that says “no default service account has cluster-admin” does not protect you when custom workload service accounts hold wildcards over custom resources, or maintain subtle privilege escalation paths like impersonate, bind, or rbac.authorization.k8s.io/escalate.

Hardening Kubernetes Role-Based Access Control (RBAC) requires a systematic approach: auditing existing bindings, capturing actual API activity from logs, and rolling out downscoped roles without service disruption.

Identifying High-Risk Bindings with Native Tooling

Before touching a single YAML manifest, you must map every non-system binding across your cluster that grants permissions beyond standard namespace boundaries. Rather than installing third-party tools right away, use kubectl and jq to query the API server directly.

Execute this command to extract all ClusterRoleBindings associated with non-system ServiceAccounts:

kubectl get clusterrolebindings -o json | jq -r '
  .items[] 
  | select(.subjects[]? | select(.kind == "ServiceAccount" and (.namespace | startswith("kube-system") | not))) 
  | {
      binding: .metadata.name, 
      role: .roleRef.name, 
      subjects: [.subjects[] | "\(.namespace)/\(.name)"]
    }'

Once you have the list of bindings, audit the target ClusterRoles for dangerous verb and resource combinations. The four most common high-risk privilege patterns to search for are:

  1. Full Wildcards: verbs: ["*"] or resources: ["*"].
  2. Privilege Escalation: Verbs bind, escalate, or impersonate on rbac.authorization.k8s.io resources.
  3. Pod Execution and Proxying: Verbs create on pods/exec or pods/proxy.
  4. Credential Harvesting: Verbs get, list, or watch on secrets.

To rapidly pinpoint roles containing wildcard permissions or secret access, pipe your cluster roles through jq:

kubectl get clusterroles -o json | jq -r '
  .items[] 
  | select(.rules[]? | select(
      (.verbs[]? == "*") or 
      (.resources[]? == "*") or 
      (.resources[]? == "secrets" and (.verbs[]? | inside(["get", "list", "watch", "*"])))
    ))
  | .metadata.name'

Any custom role returned by this query attached to a workload namespace demands immediate downsizing.

Extracting Actual API Usage from Audit Logs

Never downscope an active role based on developer assumptions or documentation. Developers often forget background synchronization loops, rare controller reconciliations, or cleanup routines triggered only during pod termination.

The API server’s audit log is your source of truth. To capture meaningful activity, ensure your cluster’s AuditPolicy tracks at least Metadata level events for target namespaces.

Here is a minimal audit policy snippet tuned for capturing RBAC access:

apiVersion: audit.k8s.io/v1
kind: Policy
rules:
  - level: Metadata
    namespaces: ["prod-payments"]
    resources:
      - group: ""
        resources: ["*"]
      - group: "apps"
        resources: ["*"]

Gather audit logs over a representative operational window (typically 7 to 14 days to capture batch jobs and maintenance windows). Extract every API call executed by your target ServiceAccount using jq:

cat audit.log | jq -r '
  select(.user.username == "system:serviceaccount:prod-payments:payment-processor")
  | [.verb, (.objectRef.apiGroup // "core"), .objectRef.resource, (.objectRef.subresource // "")]
  | @tsv' | sort -u

This output provides the raw, practical boundary of what the application actually calls. For automated policy generation, you can feed these audit events into audit2rbac:

audit2rbac --filename=audit.log \
  --user=system:serviceaccount:prod-payments:payment-processor \
  --generate-namespace=prod-payments > tightened-role.yaml

Inspect the output generated by audit2rbac. Verify that it strips out administrative verbs (delete, patch) if the application only performed read-only state checks (get, list, watch).

Downscoping ClusterRoles to Namespaced Roles

The most effective risk reduction strategy is converting a global ClusterRoleBinding into a namespaced RoleBinding. If an application pod runs exclusively inside the prod-payments namespace, it rarely has a legitimate operational reason to read resources across the entire cluster.

Consider a legacy over-privileged manifest structure:

# OLD: Over-broad ClusterRole and ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: payment-processor-role
rules:
- apiGroups: [""]
  resources: ["configmaps", "secrets", "pods"]
  verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: payment-processor-binding
subjects:
- kind: ServiceAccount
  name: payment-processor
  namespace: prod-payments
roleRef:
  kind: ClusterRole
  name: payment-processor-role
  apiGroup: rbac.authorization.k8s.io

Replace this with a scoped Role and RoleBinding confined to the target namespace, explicitly enumerating verbs and dropping wildcard permissions:

# NEW: Least-privilege namespaced Role and RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: payment-processor-role
  namespace: prod-payments
rules:
- apiGroups: [""]
  resources: ["configmaps"]
  verbs: ["get", "list", "watch"]
- apiGroups: [""]
  resources: ["secrets"]
  resourceNames: ["payment-api-keys"]
  verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: payment-processor-binding
  namespace: prod-payments
subjects:
- kind: ServiceAccount
  name: payment-processor
  namespace: prod-payments
roleRef:
  kind: Role
  name: payment-processor-role
  apiGroup: rbac.authorization.k8s.io

By leveraging resourceNames, access to sensitive objects like secrets is locked down to explicit instances rather than permitting namespace-wide extraction.

Safe Validation and Zero-Downtime Migration

Applying downscoped RBAC roles directly into active production environments without testing will eventually cause an outage. Execute a multi-stage validation pattern to verify authorization coverage safely.

First, use kubectl auth can-i to evaluate permission bounds programmatically against the target ServiceAccount:

# Verify intended access is ALLOWED
kubectl auth can-i get secret/payment-api-keys \
  --as=system:serviceaccount:prod-payments:payment-processor \
  -n prod-payments

# Verify unintended access is DENIED
kubectl auth can-i delete pods \
  --as=system:serviceaccount:prod-payments:payment-processor \
  -n prod-payments

Second, deploy the new Role and RoleBinding alongside the existing ClusterRoleBinding without deleting the old permissions immediately.

Third, configure temporary logging alerts to detect authorization failures (403 Forbidden status codes) generated by the application:

cat audit.log | jq -r '
  select(.user.username == "system:serviceaccount:prod-payments:payment-processor" and .responseStatus.code == 403)
  | [.requestURI, .verb, .responseStatus.message]
  | @tsv'

Monitor your API audit stream for 403 status codes linked to your ServiceAccount for at least 48 hours post-deployment. If zero access-denied errors occur, delete the legacy ClusterRoleBinding:

kubectl delete clusterrolebinding payment-processor-binding

Removing high-risk bindings isn’t a one-time project; it is an operational process. Enforce these boundaries permanently by integrating static analysis tools like kube-linter or Open Policy Agent (OPA) Gatekeeper into your CI/CD pipelines to block raw ClusterRoleBindings from entering your git repositories in the first place.

Want a second set of eyes on your security posture?

Let's talk about where your real exposure is.

Book an advisory call