CVE-2025-24376
Description
kubewarden-controller is a Kubernetes controller that allows you to dynamically register Kubewarden admission policies. By design, AdmissionPolicy and AdmissionPolicyGroup can evaluate only namespaced resources. The resources to be evaluated are determined by the rules provided by the user when defining the policy. There might be Kubernetes namespaced resources that should not be validated by AdmissionPolicy and by the AdmissionPolicyGroup policies because of their sensitive nature. For example, PolicyReport are namespaced resources that contain the list of non compliant objects found inside of a namespace. An attacker can use either an AdmissionPolicy or an AdmissionPolicyGroup to prevent the creation and update of PolicyReport objects to hide non-compliant resources. Moreover, the same attacker might use a mutating AdmissionPolicy to alter the contents of the PolicyReport created inside of the namespace. Starting from the 1.21.0 release, the validation rules applied to AdmissionPolicy and AdmissionPolicyGroup have been tightened to prevent them from validating sensitive types of namespaced resources.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/kubewarden/kubewarden-controllerGo | >= 1.7.0, < 1.21.0 | 1.21.0 |
Patches
22548ba11bc178124039b5f0cfix: stricter checks on AdmissionPolicy and AdmissionPolicyGroup rules
3 files changed · +300 −18
api/policies/v1/policy_validation.go+94 −0 modified@@ -40,6 +40,41 @@ var ( const maxMatchConditionsCount = 64 +type sensitiveResource struct { + APIGroup string + Resource string +} + +func (sr sensitiveResource) String() string { + return fmt.Sprintf("APIGroup: %s, Resource: %s", sr.APIGroup, sr.Resource) +} + +func (sr sensitiveResource) MatchesRules(apiGroups []string, resource []string) bool { + apiGroupMatches := false + for _, apiGroup := range apiGroups { + if apiGroup == sr.APIGroup || apiGroup == "*" { + apiGroupMatches = true + break + } + } + + resourceMatches := false + for _, res := range resource { + if res == sr.Resource || res == "*" || res == "*/*" || strings.HasPrefix(res, sr.Resource+"/") { + resourceMatches = true + break + } + } + + return apiGroupMatches && resourceMatches +} + +func defaultSensitiveResources() []sensitiveResource { + return []sensitiveResource{ + {APIGroup: "wgpolicyk8s.io", Resource: "policyreports"}, + } +} + func validatePolicyCreate(policy Policy) field.ErrorList { var allErrors field.ErrorList @@ -74,6 +109,9 @@ func validateRulesField(policy Policy) field.ErrorList { return allErrors } + _, isAdmissionPolicy := policy.(*AdmissionPolicy) + _, isAdmissionPolicyGroup := policy.(*AdmissionPolicyGroup) + for _, rule := range policy.GetRules() { switch { case len(rule.Operations) == 0: @@ -85,6 +123,11 @@ func validateRulesField(policy Policy) field.ErrorList { allErrors = append(allErrors, checkOperationsArrayForEmptyString(rule.Operations, rulesField)...) allErrors = append(allErrors, checkRulesArrayForEmptyString(rule.Rule.APIVersions, rulesField.Child("rule.apiVersions"))...) allErrors = append(allErrors, checkRulesArrayForEmptyString(rule.Rule.Resources, rulesField.Child("rule.resources"))...) + + if isAdmissionPolicy || isAdmissionPolicyGroup { + allErrors = append(allErrors, checkRulesArrayForWildcardUsage(rule.Rule.APIVersions, rule.Rule.Resources, rulesField)...) + allErrors = append(allErrors, checkRulesArrayForSensitiveResourcesBeingTargeted(rule.Rule.APIVersions, rule.Rule.Resources, rulesField)...) + } } } @@ -119,6 +162,57 @@ func checkRulesArrayForEmptyString(rulesArray []string, rulesField *field.Path) return allErrors } +// checkRulesArrayForWildcardUsage checks if the rules array contains a wildcard and returns an error if both the apiGroups +// and resources contain wildcards. +func checkRulesArrayForWildcardUsage(rulesAPIGroups []string, rulesResources []string, rulesField *field.Path) field.ErrorList { + var allErrors field.ErrorList + + apiGroupHasWildcard := false + apiGroupWildcardIndex := -1 + + resourceHasWildcard := false + resourceWildcardIndex := -1 + + for i, apiGroup := range rulesAPIGroups { + if apiGroup == "*" { + apiGroupHasWildcard = true + apiGroupWildcardIndex = i + break + } + } + + for i, resource := range rulesResources { + if resource == "*" || resource == "*/*" { + resourceHasWildcard = true + resourceWildcardIndex = i + break + } + } + + if apiGroupHasWildcard && resourceHasWildcard { + allErrors = append(allErrors, field.Forbidden(rulesField.Child("apiGroups").Index(apiGroupWildcardIndex), "apiGroups cannot use wildcards when using AdmissionPolicy or AdmissionPolicyGroup")) + allErrors = append(allErrors, field.Forbidden(rulesField.Child("resources").Index(resourceWildcardIndex), "resources cannot use wildcards when using AdmissionPolicy or AdmissionPolicyGroup")) + } + + return allErrors +} + +// checkRulesArrayForSensitiveResourcesBeingTargeted checks if any of the sensitive resources are being targeted by the +// rule. +func checkRulesArrayForSensitiveResourcesBeingTargeted(rulesAPIGroups []string, rulesResources []string, rulesField *field.Path) field.ErrorList { + var allErrors field.ErrorList + + sensitiveResources := defaultSensitiveResources() + + for _, sensitiveResource := range sensitiveResources { + if sensitiveResource.MatchesRules(rulesAPIGroups, rulesResources) { + allErrors = append(allErrors, field.Forbidden(rulesField, fmt.Sprintf("{%s} resources cannot be targeted by AdmissionPolicy or AdmissionPolicyGroup", sensitiveResource))) + } + } + + return allErrors +} + func validatePolicyServerField(oldPolicy, newPolicy Policy) *field.Error { if oldPolicy.GetPolicyServer() != newPolicy.GetPolicyServer() { return field.Forbidden(field.NewPath("spec").Child("policyServer"), "the field is immutable")
api/policies/v1/policy_validation_test.go+180 −18 modified@@ -23,11 +23,86 @@ import ( "k8s.io/apimachinery/pkg/util/validation/field" ) +func TestSensitiveResourceMatchRule(t *testing.T) { + sr := sensitiveResource{ + APIGroup: "apps", + Resource: "deployments", + } + + tests := []struct { + name string + apiGroups []string + resources []string + matches bool + }{ + { + "with matching APIGroups and Resources", + []string{"apps"}, + []string{"statefulsets", "deployments"}, + true, + }, + { + "with APIGroups using wildcard and matching Resources", + []string{"*"}, + []string{"deployments"}, + true, + }, + { + "with Resources using wildcards and APIGroups matching", + []string{"apps"}, + []string{"*"}, + true, + }, + { + "with Resources using double wildcards and APIGroups matching", + []string{"apps"}, + []string{"*/*"}, + true, + }, + { + "with sub-Resources using wildcards and APIGroups matching", + []string{"apps"}, + []string{"deployments/*"}, + true, + }, + { + "with sub-Resources and APIGroups matching", + []string{"apps"}, + []string{"deployments/status"}, + true, + }, + { + "with only APIGroups matching", + []string{"apps"}, + []string{"statefulsets"}, + false, + }, + { + "with APIGroups not matching and a Resopurce using wildcard", + []string{""}, + []string{"*"}, + false, + }, + { + "with APIGroups not matching and a Resopurce matching", + []string{"argoproj.io"}, + []string{"deployments"}, + false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + require.Equal(t, test.matches, sr.MatchesRules(test.apiGroups, test.resources)) + }) + } +} + func TestValidateRulesField(t *testing.T) { tests := []struct { - name string - policy Policy - expectedErrorMessage string // use empty string when no error is expected + name string + policy Policy + expectedErrorMessages []string // use nil when no error is expected }{ { "with valid APIVersion and resources. But with empty APIGroup", @@ -43,7 +118,7 @@ func TestValidateRulesField(t *testing.T) { }, }). WithPolicyServer("default").Build(), - "", + nil, }, { "with valid APIVersion, Resources and APIGroup", @@ -59,21 +134,21 @@ func TestValidateRulesField(t *testing.T) { }, }). WithPolicyServer("default").Build(), - "", + nil, }, { "with no operations and API groups and resources", NewClusterAdmissionPolicyFactory(). WithRules([]admissionregistrationv1.RuleWithOperations{}). WithPolicyServer("default").Build(), - "spec.rules: Required value: a value must be specified", + []string{"spec.rules: Required value: a value must be specified"}, }, { "with empty objects", NewClusterAdmissionPolicyFactory(). WithRules([]admissionregistrationv1.RuleWithOperations{{}}). WithPolicyServer("default").Build(), - "spec.rules.operations: Required value: a value must be specified", + []string{"spec.rules.operations: Required value: a value must be specified"}, }, { "with no operations", @@ -89,7 +164,7 @@ func TestValidateRulesField(t *testing.T) { }, }). WithPolicyServer("default").Build(), - "spec.rules.operations: Required value: a value must be specified", + []string{"spec.rules.operations: Required value: a value must be specified"}, }, { "with null operations", @@ -103,7 +178,7 @@ func TestValidateRulesField(t *testing.T) { }, }}). WithPolicyServer("default").Build(), - "spec.rules.operations: Required value: a value must be specified", + []string{"spec.rules.operations: Required value: a value must be specified"}, }, { "with empty operations string", @@ -117,7 +192,7 @@ func TestValidateRulesField(t *testing.T) { }, }}). WithPolicyServer("default").Build(), - "spec.rules.operations[0]: Required value: must be non-empty", + []string{"spec.rules.operations[0]: Required value: must be non-empty"}, }, { "with no apiVersion", @@ -131,7 +206,7 @@ func TestValidateRulesField(t *testing.T) { }, }}). WithPolicyServer("default").Build(), - "spec.rules: Required value: apiVersions and resources must have specified values", + []string{"spec.rules: Required value: apiVersions and resources must have specified values"}, }, { "with no resources", @@ -144,7 +219,7 @@ func TestValidateRulesField(t *testing.T) { Resources: []string{}, }, }}).WithPolicyServer("default").Build(), - "spec.rules: Required value: apiVersions and resources must have specified values", + []string{"spec.rules: Required value: apiVersions and resources must have specified values"}, }, { "with empty apiVersion string", @@ -157,7 +232,7 @@ func TestValidateRulesField(t *testing.T) { Resources: []string{"*/*"}, }, }}).WithPolicyServer("defaule").Build(), - "spec.rules.rule.apiVersions[0]: Required value: must be non-empty", + []string{"spec.rules.rule.apiVersions[0]: Required value: must be non-empty"}, }, { "with empty resources string", @@ -170,7 +245,7 @@ func TestValidateRulesField(t *testing.T) { Resources: []string{""}, }, }}).WithPolicyServer("default").Build(), - "spec.rules.rule.resources[0]: Required value: must be non-empty", + []string{"spec.rules.rule.resources[0]: Required value: must be non-empty"}, }, { "with some of the resources are empty strings", @@ -183,7 +258,7 @@ func TestValidateRulesField(t *testing.T) { Resources: []string{"", "pods"}, }, }}).WithPolicyServer("default").Build(), - "spec.rules.rule.resources[0]: Required value: must be non-empty", + []string{"spec.rules.rule.resources[0]: Required value: must be non-empty"}, }, { "with all operations and API groups and resources", @@ -198,17 +273,104 @@ func TestValidateRulesField(t *testing.T) { }, }, }).Build(), - "", + nil, + }, + { + "with wildcard usage. But an AdmissionPolicy", + NewAdmissionPolicyFactory(). + WithRules([]admissionregistrationv1.RuleWithOperations{ + { + Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll}, + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{"*"}, + APIVersions: []string{"*"}, + Resources: []string{"*/*"}, + }, + }, + }).Build(), + []string{ + "spec.rules.apiGroups[0]: Forbidden: apiGroups cannot use wildcards when using AdmissionPolicy or AdmissionPolicyGroup", + "spec.rules.resources[0]: Forbidden: resources cannot use wildcards when using AdmissionPolicy or AdmissionPolicyGroup", + }, + }, + { + "with wildcard usage. But an AdmissionPolicyGroup", + NewAdmissionPolicyGroupFactory(). + WithRules([]admissionregistrationv1.RuleWithOperations{ + { + Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll}, + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{"*"}, + APIVersions: []string{"*"}, + Resources: []string{"*"}, + }, + }, + }).Build(), + []string{ + "spec.rules.apiGroups[0]: Forbidden: apiGroups cannot use wildcards when using AdmissionPolicy or AdmissionPolicyGroup", + "spec.rules.resources[0]: Forbidden: resources cannot use wildcards when using AdmissionPolicy or AdmissionPolicyGroup", + }, + }, + { + "targeting a PolicyReport. But a ClusterAdmissionPolicy", + NewClusterAdmissionPolicyFactory(). + WithRules([]admissionregistrationv1.RuleWithOperations{ + { + Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll}, + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{"wgpolicyk8s.io"}, + APIVersions: []string{"*"}, + Resources: []string{"policyreports"}, + }, + }, + }).Build(), + nil, + }, + { + "targeting a PolicyReport. But an AdmissionPolicy", + NewAdmissionPolicyFactory(). + WithRules([]admissionregistrationv1.RuleWithOperations{ + { + Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll}, + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{"wgpolicyk8s.io"}, + APIVersions: []string{"*"}, + Resources: []string{"policyreports"}, + }, + }, + }).Build(), + []string{ + "spec.rules: Forbidden: {APIGroup: wgpolicyk8s.io, Resource: policyreports} resources cannot be targeted by AdmissionPolicy or AdmissionPolicyGroup", + }, + }, + { + "targeting a wgpolicyk8s.io resources. But an AdmissionPolicyGroup", + NewAdmissionPolicyGroupFactory(). + WithRules([]admissionregistrationv1.RuleWithOperations{ + { + Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll}, + Rule: admissionregistrationv1.Rule{ + APIGroups: []string{"wgpolicyk8s.io"}, + APIVersions: []string{"*"}, + Resources: []string{"*"}, + }, + }, + }).Build(), + []string{ + "spec.rules: Forbidden: {APIGroup: wgpolicyk8s.io, Resource: policyreports} resources cannot be targeted by AdmissionPolicy or AdmissionPolicyGroup", + }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { allErrors := validateRulesField(test.policy) - if test.expectedErrorMessage != "" { + if len(test.expectedErrorMessages) != 0 { err := prepareInvalidAPIError(test.policy, allErrors) - require.ErrorContains(t, err, test.expectedErrorMessage) + for _, expectedErrorMessage := range test.expectedErrorMessages { + require.ErrorContains(t, err, expectedErrorMessage) + } } else { require.Empty(t, allErrors) }
config/crd/bases/policies.kubewarden.io_policyservers.yaml+26 −0 modified@@ -1438,6 +1438,32 @@ spec: Note that this field cannot be set when spec.os.name is windows. format: int64 type: integer + seLinuxChangePolicy: + description: |- + seLinuxChangePolicy defines how the container's SELinux label is applied to all volumes used by the Pod. + It has no effect on nodes that do not support SELinux or to volumes does not support SELinux. + Valid values are "MountOption" and "Recursive". + + "Recursive" means relabeling of all files on all Pod volumes by the container runtime. + This may be slow for large volumes, but allows mixing privileged and unprivileged Pods sharing the same volume on the same node. + + "MountOption" mounts all eligible Pod volumes with `-o context` mount option. + This requires all Pods that share the same volume to use the same SELinux label. + It is not possible to share the same volume among privileged and unprivileged Pods. + Eligible volumes are in-tree FibreChannel and iSCSI volumes, and all CSI volumes + whose CSI driver announces SELinux support by setting spec.seLinuxMount: true in their + CSIDriver instance. Other volumes are always re-labelled recursively. + "MountOption" value is allowed only when SELinuxMount feature gate is enabled. + + If not specified and SELinuxMount feature gate is enabled, "MountOption" is used. + If not specified and SELinuxMount feature gate is disabled, "MountOption" is used for ReadWriteOncePod volumes + and "Recursive" for all other volumes. + + This field affects only Pods that have SELinux label set, either in PodSecurityContext or in SecurityContext of all containers. + + All Pods that use the same volume should use the same seLinuxChangePolicy, otherwise some pods can get stuck in ContainerCreating state. + Note that this field cannot be set when spec.os.name is windows. + type: string seLinuxOptions: description: |- The SELinux context to be applied to all containers.
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-fc89-jghx-8pvgghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-24376ghsaADVISORY
- github.com/kubewarden/kubewarden-controller/commit/8124039b5f0c955d0ee8c8ca12d4415282f02d2cnvdWEB
- github.com/kubewarden/kubewarden-controller/security/advisories/GHSA-fc89-jghx-8pvgnvdWEB
- pkg.go.dev/vuln/GO-2025-3434ghsaWEB
News mentions
0No linked articles in our index yet.