Skip to content
Chimera readability score 76 out of 100, Expert reading level.

If you run kube-linter

on your Kubernetes manifests, you probably feel pretty good about your role-based access control (RBAC) setup. It catches wildcard verbs, cluster-admin

bindings, and excessive Secret access. But there is a class of vulnerabilities it fundamentally cannot detect: indirect privilege escalation through binding chains.

This post walks through the problem, shows how an attacker exploits it, and introduces kube-chainsaw, a tool that catches what per-object linters miss by building permission graphs from static manifests.

The blind spot: Per-object linting

kube-linter processes each Kubernetes resource independently. When it encounters a cluster role, it checks the rules for wildcards and dangerous verbs. When it encounters a cluster role binding, it checks whether it references cluster-admin

. These are useful checks. But they operate in isolation.

Consider this RBAC setup for a typical Kubernetes operator:

apiVersion: rbac.authorization.k8s.io/v1

kind: ClusterRole

metadata:

name: operator-manager-role

rules:

  • apiGroups: [""]

resources: ["secrets"]

verbs: ["get", "list", "watch", "create", "update"]

  • apiGroups: ["rbac.authorization.k8s.io"]

resources: ["clusterrolebindings", "clusterroles"]

verbs: ["create", "patch", "update"]


apiVersion: rbac.authorization.k8s.io/v1

kind: ClusterRoleBinding

metadata:

name: operator-binding

roleRef:

apiGroup: rbac.authorization.k8s.io

kind: ClusterRole

name: operator-manager-role

subjects:

  • kind: ServiceAccount

name: operator-sa

namespace: operator-system


apiVersion: apps/v1

kind: Deployment

metadata:

name: operator-controller

namespace: operator-system

spec:

selector:

matchLabels:

app: operator

template:

metadata:

labels:

app: operator

spec:

serviceAccountName: operator-sa

containers:

  • name: manager

image: operator:latest

No wildcards. No cluster-admin

. kube-linter

reports nothing alarming.

But trace the chain manually: the deployment runs with operator-sa

. That service account is bound cluster-wide to operator-manager-role

via a cluster role binding. And that cluster role can create and patch cluster role bindings. In other words, the operator can grant itself, or any other service account, cluster-admin

privileges at runtime. It also has read/write access to every Secret in the cluster.

Figure 1: RBAC privilege escalation chain.

kube-linter

cannot see this because it never builds the graph. It checks each resource against a set of rules, but it does not connect deployment to service account to binding to role to permissions.

How an attacker exploits this

Here is the scary part: exploiting this does not require any special tools. If an attacker compromises the operator pod (through a container escape, a dependency vulnerability, or a supply chain attack), they inherit the ServiceAccount

token automatically. From there:

Verify permissions:

kubectl auth can-i create clusterrolebindings # yes

Grant

cluster-admin

:kubectl create clusterrolebinding pwned-admin \ --clusterrole=cluster-admin \ --serviceaccount=operator-system:operator-sa

Full cluster access:

kubectl get secrets --all-namespaces -o json

Three commands. No privilege escalation exploit needed, the RBAC configuration already grants the permissions. The operator was one cluster role binding away from full cluster-admin

privileges, and it had the ability to create that binding itself.

kube-chainsaw

catches this exact pattern by tracing the chain from the deployment through its service account to the cluster role binding, and flags it with a CRITICAL

severity rating.

This pattern is common in controller-runtime operators. The controller needs create or patch permissions on roles and role bindings for legitimate reconciliation, but the manifests grant it on cluster role bindings as well, often because the role definition was copy-pasted from a broader template.

The fix: Graph traversal on static manifests

The core problem is that detecting privilege escalation requires cross-object analysis. A tool needs to see the deployment, look up its service account, find all bindings that reference that service account, resolve the roles those bindings point to, and then evaluate whether the resulting permission set enables escalation.

As shown in Figure 2, kube-chainsaw

follows this exact approach. Instead of checking each resource in isolation, the tool performs the following actions:

  • Parses all YAML manifests from a directory: cluster roles, roles, bindings, service accounts, pods, deployments, daemon sets, stateful sets, jobs, and cronjobs.
  • Builds a directed graph: workload to service account to binding to role to verbs or resources.
  • Traverses the graph to detect 15 categories of privilege escalation and misconfiguration.
  • Adjusts severity based on binding scope: cluster-wide bindings are

