From dbb8d7d14c11c0effc496cfdad753924cbcdb026 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Wed, 17 Jun 2026 04:28:17 -0700 Subject: [PATCH 1/2] Add denylist for system namespaces in targetNamespace validation CommonSpec.validate() previously accepted any non-empty targetNamespace on Kubernetes, allowing components to be installed into well-known system namespaces such as kube-system, kube-public, kube-node-lease, or default. Add a defense-in-depth denylist that rejects those four reserved namespaces on both Kubernetes and OpenShift, while preserving the existing OpenShift-only openshift-operators rejection. Because validate() is the shared method called by every component validator, the denylist applies to all of them with no caller changes. Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> --- .../operator/v1alpha1/common_validation.go | 32 ++++++++++++++++--- .../v1alpha1/common_validation_test.go | 6 +++- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/pkg/apis/operator/v1alpha1/common_validation.go b/pkg/apis/operator/v1alpha1/common_validation.go index 384cac003f..e6fc195b16 100644 --- a/pkg/apis/operator/v1alpha1/common_validation.go +++ b/pkg/apis/operator/v1alpha1/common_validation.go @@ -22,16 +22,38 @@ import ( "knative.dev/pkg/apis" ) +var reservedSystemNamespaces = []string{ + "kube-system", + "kube-public", + "kube-node-lease", + "default", +} + func (ta *CommonSpec) validate(path string) *apis.FieldError { var errs *apis.FieldError + targetNamespace := ta.GetTargetNamespace() targetNamespacePath := fmt.Sprintf("%s.targetNamespace", path) - if ta.GetTargetNamespace() == "" { + if targetNamespace == "" { errs = errs.Also(apis.ErrMissingField(targetNamespacePath)) - } else if IsOpenShiftPlatform() { - // "openshift-operators" namespace restricted in openshift environment - if ta.GetTargetNamespace() == "openshift-operators" { - errs = errs.Also(apis.ErrInvalidValue(ta.GetTargetNamespace(), targetNamespacePath, "'openshift-operators' namespace is not allowed")) + } else { + if isReservedSystemNamespace(targetNamespace) { + errs = errs.Also(apis.ErrInvalidValue(targetNamespace, targetNamespacePath, fmt.Sprintf("'%s' is a reserved system namespace and is not allowed", targetNamespace))) + } + if IsOpenShiftPlatform() { + // "openshift-operators" namespace restricted in openshift environment + if targetNamespace == "openshift-operators" { + errs = errs.Also(apis.ErrInvalidValue(targetNamespace, targetNamespacePath, "'openshift-operators' namespace is not allowed")) + } } } return errs } + +func isReservedSystemNamespace(namespace string) bool { + for _, reservedNamespace := range reservedSystemNamespaces { + if namespace == reservedNamespace { + return true + } + } + return false +} diff --git a/pkg/apis/operator/v1alpha1/common_validation_test.go b/pkg/apis/operator/v1alpha1/common_validation_test.go index 30c21752f5..e1b52c5b04 100644 --- a/pkg/apis/operator/v1alpha1/common_validation_test.go +++ b/pkg/apis/operator/v1alpha1/common_validation_test.go @@ -34,8 +34,12 @@ func TestValidateCommonTargetNamespace(t *testing.T) { {name: "empty-value", targetNamespace: "", err: "missing field(s): spec.targetNamespace", isOpenshift: false}, {name: "ns-tekton-pipelines", targetNamespace: "tekton-pipelines", err: "", isOpenshift: false}, {name: "ns-hello", targetNamespace: "hello", err: "", isOpenshift: false}, - {name: "ns-default", targetNamespace: "default", err: "", isOpenshift: false}, + {name: "ns-kube-system", targetNamespace: "kube-system", err: "invalid value: kube-system: spec.targetNamespace\n'kube-system' is a reserved system namespace and is not allowed", isOpenshift: false}, + {name: "ns-kube-public", targetNamespace: "kube-public", err: "invalid value: kube-public: spec.targetNamespace\n'kube-public' is a reserved system namespace and is not allowed", isOpenshift: false}, + {name: "ns-kube-node-lease", targetNamespace: "kube-node-lease", err: "invalid value: kube-node-lease: spec.targetNamespace\n'kube-node-lease' is a reserved system namespace and is not allowed", isOpenshift: false}, + {name: "ns-default", targetNamespace: "default", err: "invalid value: default: spec.targetNamespace\n'default' is a reserved system namespace and is not allowed", isOpenshift: false}, {name: "ns-openshift-operators", targetNamespace: "openshift-operators", err: "", isOpenshift: false}, + {name: "openshift-ns-default", targetNamespace: "default", err: "invalid value: default: spec.targetNamespace\n'default' is a reserved system namespace and is not allowed", isOpenshift: true}, {name: "openshift-ns-openshift-operators", targetNamespace: "openshift-operators", err: "invalid value: openshift-operators: spec.targetNamespace\n'openshift-operators' namespace is not allowed", isOpenshift: true}, {name: "openshift-ns-openshift-pipelines", targetNamespace: "openshift-pipelines", err: "", isOpenshift: true}, {name: "openshift-ns-openshift-xyz", targetNamespace: "openshift-xyz", err: "", isOpenshift: true}, From 258d9b3517c4c011ef25943ee303e76d5e7f8412 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Wed, 17 Jun 2026 04:33:50 -0700 Subject: [PATCH 2/2] Enforce targetNamespace validation on TektonResult TektonResult.Validate calls TektonResultSpec.validate, which did not call the embedded CommonSpec.validate, so the new system-namespace denylist (and the pre-existing empty-field check) was bypassed for TektonResult. Wire the common validation into TektonResultSpec.validate so the denylist is enforced consistently across every CommonSpec-backed component. Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> --- .../v1alpha1/tektonresult_validation.go | 5 +++++ .../v1alpha1/tektonresult_validation_test.go | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/pkg/apis/operator/v1alpha1/tektonresult_validation.go b/pkg/apis/operator/v1alpha1/tektonresult_validation.go index 7fc48505bd..6391505f2a 100644 --- a/pkg/apis/operator/v1alpha1/tektonresult_validation.go +++ b/pkg/apis/operator/v1alpha1/tektonresult_validation.go @@ -43,6 +43,11 @@ func (tp *TektonResult) Validate(ctx context.Context) (errs *apis.FieldError) { } func (trs *TektonResultSpec) validate(path string) (errs *apis.FieldError) { + // validate the embedded CommonSpec (e.g. targetNamespace denylist), + // which TektonResult would otherwise bypass since it does not call + // CommonSpec.validate the way the other components do. + errs = errs.Also(trs.CommonSpec.validate(path)) + if trs.LokiStackName != "" { if strings.ToLower(trs.LogsType) != LogsTypeLoki && trs.LogsType != "" { errMsg := fmt.Sprintf("Loki stack is only supported when logs_type is loki or empty, got logs_type: %s", trs.LogsType) diff --git a/pkg/apis/operator/v1alpha1/tektonresult_validation_test.go b/pkg/apis/operator/v1alpha1/tektonresult_validation_test.go index 1f74366c02..30342acef4 100644 --- a/pkg/apis/operator/v1alpha1/tektonresult_validation_test.go +++ b/pkg/apis/operator/v1alpha1/tektonresult_validation_test.go @@ -37,6 +37,23 @@ func TestTektonResult_Validate(t *testing.T) { assert.Equal(t, "invalid value: wrong-name: metadata.name, Only one instance of TektonResult is allowed by name, result", err.Error()) } +func TestTektonResult_ValidateTargetNamespaceDenylist(t *testing.T) { + tr := &TektonResult{ + ObjectMeta: metav1.ObjectMeta{ + Name: "result", + Namespace: "namespace", + }, + Spec: TektonResultSpec{ + CommonSpec: CommonSpec{ + TargetNamespace: "kube-system", + }, + }, + } + + errs := tr.Validate(context.TODO()) + assert.Equal(t, "invalid value: kube-system: spec.targetNamespace\n'kube-system' is a reserved system namespace and is not allowed", errs.Error()) +} + func TestTektonResultWatcherPerformancePropertiesValidate(t *testing.T) { tr := &TektonResult{ ObjectMeta: metav1.ObjectMeta{