From db4eacd30c93f7ba262570455dfc89fa3b0bc8eb Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Sat, 4 Jul 2026 13:40:58 +0200 Subject: [PATCH 1/3] fix(kubescape): except C-0007 delete-capable RBAC by object, not namespace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Control C-0007 ("Roles with delete capabilities") is exempted only via controller-rbac.yaml's namespaceSelector, which cannot match RBAC findings: Kubescape keys them on the Role/ClusterRole/binding object, and 19 of C-0007's 26 findings terminate on cluster-scoped ClusterRoleBindings whose namespace is empty. So the exception is a silent no-op and every delete-capable controller renders failed. Move C-0007 to a kind+name resources match (new delete-rbac.yaml), mirroring wildcard-rbac.yaml / C-0187 — the proven mechanism for RBAC objects. Matches each of the 26 delete-capable bindings by kind+name so C-0007 still flags any new/accidental delete grant. Mirror the change in the Headlamp plugin ConfigMap (which keys on the owning SA's namespace). Depends on the scanner-mount fix (config.json postRenderer) landing first before it can take effect on the in-cluster scan. Co-Authored-By: Claude Opus 4.8 --- .../controller-rbac.yaml | 22 ++-- .../delete-rbac.yaml | 124 ++++++++++++++++++ .../kustomization.yaml | 1 + .../config-map-headlamp-exceptions.yaml | 31 ++++- 4 files changed, 166 insertions(+), 12 deletions(-) create mode 100644 k8s/bases/infrastructure/cluster-security-exceptions/delete-rbac.yaml diff --git a/k8s/bases/infrastructure/cluster-security-exceptions/controller-rbac.yaml b/k8s/bases/infrastructure/cluster-security-exceptions/controller-rbac.yaml index cdd396c50..b126a2227 100644 --- a/k8s/bases/infrastructure/cluster-security-exceptions/controller-rbac.yaml +++ b/k8s/bases/infrastructure/cluster-security-exceptions/controller-rbac.yaml @@ -25,8 +25,6 @@ spec: action: ignore - controlID: C-0015 action: ignore - - controlID: C-0007 - action: ignore - controlID: C-0037 action: ignore - controlID: C-0031 @@ -37,13 +35,19 @@ spec: action: ignore - controlID: C-0035 action: ignore - # NOTE: C-0187 (wildcard RBAC, CIS-5.1.3) is handled in wildcard-rbac.yaml, - # not here — a namespaceSelector exception does not suppress RBAC findings - # (Kubescape keys them on the Role/ClusterRole/binding object, so a namespace - # match never applies; proven: the namespaced velero-server Role kept failing - # despite `velero` being listed below). Headlamp is fixed at the root (SA - # scoped to read-only cluster-reader); Flux and Velero are exempted by-design - # in wildcard-rbac.yaml via an explicit kind+name resources match. + # NOTE: RBAC-object controls C-0187 (wildcard RBAC, CIS-5.1.3) and C-0007 + # (roles with delete capabilities) are handled in sibling files + # (wildcard-rbac.yaml, delete-rbac.yaml), NOT here — a namespaceSelector + # exception does not suppress RBAC findings. Kubescape keys them on the + # Role/ClusterRole/binding object, so a namespace match never applies + # (proven: the namespaced velero-server Role kept failing C-0187 despite + # `velero` being listed below; and 19 of C-0007's 26 findings terminate on + # cluster-scoped ClusterRoleBindings whose namespace is empty). Matching the + # specific binding objects by kind+name is the mechanism that works. The + # other RBAC controls kept here (C-0002/C-0015/C-0031/C-0035/C-0053/C-0063/ + # C-0188 and C-0037) share this limitation; migrate them the same way if they + # surface failing objects. Headlamp is fixed at the root (SA scoped to + # read-only cluster-reader). - controlID: C-0188 action: ignore match: diff --git a/k8s/bases/infrastructure/cluster-security-exceptions/delete-rbac.yaml b/k8s/bases/infrastructure/cluster-security-exceptions/delete-rbac.yaml new file mode 100644 index 000000000..f3c1a62b3 --- /dev/null +++ b/k8s/bases/infrastructure/cluster-security-exceptions/delete-rbac.yaml @@ -0,0 +1,124 @@ +--- +# Delete-capable-RBAC exception (Kubescape C-0007, "Roles with delete +# capabilities") for the infrastructure controllers and platform/app service +# accounts that require delete permissions BY DESIGN — GitOps reconcilers, +# operators, backup/restore, storage, virtualisation, cert/DB/secret managers, +# and tenant deployers all delete the resources they own. +# +# Why this is a separate CR matched by kind+name, and NOT a namespaceSelector +# entry in controller-rbac.yaml (mirrors wildcard-rbac.yaml / C-0187): Kubescape +# attaches an RBAC finding to the Role/ClusterRole/binding object, not to a +# namespaced workload, so a namespaceSelector never matches it. 19 of C-0007's +# 26 findings terminate on cluster-scoped ClusterRoleBindings whose namespace is +# empty — a namespace match can NEVER cover those. Matching each offending +# binding by kind+name is the mechanism that actually works. +# +# Deliberately explicit (one entry per binding) so C-0007 still flags any NEW or +# accidental delete-capable grant elsewhere — that residual signal is wanted. +# Two entries are known-fragile and may need a periodic refresh (documented +# inline): the Crossplane provider CRB embeds a provider-revision hash that +# changes on provider re-install, and the tofu-controller `tf-runner` RoleBinding +# is created on demand per Terraform apply. Keep this list reconciled against a +# fresh scan (it can only be re-verified live once the scanner mount fix lands — +# see the kubescape HelmRelease postRenderer / helm-charts#862). +apiVersion: kubescape.io/v1beta1 +kind: ClusterSecurityException +metadata: + name: delete-capable-rbac-by-design +spec: + reason: >- + Infrastructure controllers, operators and tenant deployers legitimately + require delete permissions by design (GitOps reconciliation, operator + lifecycle, backup/restore, storage, virtualisation, cert/DB/secret + management). Each offending Role/binding is matched by kind+name so C-0007 + still catches any new or accidental delete-capable grant. + posture: + - controlID: C-0007 + action: ignore + match: + resources: + # --- cluster-scoped: ClusterRoleBinding -> ClusterRole (19) --- + - apiGroup: rbac.authorization.k8s.io + kind: ClusterRoleBinding + name: cdi-sa # CDI (KubeVirt storage import) + - apiGroup: rbac.authorization.k8s.io + kind: ClusterRoleBinding + name: cert-manager-controller-issuers + - apiGroup: rbac.authorization.k8s.io + kind: ClusterRoleBinding + name: cert-manager-controller-clusterissuers + - apiGroup: rbac.authorization.k8s.io + kind: ClusterRoleBinding + name: cert-manager-controller-certificates + - apiGroup: rbac.authorization.k8s.io + kind: ClusterRoleBinding + name: cert-manager-controller-challenges + - apiGroup: rbac.authorization.k8s.io + kind: ClusterRoleBinding + name: cloudnative-pg # CNPG Postgres operator + - apiGroup: rbac.authorization.k8s.io + kind: ClusterRoleBinding + name: cluster-reconciler-flux-system # Flux kustomize/helm controllers + - apiGroup: rbac.authorization.k8s.io + kind: ClusterRoleBinding + name: flux-operator + - apiGroup: rbac.authorization.k8s.io + kind: ClusterRoleBinding + name: coroot-operator + - apiGroup: rbac.authorization.k8s.io + kind: ClusterRoleBinding + name: crossplane-admin # Crossplane core (Group crossplane-masters) + - apiGroup: rbac.authorization.k8s.io + kind: ClusterRoleBinding + # FRAGILE: the trailing hash is the provider revision; it changes on + # provider re-install. Refresh this name if the crossplane provider CRB + # churns (or replace with an objectSelector if the CSE schema gains one). + name: "crossplane:provider:provider-upjet-github-04d509bb9f6f:system" + - apiGroup: rbac.authorization.k8s.io + kind: ClusterRoleBinding + name: flagger # progressive delivery + - apiGroup: rbac.authorization.k8s.io + kind: ClusterRoleBinding + name: "kro:controller" # KRO resource-graph operator + - apiGroup: rbac.authorization.k8s.io + kind: ClusterRoleBinding + name: ksail-operator + - apiGroup: rbac.authorization.k8s.io + kind: ClusterRoleBinding + name: kube-prometheus-stack-operator + - apiGroup: rbac.authorization.k8s.io + kind: ClusterRoleBinding + name: kubevirt-apiserver + - apiGroup: rbac.authorization.k8s.io + kind: ClusterRoleBinding + name: kubevirt-controller + - apiGroup: rbac.authorization.k8s.io + kind: ClusterRoleBinding + name: longhorn-bind # Longhorn distributed storage + - apiGroup: rbac.authorization.k8s.io + kind: ClusterRoleBinding + name: reloader-reloader-role-binding # config/secret change reloader + # --- namespaced: RoleBinding -> Role/ClusterRole (7) --- + - apiGroup: rbac.authorization.k8s.io + kind: RoleBinding + name: ascoachingogvaner # tenant deployer SA (edit) + - apiGroup: rbac.authorization.k8s.io + kind: RoleBinding + name: "kyverno:admission-controller" + - apiGroup: rbac.authorization.k8s.io + kind: RoleBinding + name: "kyverno:cleanup-controller" + - apiGroup: rbac.authorization.k8s.io + kind: RoleBinding + name: longhorn + - apiGroup: rbac.authorization.k8s.io + kind: RoleBinding + # FRAGILE: tofu-controller creates this on demand per Terraform apply; + # it is absent between runs (the exception simply no-ops while absent). + name: tf-runner + - apiGroup: rbac.authorization.k8s.io + kind: RoleBinding + name: velero-server # backup/restore + - apiGroup: rbac.authorization.k8s.io + kind: RoleBinding + name: wedding-app # tenant deployer SA (edit) diff --git a/k8s/bases/infrastructure/cluster-security-exceptions/kustomization.yaml b/k8s/bases/infrastructure/cluster-security-exceptions/kustomization.yaml index 13479eced..44b8ec9ee 100644 --- a/k8s/bases/infrastructure/cluster-security-exceptions/kustomization.yaml +++ b/k8s/bases/infrastructure/cluster-security-exceptions/kustomization.yaml @@ -5,6 +5,7 @@ resources: - admission-controllers.yaml - cilium-network-policies.yaml - controller-rbac.yaml + - delete-rbac.yaml - health-probes.yaml - helm-chart-metadata.yaml - image-verification.yaml diff --git a/k8s/bases/infrastructure/controllers/kubescape/config-map-headlamp-exceptions.yaml b/k8s/bases/infrastructure/controllers/kubescape/config-map-headlamp-exceptions.yaml index da70b5a5d..312283ed4 100644 --- a/k8s/bases/infrastructure/controllers/kubescape/config-map-headlamp-exceptions.yaml +++ b/k8s/bases/infrastructure/controllers/kubescape/config-map-headlamp-exceptions.yaml @@ -69,9 +69,6 @@ data: { "controlID": "^C-0015$" }, - { - "controlID": "^C-0007$" - }, { "controlID": "^C-0037$" }, @@ -92,6 +89,34 @@ data: } ] }, + { + "name": "delete-capable-rbac-by-design", + "policyType": "postureExceptionPolicy", + "actions": [ + "alertOnly" + ], + "reason": "Controllers, operators and tenant deployers require delete permissions by design (C-0007). The operator CSE (delete-rbac.yaml) matches each RBAC binding by kind+name; this plugin mirror matches the owning ServiceAccount's namespace (plus the crossplane-masters Group by name, whose namespace is empty).", + "resources": [ + { + "designatorType": "Attributes", + "attributes": { + "namespace": "^(cdi|cert-manager|cnpg-system|flux-system|observability|crossplane-system|flagger-system|kro-system|ksail-operator|monitoring|kubevirt|longhorn-system|reloader|ascoachingogvaner|kyverno|unifi|velero|wedding-app)$" + } + }, + { + "designatorType": "Attributes", + "attributes": { + "kind": "^Group$", + "name": "^crossplane-masters$" + } + } + ], + "posturePolicies": [ + { + "controlID": "^C-0007$" + } + ] + }, { "name": "health-probes", "policyType": "postureExceptionPolicy", From c0248598f99eb670aa71a40e0a27e45739372720 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Sat, 4 Jul 2026 15:28:59 +0200 Subject: [PATCH 2/3] fix(kubescape): match the crossplane provider CRB by revision-hash pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exception names are regex-compared, so the provider-revision hash can be matched by pattern instead of pinned — the entry survives provider re-installs. Co-Authored-By: Claude Fable 5 --- .../delete-rbac.yaml | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/k8s/bases/infrastructure/cluster-security-exceptions/delete-rbac.yaml b/k8s/bases/infrastructure/cluster-security-exceptions/delete-rbac.yaml index f3c1a62b3..c6eff79c2 100644 --- a/k8s/bases/infrastructure/cluster-security-exceptions/delete-rbac.yaml +++ b/k8s/bases/infrastructure/cluster-security-exceptions/delete-rbac.yaml @@ -15,12 +15,12 @@ # # Deliberately explicit (one entry per binding) so C-0007 still flags any NEW or # accidental delete-capable grant elsewhere — that residual signal is wanted. -# Two entries are known-fragile and may need a periodic refresh (documented -# inline): the Crossplane provider CRB embeds a provider-revision hash that -# changes on provider re-install, and the tofu-controller `tf-runner` RoleBinding -# is created on demand per Terraform apply. Keep this list reconciled against a -# fresh scan (it can only be re-verified live once the scanner mount fix lands — -# see the kubescape HelmRelease postRenderer / helm-charts#862). +# Names are regex-compared, so the one revision-hashed entry (the Crossplane +# provider CRB) matches by pattern and survives provider re-installs; the +# tofu-controller `tf-runner` RoleBinding is created on demand per Terraform +# apply (the exception no-ops while it is absent). Keep this list reconciled +# against a fresh scan (it can only be re-verified live once the scanner mount +# fix lands — see the kubescape HelmRelease postRenderer / helm-charts#862). apiVersion: kubescape.io/v1beta1 kind: ClusterSecurityException metadata: @@ -70,10 +70,11 @@ spec: name: crossplane-admin # Crossplane core (Group crossplane-masters) - apiGroup: rbac.authorization.k8s.io kind: ClusterRoleBinding - # FRAGILE: the trailing hash is the provider revision; it changes on - # provider re-install. Refresh this name if the crossplane provider CRB - # churns (or replace with an objectSelector if the CSE schema gains one). - name: "crossplane:provider:provider-upjet-github-04d509bb9f6f:system" + # The trailing hash is the provider-revision hash, rewritten on every + # provider re-install, so match it by pattern — exception name matching + # is regex-based (kubescape/opa-utils exceptions comparator regexCompare), + # the same mechanism that lets the literal names above match. + name: "^crossplane:provider:provider-upjet-github-[0-9a-f]+:system$" - apiGroup: rbac.authorization.k8s.io kind: ClusterRoleBinding name: flagger # progressive delivery From 7b82f1ed730e98be06bd6f1010758551abbd833e Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Sat, 4 Jul 2026 15:29:13 +0200 Subject: [PATCH 3/3] fix(kubescape): mirror C-0007 by RBAC binding kind+name in the Headlamp exceptions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C-0007 findings terminate on the binding objects (19 of 26 cluster-scoped, namespace empty), so the namespace designator never covered them — mirror the CSE's kind+name matches instead, keeping the crossplane-masters Group. Co-Authored-By: Claude Fable 5 --- .../kubescape/config-map-headlamp-exceptions.yaml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/k8s/bases/infrastructure/controllers/kubescape/config-map-headlamp-exceptions.yaml b/k8s/bases/infrastructure/controllers/kubescape/config-map-headlamp-exceptions.yaml index 312283ed4..8634949d2 100644 --- a/k8s/bases/infrastructure/controllers/kubescape/config-map-headlamp-exceptions.yaml +++ b/k8s/bases/infrastructure/controllers/kubescape/config-map-headlamp-exceptions.yaml @@ -95,12 +95,20 @@ data: "actions": [ "alertOnly" ], - "reason": "Controllers, operators and tenant deployers require delete permissions by design (C-0007). The operator CSE (delete-rbac.yaml) matches each RBAC binding by kind+name; this plugin mirror matches the owning ServiceAccount's namespace (plus the crossplane-masters Group by name, whose namespace is empty).", + "reason": "Controllers, operators and tenant deployers require delete permissions by design (C-0007). Mirrors the operator CSE (delete-rbac.yaml) by kind+name: C-0007 findings terminate on the RBAC binding objects themselves (19 of 26 on cluster-scoped ClusterRoleBindings whose namespace is empty), so a namespace designator can never cover them.", "resources": [ { "designatorType": "Attributes", "attributes": { - "namespace": "^(cdi|cert-manager|cnpg-system|flux-system|observability|crossplane-system|flagger-system|kro-system|ksail-operator|monitoring|kubevirt|longhorn-system|reloader|ascoachingogvaner|kyverno|unifi|velero|wedding-app)$" + "kind": "^ClusterRoleBinding$", + "name": "^(cdi-sa|cert-manager-controller-(issuers|clusterissuers|certificates|challenges)|cloudnative-pg|cluster-reconciler-flux-system|flux-operator|coroot-operator|crossplane-admin|crossplane:provider:provider-upjet-github-[0-9a-f]+:system|flagger|kro:controller|ksail-operator|kube-prometheus-stack-operator|kubevirt-apiserver|kubevirt-controller|longhorn-bind|reloader-reloader-role-binding)$" + } + }, + { + "designatorType": "Attributes", + "attributes": { + "kind": "^RoleBinding$", + "name": "^(ascoachingogvaner|kyverno:admission-controller|kyverno:cleanup-controller|longhorn|tf-runner|velero-server|wedding-app)$" } }, {