Skip to content

WIP: Match risk rules against wildcard verbs and resources (#529) - #531

Draft
marcinc wants to merge 1 commit into
masterfrom
fix/529-match-wildcard-verbs-resources
Draft

WIP: Match risk rules against wildcard verbs and resources (#529)#531
marcinc wants to merge 1 commit into
masterfrom
fix/529-match-wildcard-verbs-resources

Conversation

@marcinc

@marcinc marcinc commented Aug 21, 2026

Copy link
Copy Markdown
Member

Fixes #529.

What is wrong

A risk rule's match rules name a resource and a verb. The generated query selected a Rule node by exact equality on both, so a role rule granting the RBAC wildcard in their place was not matched, even though it grants what the rule asks for.

This is the opposite of #80. That issue was about a match rule being too broad; this one is about it being too narrow.

On a test cluster:

  • a ClusterRole granting verbs: ['*'] on secrets was reported by risky-any-verb-secrets, but not by risky-get-secrets or risky-list-secrets
  • a ClusterRole granting get, list, delete on resources: ['*'] in the core group was reported by nothing at all, because the fix for apiGroup is not being taken into account #80 scopes the * resource rules to the * API group
  • two escalation paths were missed entirely: a role granting every verb on rolebindings plus bind on roles satisfied neither risky-create-rolebinding-role nor risky-add-rolebinding, and a role granting every verb on pods/exec was missed by risky-exec-pods

What changed

The wildcard is now accepted alongside whatever a match rule names, in the same way the API groups already are.

A resource and a verb stay one per selector, because their values are matched together and a role only matches when it grants all of them. Only the wildcard joins each of them.

A selector value is now either a single value the Rule property must equal, or a list of alternatives. The builder tells them apart by shape rather than by name, and the :api_groups key becomes :api_group so that it matches the property it tests.

nonResourceURLs are left as written. RBAC matches a URL by prefix, so * is only one of the patterns that can cover a URL, and singling it out would be arbitrary.

The grant left out

A role rule granting every verb on every resource of the * API group is excluded.

Such a role can do anything. That is not a specific risk to report against a named resource and verb, and unrestricted-cluster-wide-subjects and unrestricted-ns-level-subjects already report it in those words, for every subject it reaches. Without the exclusion a role of that shape turns up under every risky-role rule there is, 46 findings per role on the test cluster, which buries the findings a reader can act on.

A role rule that names an API group is not excluded, core included. It grants nothing outside that group, so the resources it does cover are worth naming. Excluding core as well was measured and rejected: it hid 12 accurate findings, and left core: */* reporting once while apps: */* reported 12 times. Only the choice of groups the subject templates happen to cover would justify that difference.

Testing

Against a kind cluster carrying operator, team and CI RBAC, plus the wildcard shapes from the issue:

  • 30 findings before, 73 after, none lost
  • all three cases in the issue now report as the issue expects
  • apps: */* still reports 12 findings; */*/* is kept out of the risky-role rules but still reported for its subjects
  • with RISK_RULE_QUERY_EXCLUDE_DEFAULT_ROLES=false, cluster-admin is left out and admin and edit report as before
  • overriding match_rules in custom-rules.yaml still works, including a rule matching on nonResourceURLs
  • the dashboard serves all 73 findings

286 examples pass. The new guards were checked by breaking the code on purpose: removing the exclusion fails 3 of them, dropping the verb widening fails 9, dropping the resource widening fails 10.

Worth knowing

The report gets longer. Most of that is real coverage, but a core: */* role is now reported both for its subjects and under 13 risky-role titles.

The risk rule queries take a bit longer.

A risk rule's match rules named a resource and a verb, and the generated query
selected a Rule node by equality on both, so a role rule granting the RBAC
wildcard in their place was not matched even though it grants what the rule asks
for. This is the mirror image of #80: that issue was about a match rule being too
broad, this one about it being too narrow.

Reported in issue #529. A ClusterRole granting `verbs: ['*']` on secrets was
reported by risky-any-verb-secrets but not by risky-get-secrets or
risky-list-secrets, costing precision. Worse, a ClusterRole granting
`get, list, delete` on `resources: ['*']` within the core group was reported by
nothing at all, since the fix for #80 scopes the `*` resource rules to the `*`
API group. Two escalation paths were invisible entirely: a role granting every
verb on rolebindings plus `bind` on roles satisfied neither
risky-create-rolebinding-role nor risky-add-rolebinding, and a role granting
every verb on pods/exec was missed by risky-exec-pods.

Accept the wildcard alongside whatever a match rule names, the way the API
groups already are. A resource and a verb stay one per selector, since their
values are matched together and a role only matches when it grants all of them -
only the wildcard joins each of them. Selector values are now uniformly either a
single value the Rule node property must equal or a list of alternatives, so the
builder splits them by shape rather than by name, and the `:api_groups` key is
`:api_group` to match the property it tests. `nonResourceURLs` are left as
written: RBAC matches a URL by prefix, so `*` is only one of the patterns that
can cover it, and singling it out would be arbitrary.

One grant is excluded: every verb on every resource of the `*` API group. That
role can do anything, which is not a specific risk to report against a named
resource and verb, and unrestricted-cluster-wide-subjects and
unrestricted-ns-level-subjects already report it in those words for every subject
it reaches. Without the exclusion a cluster-admin-equivalent role appears under
every risky-role rule there is - 46 findings on the test cluster, per role -
burying the findings a reader can act on under one they cannot.

A role rule naming an API group is NOT excluded, `core` included. Those two
templates additionally treat `core` as unrestricted, which is a fair reading of
what a subject can reach through it, but it does not follow that every resource
in the group goes unnamed: such a role grants nothing on apps, batch, RBAC or any
CRD, so the core resources it does cover are worth naming. Excluding `core` as
well was measured and rejected - it suppressed 12 accurate findings and left
`core: */*` reporting once while `apps: */*` reported twelve times, an asymmetry
justified only by which groups the subject templates happen to cover.

Measured against a kind cluster carrying operator, team and CI RBAC plus the
wildcard shapes from the issue: 30 findings before, 73 after, none lost. The
risk rule queries cost 89ms in total there against 43ms before, both negligible
beside ingest.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Risk rules do not match role rules granting wildcard verbs or resources

1 participant