Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 34 additions & 19 deletions docs/rbac-audit.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,34 @@
# Privileged RBAC audit
# Privileged RBAC enforcement

The `audit-privileged-rbac` Kyverno ClusterPolicy reports Role and ClusterRole
The `audit-privileged-rbac` Kyverno ClusterPolicy rejects Role and ClusterRole
rules that grant wildcard administration, RBAC `bind`/`escalate`, or identity
impersonation. Wildcard verbs, resources and API groups are evaluated as effective
permissions, so renaming a chart's administrative role does not hide it.

The policy runs in **Audit** mode. It reports privileged grants while controllers
continue reconciling. `policies.kyverno.io/scored: "false"` records these staged
findings as warnings in PolicyReports. It has no chart-name, namespace-label or resource-name
exceptions. A `resourceNames` restriction is retained for review, rather than
treated as proof that a powerful grant is safe.
The policy runs in **Enforce** mode, set on its rule. A create or update that
introduces such a grant is refused at admission, whatever renders it, including
charts that Flux renders in-cluster and CI never sees. Background scanning stays on,
so violations appear in PolicyReports as `fail`. A `resourceNames` restriction is
not treated as proof that a powerful grant is safe.

Objects that legitimately need these grants are listed as **exact-identity
exclusions** in the policy: ClusterRoles by name, Roles by namespace and name. A
different object cannot borrow an exclusion by reusing a name in another namespace
or as another kind, and no exclusion keys on labels, namespace selectors or the
requesting user. The excluded identities are Kubernetes' built-in roles and the
Crossplane, KRO, KSail operator, Longhorn and Velero controllers. Adding one
means changing the policy and the pinned list in
`scripts/tests/test-audit-privileged-rbac.sh` in the same reviewed change.

The excluded names come from the charts and releases that create those roles. A
chart change that renames one of them, or adds a new privileged role, is refused at
admission. Flux dry-runs through the same webhook, so the whole Kustomization fails
until the new role is reviewed and added here.

Kyverno's own resource filters skip `kube-system`, `kube-public`,
`kube-node-lease`, `kyverno` and Kyverno's controller roles for every policy. A
privileged grant created there is not evaluated, so this policy does not cover
those namespaces.