HIGH

orCRITICAL

, namespace-scoped bindings areWARNING

, unbound roles areINFO

.

Figure 2: kube-chainsaw analysis pipeline.

The severity model is where graph analysis really pays off. The same cluster role produces different severity levels depending on how it is bound:

| Binding type | Severity | Rationale |

|---|---|---|

ClusterRoleBinding (cluster-wide) with wildcards | CRITICAL | Full cluster access |

ClusterRoleBinding (cluster-wide) without wildcards | HIGH | Broad but scoped access |

RoleBinding (namespace-scoped) with wildcards | HIGH | Namespace-wide access |

RoleBinding (namespace-scoped) without wildcards | WARNING | Limited blast radius |

| No binding (unbound) | INFO | Dormant template |

No per-object linter can make this distinction. It requires correlating the role with its bindings.

Hands-on walkthrough

Step 1: Install

Install the tool:

go install github.com/ugiordan/kube-chainsaw/cmd/kube-chainsaw@latest

Or download a binary from the GitHub releases page. Docker and GitHub Action are also available.

Step 2: Scan your manifests

Point kube-chainsaw

at a directory containing Kubernetes YAML files:

$ kube-chainsaw config/

Output:

=== HIGH ===

[KC-006] Secrets access

File: config/rbac/role.yaml

Resource: ClusterRole/operator-manager-role

Description: Role "operator-manager-role" grants access to

dangerous resource "secrets"

Remediation: Restrict Secrets access to specific namespaces

and only the verbs needed

[KC-010] RBAC modification capability

File: config/rbac/role.yaml

Resource: ClusterRole/operator-manager-role

Description: Role "operator-manager-role" grants access to

dangerous resource "clusterrolebindings"

Remediation: Limit RBAC modification to dedicated admin Roles

with proper audit

[KC-011] Privilege escalation via Role/Binding modification

File: config/rbac/role.yaml

Resource: ClusterRole/operator-manager-role

Description: Role "operator-manager-role" can create/modify

Roles or Bindings

Remediation: Restrict ability to create/modify Roles and

Bindings to admin users only

Total: 3 findings [3 HIGH]

Each finding in the output includes a rule identifier (like KC-006 or KC-011), a human-readable title, the file and resource that triggered it, a description of the issue, and a remediation suggestion. The full list of all 15 rules with YAML examples is in the detection rules reference.

Compare this with kube-linter

on the same manifests:

$ kube-linter lint config/

Only flags: "binding to cluster role that has

[create get list patch update watch] access to [secrets]"

Misses: RBAC modification, privilege escalation via

Bindings, workload creation risk

Step 3: Generate SARIF for GitHub code scanning

Run the following command to scan your directory and save the results as a Static Analysis Results Interchange Format (SARIF) file:

$ kube-chainsaw config/ --output results.sarif

This writes a SARIF 2.1.0 file that integrates directly with GitHub code scanning, GitLab SAST, or any SARIF-compatible security platform. The console report still prints to stdout

for human review.

Step 4: Suppress accepted risks

Not every finding requires a fix. If the operator genuinely needs cluster-wide Secret access (for example, to manage TLS certificates), create a suppression file:

suppressions.yaml

suppressions:

  • rule_id: KC-006

resource_name: operator-manager-role

reason: "Operator manages TLS certificates stored as Secrets"

Execute the scan using the --suppressions

flag to apply your baseline exclusions:

$ kube-chainsaw config/ --suppressions suppressions.yaml

Suppressed findings still appear in the output (marked as suppressed) for an audit trail, but they do not affect the exit code. This is important for CI pipelines: the scan passes even when known risks exist, as long as they are documented.

Step 5: Continuous integration setup

Add kube-chainsaw

to a GitHub Actions workflow:

  • uses: ugiordan/kube-chainsaw@v1

with:

paths: config/ deploy/

fail-on: HIGH

format: sarif

output: results.sarif

  • uses: github/codeql-action/upload-sarif@v3

with:

sarif_file: results.sarif

The workflow fails if any unsuppressed finding at HIGH

