From bbb248c4aa7492295e1721d8fe0810eb28a2f26b Mon Sep 17 00:00:00 2001 From: Jawed khelil Date: Mon, 13 Jul 2026 16:01:25 +0200 Subject: [PATCH 1/2] feat(operator): add NetworkPolicy for operator namespace Ship fixed NetworkPolicies for the operator's own namespace (tekton-operator / openshift-operators) as static manifests alongside the Deployment/RBAC, scoping the main operator and CR admission webhook pods for ingress/egress. No CR watches the operator's own namespace, so these cannot be reconciled and ship statically instead. Each podSelector targets this operator's own pod labels only, never an empty/namespace-wide selector, so the bundle never affects other pods sharing openshift-operators. Related to SRVKP-12050. Signed-off-by: Jawed khelil Assisted-by: Claude Sonnet 5 (via Cursor) Co-authored-by: Cursor --- config/kubernetes/base/kustomization.yaml | 1 + config/kubernetes/base/networkpolicy.yaml | 89 ++++++++++++++++++++++ config/openshift/base/kustomization.yaml | 1 + config/openshift/base/networkpolicy.yaml | 90 +++++++++++++++++++++++ docs/NetworkPolicy.md | 38 ++++++++++ 5 files changed, 219 insertions(+) create mode 100644 config/kubernetes/base/networkpolicy.yaml create mode 100644 config/openshift/base/networkpolicy.yaml diff --git a/config/kubernetes/base/kustomization.yaml b/config/kubernetes/base/kustomization.yaml index 4cf6421b53..9a2a1edb6f 100644 --- a/config/kubernetes/base/kustomization.yaml +++ b/config/kubernetes/base/kustomization.yaml @@ -34,3 +34,4 @@ resources: - ../../webhooks/ - operator_service.yaml - pipelinesascode.yaml +- networkpolicy.yaml diff --git a/config/kubernetes/base/networkpolicy.yaml b/config/kubernetes/base/networkpolicy.yaml new file mode 100644 index 0000000000..aefc05685c --- /dev/null +++ b/config/kubernetes/base/networkpolicy.yaml @@ -0,0 +1,89 @@ +# Copyright 2026 The Tekton Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# NetworkPolicy resources for the operator's own namespace (tekton-operator). Unlike the +# NetworkPolicies documented in docs/NetworkPolicy.md (reconciled dynamically by the operator for +# operand namespaces such as tekton-pipelines/tekton-triggers), no CR watches the operator's own +# namespace, so these ship statically with the install manifests/bundle instead. podSelectors are +# scoped to this operator's own pod labels only — never an empty/namespace-wide selector — so +# installing this bundle never affects other pods/operators that might share the namespace. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: tekton-operator +spec: + podSelector: + matchLabels: + name: tekton-operator + policyTypes: + - Ingress + - Egress + ingress: + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: monitoring + ports: + - protocol: TCP + port: 9090 + egress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + podSelector: + matchLabels: + k8s-app: kube-dns + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + - ports: + - protocol: TCP + port: 443 +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: tekton-operator-webhook +spec: + podSelector: + matchLabels: + name: tekton-operator-webhook + policyTypes: + - Ingress + - Egress + ingress: + # Admission webhook callbacks originate from the API server; source IPs vary by cluster + # network plugin, so no ingress "from" restriction is applied here. + - ports: + - protocol: TCP + port: 8443 + egress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + podSelector: + matchLabels: + k8s-app: kube-dns + ports: + - protocol: UDP + port: 53 + - protocol: TCP + port: 53 + - ports: + - protocol: TCP + port: 443 diff --git a/config/openshift/base/kustomization.yaml b/config/openshift/base/kustomization.yaml index afb1baba37..f48152023e 100644 --- a/config/openshift/base/kustomization.yaml +++ b/config/openshift/base/kustomization.yaml @@ -56,6 +56,7 @@ resources: - ../../webhooks - operator_service.yaml - operator_servicemonitor.yaml +- networkpolicy.yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization diff --git a/config/openshift/base/networkpolicy.yaml b/config/openshift/base/networkpolicy.yaml new file mode 100644 index 0000000000..faa22cae23 --- /dev/null +++ b/config/openshift/base/networkpolicy.yaml @@ -0,0 +1,90 @@ +# Copyright 2026 The Tekton Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# NetworkPolicy resources for the operator's own namespace (openshift-operators). Unlike the +# NetworkPolicies documented in docs/NetworkPolicy.md (reconciled dynamically by the operator for +# operand namespaces such as openshift-pipelines), no CR watches the operator's own namespace, so +# these ship statically with the install manifests/OLM bundle instead. podSelectors are scoped to +# this operator's own pod labels only — never an empty/namespace-wide selector, per the OLM bundle +# guidance — so installing this bundle never affects other operators that may share the namespace +# (openshift-operators is typically shared across many cluster-scoped operators). +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: openshift-pipelines-operator +spec: + podSelector: + matchLabels: + name: openshift-pipelines-operator + policyTypes: + - Ingress + - Egress + ingress: + - from: + - namespaceSelector: + matchLabels: + openshift.io/cluster-monitoring: "true" + ports: + - protocol: TCP + port: 9090 + egress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: openshift-dns + podSelector: + matchLabels: + dns.operator.openshift.io/daemonset-dns: default + ports: + - protocol: UDP + port: 5353 + - protocol: TCP + port: 5353 + - ports: + - protocol: TCP + port: 6443 +--- +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: tekton-operator-webhook +spec: + podSelector: + matchLabels: + name: tekton-operator-webhook + policyTypes: + - Ingress + - Egress + ingress: + # Admission webhook callbacks originate from the API server; source IPs vary by cluster + # network plugin, so no ingress "from" restriction is applied here. + - ports: + - protocol: TCP + port: 8443 + egress: + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: openshift-dns + podSelector: + matchLabels: + dns.operator.openshift.io/daemonset-dns: default + ports: + - protocol: UDP + port: 5353 + - protocol: TCP + port: 5353 + - ports: + - protocol: TCP + port: 6443 diff --git a/docs/NetworkPolicy.md b/docs/NetworkPolicy.md index 2bb28e97cf..af55ec728d 100644 --- a/docs/NetworkPolicy.md +++ b/docs/NetworkPolicy.md @@ -63,6 +63,44 @@ to the operand namespace (e.g. `tekton-pipelines` or `openshift-pipelines`): | API server port | 443 | 6443 | | Prometheus namespace label | `kubernetes.io/metadata.name: monitoring` | `openshift.io/cluster-monitoring: "true"` | +## Operator's own namespace + +The operator's own namespace (`tekton-operator` on Kubernetes, `openshift-operators` +on OpenShift) ships two fixed NetworkPolicies as static manifests alongside the +operator's Deployment/RBAC — in `config/kubernetes/base/networkpolicy.yaml` and +`config/openshift/base/networkpolicy.yaml` respectively. These are **not** +reconciled by a controller and are **not** configurable via `spec.networkPolicy`: +no CR watches the operator's own namespace, so there is nothing to gate this on. +They are also not a namespace-wide default-deny — each policy's `podSelector` is +scoped to one of the operator's own pods (`name: tekton-operator` / +`name: openshift-pipelines-operator` for the main controller, and +`name: tekton-operator-webhook` for the CR admission webhook) so that installing +the operator's bundle never affects unrelated pods that might share the namespace +(`openshift-operators` in particular is commonly shared by many operators). + +| Policy | Direction | Port | Source / Destination | +|---|---|---|---| +| `tekton-operator` / `openshift-pipelines-operator` | ingress | TCP/9090 | Prometheus namespace | +| | egress | UDP+TCP/53 or 5353 | DNS resolver pods | +| | egress | TCP/443 or 6443 | API server | +| `tekton-operator-webhook` | ingress | TCP/8443 | Any (admission webhook) | +| | egress | UDP+TCP/53 or 5353 | DNS resolver pods | +| | egress | TCP/443 or 6443 | API server | + +**OpenShift caveat**: `openshift-operators` is a shared namespace where OLM installs +operators from OperatorHub, many of which ship no NetworkPolicy of their own. To +avoid silently breaking those operators' networking, OpenShift's platform payload +ships a permissive `default-allow-all` NetworkPolicy in that namespace out of the +box (labeled `capability.openshift.io/name: OperatorLifecycleManager`), with an +empty `podSelector` allowing all ingress/egress for every pod in the namespace. +Because NetworkPolicy rules are additive (a pod's allowed traffic is the union of +every policy that selects it, not the intersection), this platform-shipped policy +supersedes the two policies above in practice — the operator's own pods remain +fully open on a stock OpenShift cluster until a cluster admin removes or replaces +`default-allow-all`. The `openshift-pipelines` (operand) namespace has no such +baseline policy, so the `proxy-webhook` policies further up this page are enforced +as documented without this caveat. + ## Disabling ```yaml From cb1102009e4f35a9db882f1c0b732e81fafd0ca0 Mon Sep 17 00:00:00 2001 From: Jawed khelil Date: Mon, 13 Jul 2026 16:02:07 +0200 Subject: [PATCH 2/2] feat(tektonpipeline): add NetworkPolicy for proxy-webhook Reuse the NetworkPolicy foundation built for TektonTrigger to scope ingress/egress for the proxy-webhook workload TektonPipeline deploys into the operand namespace. Adds spec.networkPolicy to TektonPipeline, propagated from TektonConfig, and reconciles it via a new custom InstallerSet, mirroring the TektonTrigger pattern. Related to SRVKP-12050. Signed-off-by: Jawed khelil Assisted-by: Claude Sonnet 5 (via Cursor) Co-authored-by: Cursor --- .../templates/kubernetes-crds.yaml | 18 +++ .../operator.tekton.dev_tektonpipelines.yaml | 18 +++ docs/NetworkPolicy.md | 19 ++- .../operator/v1alpha1/tektonconfig_types.go | 6 +- .../operator/v1alpha1/tektonpipeline_types.go | 4 + .../v1alpha1/zz_generated.deepcopy.go | 1 + .../kubernetes/tektonpipeline/controller.go | 7 + .../kubernetes/tektonpipeline/finalize.go | 5 + .../tektonpipeline/networkpolicies.go | 105 +++++++++++++ .../kubernetes/tektonpipeline/reconcile.go | 13 ++ .../shared/tektonconfig/pipeline/pipeline.go | 10 +- .../05_tektonpipeline_networkpolicy_test.go | 144 ++++++++++++++++++ 12 files changed, 342 insertions(+), 8 deletions(-) create mode 100644 pkg/reconciler/kubernetes/tektonpipeline/networkpolicies.go create mode 100644 test/e2e/common/05_tektonpipeline_networkpolicy_test.go diff --git a/charts/tekton-operator/templates/kubernetes-crds.yaml b/charts/tekton-operator/templates/kubernetes-crds.yaml index 30dee1d736..665be83917 100644 --- a/charts/tekton-operator/templates/kubernetes-crds.yaml +++ b/charts/tekton-operator/templates/kubernetes-crds.yaml @@ -2829,6 +2829,24 @@ spec: type: string metrics.taskrun.level: type: string + networkPolicy: + description: |- + NetworkPolicy configures NetworkPolicy creation for the proxy-webhook + workload deployed by TektonPipeline. + properties: + disabled: + description: |- + Disabled disables all NetworkPolicy creation for this component. + Existing policies are removed on the next reconcile. + type: boolean + policies: + description: |- + Policies merges with the operator's default NetworkPolicies by name. + A key matching a default policy name replaces that default entirely. + A key not matching any default is added alongside the defaults. + If nil or empty, all operator defaults are applied unchanged. + x-kubernetes-preserve-unknown-fields: true + type: object options: description: options holds additions fields and these fields will be updated on the manifests diff --git a/config/base/generated-crds/operator.tekton.dev_tektonpipelines.yaml b/config/base/generated-crds/operator.tekton.dev_tektonpipelines.yaml index 2c3d58d9de..afde5e38ef 100644 --- a/config/base/generated-crds/operator.tekton.dev_tektonpipelines.yaml +++ b/config/base/generated-crds/operator.tekton.dev_tektonpipelines.yaml @@ -196,6 +196,24 @@ spec: type: string metrics.taskrun.level: type: string + networkPolicy: + description: |- + NetworkPolicy configures NetworkPolicy creation for the proxy-webhook + workload deployed by TektonPipeline. + properties: + disabled: + description: |- + Disabled disables all NetworkPolicy creation for this component. + Existing policies are removed on the next reconcile. + type: boolean + policies: + description: |- + Policies merges with the operator's default NetworkPolicies by name. + A key matching a default policy name replaces that default entirely. + A key not matching any default is added alongside the defaults. + If nil or empty, all operator defaults are applied unchanged. + x-kubernetes-preserve-unknown-fields: true + type: object options: description: options holds additions fields and these fields will be updated on the manifests diff --git a/docs/NetworkPolicy.md b/docs/NetworkPolicy.md index af55ec728d..eaf6371d29 100644 --- a/docs/NetworkPolicy.md +++ b/docs/NetworkPolicy.md @@ -7,7 +7,8 @@ weight: 15 # NetworkPolicy The operator can manage [NetworkPolicy][np] resources for Tekton component workloads. -Currently only TektonTrigger is supported; other components will be added later. +Currently TektonTrigger and TektonPipeline (proxy-webhook only) are supported; +other components will be added later. Configuration is available via `TektonConfig`: @@ -30,8 +31,9 @@ spec: - port: 9000 ``` -The `networkPolicy` field is propagated from `TektonConfig` to `TektonTrigger`. -Users can also configure it directly on the `TektonTrigger` CR. +The `networkPolicy` field is propagated from `TektonConfig` to `TektonTrigger` and +`TektonPipeline`. Users can also configure it directly on the `TektonTrigger` or +`TektonPipeline` CR. ## Default Policies @@ -53,6 +55,17 @@ to the operand namespace (e.g. `tekton-pipelines` or `openshift-pipelines`): | | egress | UDP+TCP/53 or 5353 | DNS resolver pods | | | egress | TCP/443 or 6443 | API server | | | egress | TCP/80, 443 | Any (external APIs e.g. GitHub) | +| `tekton-proxy-webhook-default-deny` | deny all | — | All pods with `name: tekton-operator` (proxy-webhook) in the Pipeline target namespace | +| `proxy-webhook` | ingress | TCP/8443 | Any (admission webhook) | +| | egress | UDP+TCP/53 or 5353 | DNS resolver pods | +| | egress | TCP/443 or 6443 | API server | + +The `proxy-webhook` policies apply to the TektonPipeline target namespace (e.g. +`tekton-pipelines` or `openshift-pipelines`), where the operator deploys the +proxy-webhook Deployment. They do not cover the operator's own namespace +(`tekton-operator` / `openshift-operators`), which ships fixed, non-configurable +NetworkPolicies as part of the operator's own install manifests/bundle (see +[Operator's own namespace](#operators-own-namespace) below). ### Platform differences diff --git a/pkg/apis/operator/v1alpha1/tektonconfig_types.go b/pkg/apis/operator/v1alpha1/tektonconfig_types.go index 3f1f33b590..68b22d461b 100644 --- a/pkg/apis/operator/v1alpha1/tektonconfig_types.go +++ b/pkg/apis/operator/v1alpha1/tektonconfig_types.go @@ -135,9 +135,9 @@ type TektonConfigSpec struct { // +optional TargetNamespaceMetadata *NamespaceMetadata `json:"targetNamespaceMetadata,omitempty"` // NetworkPolicy configures NetworkPolicy resources for the operand namespace. - // This field is propagated to TektonTrigger, which is the only component with - // NetworkPolicy reconciliation implemented. Other components (Pipeline, Chains, - // Results, Dashboard) do not yet act on this field. + // This field is propagated to TektonTrigger and TektonPipeline, which are the + // only components with NetworkPolicy reconciliation implemented. Other + // components (Chains, Results, Dashboard) do not yet act on this field. // +optional NetworkPolicy NetworkPolicyConfig `json:"networkPolicy,omitempty"` } diff --git a/pkg/apis/operator/v1alpha1/tektonpipeline_types.go b/pkg/apis/operator/v1alpha1/tektonpipeline_types.go index 7706c86991..b9ae70d2e2 100644 --- a/pkg/apis/operator/v1alpha1/tektonpipeline_types.go +++ b/pkg/apis/operator/v1alpha1/tektonpipeline_types.go @@ -56,6 +56,10 @@ type TektonPipelineSpec struct { // Config holds the configuration for resources created by TektonPipeline // +optional Config Config `json:"config,omitempty"` + // NetworkPolicy configures NetworkPolicy creation for the proxy-webhook + // workload deployed by TektonPipeline. + // +optional + NetworkPolicy NetworkPolicyConfig `json:"networkPolicy,omitempty"` } // TektonPipelineStatus defines the observed state of TektonPipeline diff --git a/pkg/apis/operator/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/operator/v1alpha1/zz_generated.deepcopy.go index 2227f7becc..6326fbd88d 100644 --- a/pkg/apis/operator/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/operator/v1alpha1/zz_generated.deepcopy.go @@ -2156,6 +2156,7 @@ func (in *TektonPipelineSpec) DeepCopyInto(out *TektonPipelineSpec) { out.CommonSpec = in.CommonSpec in.Pipeline.DeepCopyInto(&out.Pipeline) in.Config.DeepCopyInto(&out.Config) + in.NetworkPolicy.DeepCopyInto(&out.NetworkPolicy) return } diff --git a/pkg/reconciler/kubernetes/tektonpipeline/controller.go b/pkg/reconciler/kubernetes/tektonpipeline/controller.go index 71d41acf98..5e66a5c30a 100644 --- a/pkg/reconciler/kubernetes/tektonpipeline/controller.go +++ b/pkg/reconciler/kubernetes/tektonpipeline/controller.go @@ -25,6 +25,7 @@ import ( tektonPipelineInformer "github.com/tektoncd/operator/pkg/client/injection/informers/operator/v1alpha1/tektonpipeline" tektonPipelineReconciler "github.com/tektoncd/operator/pkg/client/injection/reconciler/operator/v1alpha1/tektonpipeline" "github.com/tektoncd/operator/pkg/reconciler/common" + "github.com/tektoncd/operator/pkg/reconciler/common/networkpolicy" "github.com/tektoncd/operator/pkg/reconciler/kubernetes/tektoninstallerset/client" "k8s.io/client-go/tools/cache" kubeclient "knative.dev/pkg/client/injection/kube/client" @@ -62,12 +63,18 @@ func NewExtendedController(generator common.ExtensionGenerator) injection.Contro } tisClient := operatorclient.Get(ctx).OperatorV1alpha1().TektonInstallerSets() + params := networkpolicy.KubernetesPlatformDefaults() + if v1alpha1.IsOpenShiftPlatform() { + params = networkpolicy.OpenShiftPlatformDefaults() + } + c := &Reconciler{ kubeClientSet: kubeclient.Get(ctx), extension: generator(ctx), manifest: manifest, pipelineVersion: pipelineVer, installerSetClient: client.NewInstallerSetClient(tisClient, operatorVer, pipelineVer, v1alpha1.KindTektonPipeline, metrics), + platformParams: params, } impl := tektonPipelineReconciler.NewImpl(ctx, c) diff --git a/pkg/reconciler/kubernetes/tektonpipeline/finalize.go b/pkg/reconciler/kubernetes/tektonpipeline/finalize.go index eed4ad3834..fa3c5fd9ae 100644 --- a/pkg/reconciler/kubernetes/tektonpipeline/finalize.go +++ b/pkg/reconciler/kubernetes/tektonpipeline/finalize.go @@ -45,6 +45,11 @@ func (r *Reconciler) FinalizeKind(ctx context.Context, original *v1alpha1.Tekton return err } + if err := r.installerSetClient.CleanupCustomSet(ctx, "pipeline-network-policies"); err != nil { + logger.Error("failed to cleanup pipeline network policies installerset: ", err) + return err + } + if err := r.extension.Finalize(ctx, original); err != nil { logger.Error("Failed to finalize platform resources: ", err) } diff --git a/pkg/reconciler/kubernetes/tektonpipeline/networkpolicies.go b/pkg/reconciler/kubernetes/tektonpipeline/networkpolicies.go new file mode 100644 index 0000000000..75224d87a2 --- /dev/null +++ b/pkg/reconciler/kubernetes/tektonpipeline/networkpolicies.go @@ -0,0 +1,105 @@ +/* +Copyright 2026 The Tekton Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package tektonpipeline + +import ( + "context" + + mf "github.com/manifestival/manifestival" + "github.com/tektoncd/operator/pkg/apis/operator/v1alpha1" + "github.com/tektoncd/operator/pkg/reconciler/common/networkpolicy" + "github.com/tektoncd/operator/pkg/reconciler/kubernetes/tektoninstallerset/client" + networkingv1 "k8s.io/api/networking/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +// proxyWebhookPodSelector matches the proxy-webhook Deployment/Service selector +// (name: tekton-operator) shipped in cmd/{kubernetes,openshift}/operator/kodata/webhook/webhook.yaml. +// This label is the same value used by the main operator Deployment in the operator's own +// namespace (see tektoncd/operator#3227) — harmless here because the proxy-webhook runs in the +// operand namespace (e.g. tekton-pipelines / openshift-pipelines), never the operator namespace. +var proxyWebhookPodSelector = metav1.LabelSelector{ + MatchLabels: map[string]string{"name": "tekton-operator"}, +} + +// proxyWebhookDefaultPolicies returns the default NetworkPolicies for the proxy-webhook +// workload specifically. As more TektonPipeline-managed workloads gain NetworkPolicy +// support, they should get their own DefaultPolicies function rather than +// growing this one, so reconcileNetworkPolicies can just concatenate each workload's +// defaults (see triggersDefaultPolicies in tektontrigger for the alternative — a single +// function covering multiple workloads — which this intentionally avoids). +func proxyWebhookDefaultPolicies(params networkpolicy.PlatformParams) []networkingv1.NetworkPolicy { + webhookPort := intstr.FromInt32(8443) + + return []networkingv1.NetworkPolicy{ + { + ObjectMeta: metav1.ObjectMeta{Name: "proxy-webhook"}, + Spec: networkingv1.NetworkPolicySpec{ + PodSelector: proxyWebhookPodSelector, + PolicyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress}, + Ingress: []networkingv1.NetworkPolicyIngressRule{ + // cidr="" → permissive; restrict via spec.networkPolicy.policies if needed. + networkpolicy.WebhookIngressRule("", webhookPort), + }, + Egress: []networkingv1.NetworkPolicyEgressRule{ + networkpolicy.DNSEgressRule(params), + networkpolicy.APIServerEgressRule(params), + }, + }, + }, + } +} + +// proxyWebhookDefaultDenyPolicy returns the scoped default-deny for the proxy-webhook pod +// only. It is not a component-wide default-deny — each TektonPipeline-managed workload +// that gains NetworkPolicy support should define its own DefaultDenyPolicy. +func proxyWebhookDefaultDenyPolicy() networkingv1.NetworkPolicy { + return networkpolicy.DefaultDenyPolicy("tekton-proxy-webhook-default-deny", proxyWebhookPodSelector) +} + +// reconcileNetworkPolicies reconciles every NetworkPolicy owned by TektonPipeline as a +// single CustomSet. Currently that's just the proxy-webhook's; as more workloads gain +// NetworkPolicy support, append their DefaultDenyPolicy/DefaultPolicies +// results into defaults below rather than renaming or overloading these. +func (r *Reconciler) reconcileNetworkPolicies(ctx context.Context, tp *v1alpha1.TektonPipeline) error { + if tp.Spec.NetworkPolicy.Disabled { + return r.installerSetClient.CleanupCustomSet(ctx, "pipeline-network-policies") + } + defaults := append( + []networkingv1.NetworkPolicy{proxyWebhookDefaultDenyPolicy()}, + proxyWebhookDefaultPolicies(r.platformParams)..., + ) + manifest, err := networkpolicy.Generate( + tp.Spec.NetworkPolicy, + tp.Spec.GetTargetNamespace(), + defaults, + ) + if err != nil { + return err + } + return r.installerSetClient.CustomSet(ctx, tp, "pipeline-network-policies", &manifest, passthroughTransform, nil) +} + +// passthroughTransform is a no-op FilterAndTransform used for pre-built manifests +// where namespace injection is already handled by Generate. +func passthroughTransform(_ context.Context, m *mf.Manifest, _ v1alpha1.TektonComponent) (*mf.Manifest, error) { + return m, nil +} + +// Ensure passthroughTransform satisfies the FilterAndTransform type. +var _ client.FilterAndTransform = passthroughTransform diff --git a/pkg/reconciler/kubernetes/tektonpipeline/reconcile.go b/pkg/reconciler/kubernetes/tektonpipeline/reconcile.go index 8c8bf26f42..5dbd860eae 100644 --- a/pkg/reconciler/kubernetes/tektonpipeline/reconcile.go +++ b/pkg/reconciler/kubernetes/tektonpipeline/reconcile.go @@ -24,6 +24,7 @@ import ( "github.com/tektoncd/operator/pkg/apis/operator/v1alpha1" tektonpipelinereconciler "github.com/tektoncd/operator/pkg/client/injection/reconciler/operator/v1alpha1/tektonpipeline" "github.com/tektoncd/operator/pkg/reconciler/common" + "github.com/tektoncd/operator/pkg/reconciler/common/networkpolicy" "github.com/tektoncd/operator/pkg/reconciler/kubernetes/tektoninstallerset/client" "k8s.io/client-go/kubernetes" "knative.dev/pkg/apis" @@ -48,6 +49,8 @@ type Reconciler struct { kubeClientSet kubernetes.Interface // version of pipelines which we are installing pipelineVersion string + // platformParams holds platform-specific values for building NetworkPolicy rules + platformParams networkpolicy.PlatformParams } // Check that our Reconciler implements controller.Reconciler @@ -154,6 +157,16 @@ func (r *Reconciler) ReconcileKind(ctx context.Context, tp *v1alpha1.TektonPipel } logger.Debug("Main manifest applied successfully") + if err := r.reconcileNetworkPolicies(ctx, tp); err != nil { + if err == v1alpha1.REQUEUE_EVENT_AFTER { + return err + } + msg := fmt.Sprintf("NetworkPolicy reconciliation failed: %s", err.Error()) + logger.Errorw("NetworkPolicy reconciliation failed", "error", err) + tp.Status.MarkInstallerSetNotReady(msg) + return nil + } + logger.Debug("Executing post-reconciliation") if err := r.extension.PostReconcile(ctx, tp); err != nil { msg := fmt.Sprintf("PostReconciliation failed: %s", err.Error()) diff --git a/pkg/reconciler/shared/tektonconfig/pipeline/pipeline.go b/pkg/reconciler/shared/tektonconfig/pipeline/pipeline.go index 61d8489df1..8d7ca195d3 100644 --- a/pkg/reconciler/shared/tektonconfig/pipeline/pipeline.go +++ b/pkg/reconciler/shared/tektonconfig/pipeline/pipeline.go @@ -76,8 +76,9 @@ func GetTektonPipelineCR(config *v1alpha1.TektonConfig, operatorVersion string) CommonSpec: v1alpha1.CommonSpec{ TargetNamespace: config.Spec.TargetNamespace, }, - Pipeline: config.Spec.Pipeline, - Config: config.Spec.Config, + Pipeline: config.Spec.Pipeline, + Config: config.Spec.Config, + NetworkPolicy: config.Spec.NetworkPolicy, }, } } @@ -116,6 +117,11 @@ func UpdatePipeline(ctx context.Context, old *v1alpha1.TektonPipeline, new *v1al updated = true } + if !reflect.DeepEqual(old.Spec.NetworkPolicy, new.Spec.NetworkPolicy) { + old.Spec.NetworkPolicy = new.Spec.NetworkPolicy + updated = true + } + if old.ObjectMeta.OwnerReferences == nil { old.ObjectMeta.OwnerReferences = new.ObjectMeta.OwnerReferences updated = true diff --git a/test/e2e/common/05_tektonpipeline_networkpolicy_test.go b/test/e2e/common/05_tektonpipeline_networkpolicy_test.go new file mode 100644 index 0000000000..c8ac0bb9a7 --- /dev/null +++ b/test/e2e/common/05_tektonpipeline_networkpolicy_test.go @@ -0,0 +1,144 @@ +//go:build e2e +// +build e2e + +/* +Copyright 2026 The Tekton Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package common + +import ( + "context" + "fmt" + "testing" + + "github.com/tektoncd/operator/test/client" + "github.com/tektoncd/operator/test/resources" + "github.com/tektoncd/operator/test/utils" + pipelinev1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// TestTektonPipelineNetworkPolicy verifies NetworkPolicies are created by default +// for the proxy-webhook workload TektonPipeline deploys, that the proxy-webhook +// keeps working under those policies (its mutating admission callback for TaskRun +// Pods still succeeds), and that toggling spec.networkPolicy.disabled correctly +// adds and removes the policies. +func TestTektonPipelineNetworkPolicy(t *testing.T) { + crNames := utils.GetResourceNames() + clients := client.Setup(t, crNames.TargetNamespace) + + utils.CleanupOnInterrupt(func() { utils.TearDownPipeline(clients, crNames.TektonPipeline) }) + utils.CleanupOnInterrupt(func() { utils.TearDownNamespace(clients, crNames.TargetNamespace) }) + defer utils.TearDownNamespace(clients, crNames.TargetNamespace) + defer utils.TearDownPipeline(clients, crNames.TektonPipeline) + + resources.EnsureNoTektonConfigInstance(t, clients, crNames) + + if _, err := resources.EnsureTektonPipelineExists(clients.TektonPipeline(), crNames); err != nil { + t.Fatalf("TektonPipeline %q failed to create: %v", crNames.TektonPipeline, err) + } + resources.AssertTektonPipelineCRReadyStatus(t, clients, crNames) + + expectedPolicies := []string{ + "tekton-proxy-webhook-default-deny", + "proxy-webhook", + } + + t.Run("default-policies-created", func(t *testing.T) { + resources.AssertNetworkPoliciesExist(t, clients, crNames.TargetNamespace, expectedPolicies) + }) + + // The proxy-webhook's MutatingWebhookConfiguration (failurePolicy: Fail) intercepts + // Pod Create requests labeled app.kubernetes.io/managed-by=tekton-pipelines (see + // pkg/reconciler/proxy/proxy.go). A TaskRun's underlying Pod carries that label, so + // running one to completion proves the API server could reach the proxy-webhook on + // port 8443 under the NetworkPolicy's ingress rule — if it couldn't, Pod admission + // would be rejected and the TaskRun would never start. + t.Run("proxy-webhook-functional-with-networkpolicies", func(t *testing.T) { + taskRun := createNetworkPolicyProbeTaskRun(crNames.TargetNamespace) + createdTaskRun, err := clients.TektonClient.TaskRuns(crNames.TargetNamespace).Create( + context.TODO(), taskRun, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("failed to create TaskRun: %v", err) + } + + if err := resources.WaitForTaskRunHappy( + clients.TektonClient, + crNames.TargetNamespace, + createdTaskRun.Name, + func(tr *pipelinev1.TaskRun) (bool, error) { + if tr.IsDone() { + if tr.IsSuccessful() { + return true, nil + } + return false, fmt.Errorf("TaskRun failed") + } + return false, nil + }, + ); err != nil { + t.Fatalf("TaskRun did not complete successfully under NetworkPolicy: %v", err) + } + }) + + t.Run("disable-removes-policies", func(t *testing.T) { + tp, err := clients.TektonPipeline().Get(context.TODO(), crNames.TektonPipeline, metav1.GetOptions{}) + if err != nil { + t.Fatalf("failed to get TektonPipeline: %v", err) + } + tp.Spec.NetworkPolicy.Disabled = true + if _, err := clients.TektonPipeline().Update(context.TODO(), tp, metav1.UpdateOptions{}); err != nil { + t.Fatalf("failed to disable NetworkPolicy on TektonPipeline: %v", err) + } + resources.AssertTektonPipelineCRReadyStatus(t, clients, crNames) + resources.AssertNetworkPoliciesAbsent(t, clients, crNames.TargetNamespace, expectedPolicies) + }) + + t.Run("reenable-restores-policies", func(t *testing.T) { + tp, err := clients.TektonPipeline().Get(context.TODO(), crNames.TektonPipeline, metav1.GetOptions{}) + if err != nil { + t.Fatalf("failed to get TektonPipeline: %v", err) + } + tp.Spec.NetworkPolicy.Disabled = false + if _, err := clients.TektonPipeline().Update(context.TODO(), tp, metav1.UpdateOptions{}); err != nil { + t.Fatalf("failed to re-enable NetworkPolicy on TektonPipeline: %v", err) + } + resources.AssertTektonPipelineCRReadyStatus(t, clients, crNames) + resources.AssertNetworkPoliciesExist(t, clients, crNames.TargetNamespace, expectedPolicies) + }) +} + +// createNetworkPolicyProbeTaskRun creates a minimal TaskRun whose Pod triggers the +// proxy-webhook's mutating admission callback (see comment above). +func createNetworkPolicyProbeTaskRun(namespace string) *pipelinev1.TaskRun { + return &pipelinev1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "networkpolicy-probe-taskrun-", + Namespace: namespace, + }, + Spec: pipelinev1.TaskRunSpec{ + TaskSpec: &pipelinev1.TaskSpec{ + Steps: []pipelinev1.Step{ + { + Name: "echo", + Image: "busybox:stable", + Command: []string{"echo"}, + Args: []string{"proxy-webhook NetworkPolicy probe"}, + }, + }, + }, + }, + } +}