Impersonation is checked against its actual Kubernetes resources: users, groups
and service accounts in the core API group, plus UIDs and user-extra subresources
Expand All @@ -25,15 +44,11 @@ and [RBAC privilege escalation](https://kubernetes.io/docs/reference/access-auth
The reports controller receives only `get`, `list` and `watch` on Roles and
ClusterRoles. That grant is applied in the controllers layer before the policy's
infrastructure layer. It grants no binding, escalation, impersonation or resource
mutation. Existing Kyverno resource filters still govern background scanning.

The local regression suite checks ordinary and privileged grants with the same
Kyverno version used in CI and production. A separate application check requires
the exact fixture census, so a policy that stops matching cannot pass merely by
skipping every test.

Promotion to **Enforce** remains tracked in #3486. It requires a fresh complete
holder inventory, exact owner-reviewed exceptions, regression fixtures for those
exceptions, and evidence that existing-resource reports are complete. Existing
Kubescape scanner exceptions are not admission exceptions. Production role and
binding inventories remain private operator evidence.
mutation.

The local regression suite checks ordinary grants, privileged grants and the
exclusion boundaries with the same Kyverno version used in CI and production. A
separate check requires the exact fixture census, the exact exclusion list and
the policy's complete enforcement shape. A policy that stops matching, widens an
exclusion, or weakens enforcement through another field cannot pass. Existing Kubescape scanner exceptions are not admission exceptions.
Production role and binding inventories remain private operator evidence.
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,34 @@ kind: ClusterPolicy
metadata:
name: audit-privileged-rbac
annotations:
policies.kyverno.io/title: Audit Privileged RBAC
policies.kyverno.io/title: Restrict Privileged RBAC
policies.kyverno.io/category: Security
policies.kyverno.io/severity: high
policies.kyverno.io/scored: "false"
policies.kyverno.io/subject: Role, ClusterRole
policies.kyverno.io/description: >-
Reports wildcard administration and effective bind, escalate, and
impersonate grants, including grants expressed through wildcard verbs.
Rejects Roles and ClusterRoles that grant wildcard administration or
effective bind, escalate, or impersonate permissions, including grants
expressed through wildcard verbs, unless the object is one of the
reviewed identities excluded below.
spec:
# Existing controllers require some of these grants. Audit reports resources
# eligible under the existing Kyverno filters without blocking reconciliation
# or silently exempting chart names.
# Enforce requires separately reviewed, measured exceptions (platform#3486).
validationFailureAction: Audit
# Enforced at admission (platform#3486). The name stays audit-privileged-rbac so
# its reports and references stay continuous.
#
# Every exclusion is a reviewed object that legitimately needs such a grant, and
# each is an exact identity: ClusterRoles by name, Roles by namespace AND name.
# A different object therefore cannot borrow an exclusion by reusing a name in
# another namespace or as another kind. scripts/tests/test-audit-privileged-rbac.sh
# pins this policy's whole shape and the list, so an exclusion is a reviewed change
# to that test too.
#
# The excluded names are set by the charts and releases that create them. A chart
# change that renames one of these roles, or adds a new privileged role, is refused
# at admission; because Flux dry-runs through the same webhook, that fails the
# whole Kustomization until the new role is reviewed here.
#
# Coverage limit: Kyverno's own resource filters skip kube-system, kube-public,
# kube-node-lease, kyverno, and Kyverno's controller roles for every policy, so a
# privileged grant created there is not evaluated in any mode.
background: true
rules:
- name: privileged-rules
Expand All @@ -27,7 +41,66 @@ spec:
kinds:
- rbac.authorization.k8s.io/v1/Role
- rbac.authorization.k8s.io/v1/ClusterRole
exclude:
any:
# Kubernetes built-ins. The API server bootstraps them, and the
# clusterrole aggregation controller rewrites the aggregated rules.
- resources:
kinds:
- rbac.authorization.k8s.io/v1/ClusterRole
names:
- admin
- cluster-admin
- edit
- system:aggregate-to-edit
- system:controller:clusterrole-aggregation-controller
# Crossplane core. The RBAC manager creates and binds provider roles.
- resources:
kinds:
- rbac.authorization.k8s.io/v1/ClusterRole
names:
- crossplane
- crossplane-rbac-manager
- crossplane:system:aggregate-to-crossplane
# KRO. The controller, and the platform's tenant ResourceGraphDefinition
# role, which binds the ClusterRoles each tenant is provisioned with.
- resources:
kinds:
- rbac.authorization.k8s.io/v1/ClusterRole
names:
- kro-tenant-rgd
- kro:controller
# KSail operator. Creates and binds the RBAC of the clusters it manages.
# This is the broadest grant excluded here: bind and escalate on every
# RBAC resource.
- resources:
kinds:
- rbac.authorization.k8s.io/v1/ClusterRole
names:
- ksail-operator
# Longhorn storage controller.
- resources:
kinds:
- rbac.authorization.k8s.io/v1/ClusterRole
names:
- longhorn-role
- resources:
kinds:
- rbac.authorization.k8s.io/v1/Role
namespaces:
- longhorn-system
names:
- longhorn
# Velero server. Restores arbitrary namespaced objects in its namespace.
- resources:
kinds:
- rbac.authorization.k8s.io/v1/Role
namespaces:
- velero
names:
- velero-server
validate:
failureAction: Enforce
cel:
expressions:
- expression: >-
Expand All @@ -52,4 +125,5 @@ spec:
)))
message: >-
Wildcard administration, RBAC bind/escalate, and identity
impersonation grants require review.
impersonation grants are rejected unless the object is a
reviewed exclusion in the audit-privileged-rbac policy.
85 changes: 78 additions & 7 deletions scripts/tests/test-audit-privileged-rbac.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#!/usr/bin/env bash
# A renamed/removed rule can look Excluded to kyverno test. This gate exercises
# the actual resource census and requires non-zero, exact Audit results.
# the actual resource census and requires non-zero, exact Enforce results, and it
# pins the policy's whole enforcement shape, so any edit that exempts more objects
# or enforces less fails here instead of passing unnoticed.
set -euo pipefail

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
Expand All @@ -9,8 +11,6 @@ grant="${repo_root}/k8s/bases/infrastructure/controllers/kyverno/cluster-role-re
result="$(mktemp)"
trap 'rm -f "$result"' EXIT

