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:latestNo 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 # yesGrant
cluster-admin:kubectl create clusterrolebinding pwned-admin \ --clusterrole=cluster-admin \ --serviceaccount=operator-system:operator-saFull 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
HIGHorCRITICAL, 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@latestOr 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 riskStep 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.sarifThis 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.yamlSuppressed 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.sarifThe 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/execandpods/attach(container hijacking), nodes (node-level compromise), persistent volumes (storage access), cluster roles and cluster role bindings (RBAC modification). - Escalation combinations:
create,patch, orupdatepermissions on roles, cluster roles, role bindings, or cluster role bindings within the same rule (enables self-granting privileges), orcreatepermissions 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