or above is detected. SARIF results appear in the Security tab of the repository.

What gets detected

The kube-chainsaw

tool runs 15 detection rules organized in three categories. The source code for all rules is in rules.go and analyzer.go.

The tool scans for risky permissions defined on separate cluster roles and roles, including:

  • Wildcard permissions:

verbs: ["*"]

orresources: ["*"]

that grant unrestricted access. - Dangerous verbs:

escalate

(self-promote RBAC privileges),impersonate

(assume another identity),bind

(attach roles without restriction). - Sensitive resource access: Secrets (credential theft),

pods/exec

andpods/attach

(container hijacking), nodes (node-level compromise), persistent volumes (storage access), cluster roles and cluster role bindings (RBAC modification). - Escalation combinations:

create

,patch

, orupdate

permissions on roles, cluster roles, role bindings, or cluster role bindings within the same rule (enables self-granting privileges), orcreate

permissions on pods (enables deploying privileged workloads).

These checks are apiGroup

-aware, so a custom resource definition (CRD) named secrets

in the custom.example.com

group does not trigger a false positive.

Workload-to-cluster-admin privilege chains

These rules trace the full permission path from a workload (pod, deployment, or job) through its service account and bindings to the referenced role.

One rule flags workloads whose service account is bound to cluster-admin

, the most privileged cluster role in Kubernetes. This is the pattern demonstrated in the attack scenario earlier in this post.

Another rule flags role bindings that reference a cluster role instead of a namespace-scoped role. This is a common misconfiguration where a developer creates a role binding intending namespace-level access but references a cluster role, inadvertently granting the service account unintended permissions.

Aggregated ClusterRole detection

The tool detects aggregated cluster roles, which use label selectors to dynamically combine permissions from other cluster roles. Because the effective permissions depend on which cluster roles match the selector at runtime, you cannot fully determine them from static manifests alone.

Tips and best practices

Always scope cluster roles to the minimum required resources and verbs. If the operator only needs to read Secrets in its own namespace, use a local role and role binding instead of a cluster-wide cluster role and cluster role binding.

Never grant create

, patch

, or update

verbs on cluster role bindings unless absolutely required. This is the most dangerous permission an operator can have because it enables self-escalation to cluster-admin

.

Use role bindings instead of cluster role bindings when possible. A role binding referencing a cluster role limits the scope to a single namespace, significantly reducing the blast radius.

Run kube-chainsaw

in your continuous integration (CI) pipeline alongside kube-linter

. The tools are complementary. While kube-linter

covers CIS benchmark checks, resource limits, SecurityContext

configurations, and container best practices, kube-chainsaw

focuses on RBAC privilege chain analysis. Together, they provide broad static analysis coverage for your Kubernetes manifests.

Suppress findings with documented reasons. Every suppression should include a reason

field explaining why the risk is accepted. This creates an audit trail and forces a conscious decision about each exception.

Comparison

The following table compares kube-chainsaw

with other common Kubernetes RBAC analysis tools:

| Tool | Static analysis | Graph traversal | Privilege chains | Workload analysis |

|---|---|---|---|---|

kube-chainsaw | Yes | Yes | Yes | Yes |

kube-linter | Yes | No | No | No |

KubiScan | No (live cluster) | Yes | Yes | No |

rbac-tool | No (live cluster) | Yes | No | No |

kubectl-who-can | No (live cluster) | Yes | No | No |

Among the surveyed tools, kube-chainsaw

is the only option that performs static graph traversal on YAML manifests to detect privilege escalation chains before deployment, requiring no live cluster.

Wrap up

Per-object RBAC linting catches the obvious problems: wildcards, cluster-admin

bindings, missing SecurityContext

configurations. But the subtle privilege escalation paths—the ones that chain through service accounts, bindings, and roles across multiple manifests—require graph-level analysis.

kube-chainsaw

fills that gap. It runs on static manifests, integrates with CI pipelines, produces SARIF data for security platforms, and supports suppression files for accepted risks. Try it on your operator manifests. The results might surprise you.

Learn more:

  • kube-chainsaw documentation
  • kube-chainsaw on GitHub
  • Detection rules reference
  • kube-linter on GitHub for complementary Kubernetes manifest linting