yq -e '.spec.validationFailureAction == "Audit" and .spec.background == true' "$policy" >/dev/null
yq -e '.metadata.annotations."policies.kyverno.io/scored" == "false"' "$policy" >/dev/null
yq -o=json "$grant" | jq -e '
.metadata.labels."rbac.kyverno.io/aggregate-to-reports-controller" == "true" and
(.rules | length) == 1 and
Expand All @@ -20,12 +20,83 @@ yq -o=json "$grant" | jq -e '
(.rules[0].resourceNames // [] | length) == 0
' >/dev/null

# EXACT SHAPE, not a blocklist. Many single-field edits weaken an Enforce policy while
# leaving its expression untouched: spec.admission false, failureActionOverrides back to
# Audit, match operations limited to CREATE, preconditions keyed on a label, a
# webhookConfiguration matchCondition, or a `scored: "false"` annotation that reports
# every rejection as a warn. Pinning the complete key set of each level rejects all of
# them, and any field Kyverno adds later, until this test is changed in review.
#
# The exclusions are exact identities: one kind per entry, at least one literal name
# (an empty list means "any name" to Kyverno), exactly one namespace for a Role, and no
# `*` or `?`, which Kyverno treats as wildcards. The expanded kind|namespace|name set
# must equal the reviewed list exactly.
expected='[
"rbac.authorization.k8s.io/v1/ClusterRole||admin",
"rbac.authorization.k8s.io/v1/ClusterRole||cluster-admin",
"rbac.authorization.k8s.io/v1/ClusterRole||crossplane",
"rbac.authorization.k8s.io/v1/ClusterRole||crossplane-rbac-manager",
"rbac.authorization.k8s.io/v1/ClusterRole||crossplane:system:aggregate-to-crossplane",
"rbac.authorization.k8s.io/v1/ClusterRole||edit",
"rbac.authorization.k8s.io/v1/ClusterRole||kro-tenant-rgd",
"rbac.authorization.k8s.io/v1/ClusterRole||kro:controller",
"rbac.authorization.k8s.io/v1/ClusterRole||ksail-operator",
"rbac.authorization.k8s.io/v1/ClusterRole||longhorn-role",
"rbac.authorization.k8s.io/v1/ClusterRole||system:aggregate-to-edit",
"rbac.authorization.k8s.io/v1/ClusterRole||system:controller:clusterrole-aggregation-controller",
"rbac.authorization.k8s.io/v1/Role|longhorn-system|longhorn",
"rbac.authorization.k8s.io/v1/Role|velero|velero-server"
]'
if ! yq -o=json '.' "$policy" | jq -e --argjson expected "$expected" '
def literal: type == "string" and length > 0 and (test("[*?]") | not);
.apiVersion == "kyverno.io/v1" and .kind == "ClusterPolicy" and
.metadata.name == "audit-privileged-rbac" and
(.metadata | keys) == ["annotations", "name"] and
(.metadata.annotations | has("policies.kyverno.io/scored") | not) and
(.spec | keys) == ["background", "rules"] and
.spec.background == true and
(.spec.rules | length) == 1 and
(.spec.rules[0] | keys) == ["exclude", "match", "name", "validate"] and
.spec.rules[0].name == "privileged-rules" and
.spec.rules[0].match == {"any": [{"resources": {"kinds": [
"rbac.authorization.k8s.io/v1/Role",
"rbac.authorization.k8s.io/v1/ClusterRole"]}}]} and
(.spec.rules[0].validate | keys) == ["cel", "failureAction"] and
.spec.rules[0].validate.failureAction == "Enforce" and
(.spec.rules[0].validate.cel | keys) == ["expressions"] and
(.spec.rules[0].validate.cel.expressions | length) == 1 and
(.spec.rules[0].validate.cel.expressions[0] | keys) == ["expression", "message"] and
(.spec.rules[0].exclude | keys) == ["any"] and
all(.spec.rules[0].exclude.any[];
keys == ["resources"] and
(.resources.kinds | length) == 1 and
(.resources.names | type) == "array" and (.resources.names | length) >= 1 and
all(.resources.names[]; literal) and
(((.resources | keys) == ["kinds", "names"] and
.resources.kinds[0] == "rbac.authorization.k8s.io/v1/ClusterRole")
or ((.resources | keys) == ["kinds", "names", "namespaces"] and
.resources.kinds[0] == "rbac.authorization.k8s.io/v1/Role" and
(.resources.namespaces | length) == 1 and
(.resources.namespaces[0] | literal)))) and
([.spec.rules[0].exclude.any[].resources | .kinds[0] as $kind
| (.namespaces // [""])[] as $ns | .names[] | "\($kind)|\($ns)|\(.)"] | sort)
== ($expected | sort)
' >/dev/null; then
echo 'RBAC policy differs from the reviewed Enforce shape or exact-identity exclusion list' >&2
exit 1
fi

# kyverno apply exits 1 when an Enforce rule fails, which the fixtures do on purpose;
# anything higher is a real error. The census is the assertion: excluded objects are
# not evaluated, so they appear in no count. `fail: 24` proves both exclusion fixtures
# really were excluded (it would be 26 otherwise) while their lookalikes were not.
apply_status=0
kyverno apply "$policy" \
--resource "${repo_root}/tests/audit-privileged-rbac/resources.yaml" \
--audit-warn --warn-exit-code 0 >"$result" 2>&1
if ! grep -Fq 'pass: 15, fail: 0, warn: 21, error: 0, skip: 0' "$result"; then
>"$result" 2>&1 || apply_status=$?
if [ "$apply_status" -gt 1 ] || ! grep -Fq 'pass: 15, fail: 24, warn: 0, error: 0, skip: 0' "$result"; then
cat "$result"
echo 'RBAC Audit fixture census did not match the expected results' >&2
echo 'RBAC Enforce fixture census did not match the expected results' >&2
exit 1
fi
echo 'RBAC Audit: 15 ordinary or inert grants pass, 21 privileged grants warn; reporting access is read-only.'
echo 'RBAC Enforce: 15 ordinary or inert grants pass, 24 privileged grants fail, 2 reviewed exclusions are not evaluated; reporting access is read-only.'
27 changes: 25 additions & 2 deletions tests/audit-privileged-rbac/kyverno-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ results:
- policy: audit-privileged-rbac
rule: privileged-rules
kind: ClusterRole
result: warn
result: fail
resources:
- triple-wildcard
- wildcard-with-extra-values
Expand All @@ -46,6 +46,20 @@ results:
- impersonate-auth-wildcard-verbs
- impersonate-auth-wildcard-resources
- resource-name-constrained
# A name that only extends an excluded name is not excluded.
- crossplane-rbac-manager-lookalike
# A Role exclusion never exempts a ClusterRole of the same name.
- velero-server
# An excluded ClusterRole is skipped by name, whatever it grants. kyverno test also
# reports a rule that never matched as skip, so these rows alone cannot tell
# "excluded" from "not evaluated": the exact census in
# scripts/tests/test-audit-privileged-rbac.sh is what proves the exclusion applies.
- policy: audit-privileged-rbac
rule: privileged-rules
kind: ClusterRole
result: skip
resources:
- crossplane-rbac-manager
- policy: audit-privileged-rbac
rule: privileged-rules
kind: Role
Expand All @@ -59,8 +73,17 @@ results:
- policy: audit-privileged-rbac
rule: privileged-rules
kind: Role
result: warn
result: fail
resources:
- sample/namespaced-wildcard
- sample/impersonate-serviceaccounts
- sample/namespaced-clusterrole-bind
# An excluded Role name in another namespace is not excluded.
- sample/velero-server
# An excluded Role is skipped only in its own namespace.
- policy: audit-privileged-rbac
rule: privileged-rules
kind: Role
result: skip
resources:
- velero/velero-server
47 changes: 47 additions & 0 deletions tests/audit-privileged-rbac/resources.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -324,3 +324,50 @@ rules:
- apiGroups: ["rbac.authorization.k8s.io"]
resources: ["clusterroles"]
verbs: ["bind"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: crossplane-rbac-manager
rules:
- apiGroups: ["rbac.authorization.k8s.io"]
resources: ["clusterroles"]
verbs: ["bind", "escalate"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: crossplane-rbac-manager-lookalike
rules:
- apiGroups: ["rbac.authorization.k8s.io"]
resources: ["clusterroles"]
verbs: ["bind"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: velero-server
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: velero-server
namespace: velero
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: velero-server
namespace: sample
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["*"]
Loading