From 896ee815a14ff145312fb80fd8f3ee24633358e6 Mon Sep 17 00:00:00 2001 From: Vinay Pai Date: Thu, 18 Jun 2026 11:55:38 -0700 Subject: [PATCH] conformance: verify CNP ingress/egress directional isolation (Admin + Baseline) Adds dedicated conformance tests confirming a ClusterNetworkPolicy with rules in only one direction does not affect the subject's traffic in the other direction, for both Admin and Baseline tiers. See #103. Signed-off-by: Vinay Pai --- .../admin_tier/standard-egress-isolation.yaml | 23 ++++ .../standard-ingress-isolation.yaml | 23 ++++ .../standard-egress-isolation.yaml | 23 ++++ .../standard-ingress-isolation.yaml | 23 ++++ ...olicy-standard-ingress-egress-isolation.go | 112 ++++++++++++++++++ ...olicy-standard-ingress-egress-isolation.go | 106 +++++++++++++++++ 6 files changed, 310 insertions(+) create mode 100644 conformance/base/admin_tier/standard-egress-isolation.yaml create mode 100644 conformance/base/admin_tier/standard-ingress-isolation.yaml create mode 100644 conformance/base/baseline_tier/standard-egress-isolation.yaml create mode 100644 conformance/base/baseline_tier/standard-ingress-isolation.yaml create mode 100644 conformance/tests/admin-network-policy-standard-ingress-egress-isolation.go create mode 100644 conformance/tests/baseline-admin-network-policy-standard-ingress-egress-isolation.go diff --git a/conformance/base/admin_tier/standard-egress-isolation.yaml b/conformance/base/admin_tier/standard-egress-isolation.yaml new file mode 100644 index 00000000..82c9e47e --- /dev/null +++ b/conformance/base/admin_tier/standard-egress-isolation.yaml @@ -0,0 +1,23 @@ +# An Admin-tier ClusterNetworkPolicy with ONLY egress rules on the gryffindor +# subject (and deliberately NO ingress section). Per the API contract — "CNPs with +# no ingress rules do not affect ingress traffic" — the subject's ingress must remain +# fully open even though egress to slytherin is denied. +# Used by CNPAdminTierEgressRulesIngressUnaffected (issue #103). +apiVersion: policy.networking.k8s.io/v1alpha2 +kind: ClusterNetworkPolicy +metadata: + name: admin-egress-isolation +spec: + tier: Admin + priority: 20 + subject: + namespaces: + matchLabels: + kubernetes.io/metadata.name: network-policy-conformance-gryffindor + egress: + - name: "deny-to-slytherin" + action: "Deny" + to: + - namespaces: + matchLabels: + kubernetes.io/metadata.name: network-policy-conformance-slytherin diff --git a/conformance/base/admin_tier/standard-ingress-isolation.yaml b/conformance/base/admin_tier/standard-ingress-isolation.yaml new file mode 100644 index 00000000..ed54d9b2 --- /dev/null +++ b/conformance/base/admin_tier/standard-ingress-isolation.yaml @@ -0,0 +1,23 @@ +# An Admin-tier ClusterNetworkPolicy with ONLY ingress rules on the gryffindor +# subject (and deliberately NO egress section). Per the API contract — "CNPs with +# no egress rules do not affect egress traffic" — the subject's egress must remain +# fully open even though ingress from slytherin is denied. +# Used by CNPAdminTierIngressRulesEgressUnaffected (issue #103). +apiVersion: policy.networking.k8s.io/v1alpha2 +kind: ClusterNetworkPolicy +metadata: + name: admin-ingress-isolation +spec: + tier: Admin + priority: 20 + subject: + namespaces: + matchLabels: + kubernetes.io/metadata.name: network-policy-conformance-gryffindor + ingress: + - name: "deny-from-slytherin" + action: "Deny" + from: + - namespaces: + matchLabels: + kubernetes.io/metadata.name: network-policy-conformance-slytherin diff --git a/conformance/base/baseline_tier/standard-egress-isolation.yaml b/conformance/base/baseline_tier/standard-egress-isolation.yaml new file mode 100644 index 00000000..d821d2b9 --- /dev/null +++ b/conformance/base/baseline_tier/standard-egress-isolation.yaml @@ -0,0 +1,23 @@ +# A Baseline-tier ClusterNetworkPolicy with ONLY egress rules on the gryffindor +# subject (and deliberately NO ingress section). Per the API contract — "CNPs with +# no ingress rules do not affect ingress traffic" — the subject's ingress must remain +# fully open even though egress to slytherin is denied. +# Baseline-tier sibling; used by CNPBaselineTierEgressRulesIngressUnaffected (issue #103). +apiVersion: policy.networking.k8s.io/v1alpha2 +kind: ClusterNetworkPolicy +metadata: + name: baseline-egress-isolation +spec: + tier: Baseline + priority: 20 + subject: + namespaces: + matchLabels: + kubernetes.io/metadata.name: network-policy-conformance-gryffindor + egress: + - name: "deny-to-slytherin" + action: "Deny" + to: + - namespaces: + matchLabels: + kubernetes.io/metadata.name: network-policy-conformance-slytherin diff --git a/conformance/base/baseline_tier/standard-ingress-isolation.yaml b/conformance/base/baseline_tier/standard-ingress-isolation.yaml new file mode 100644 index 00000000..ed9e7073 --- /dev/null +++ b/conformance/base/baseline_tier/standard-ingress-isolation.yaml @@ -0,0 +1,23 @@ +# A Baseline-tier ClusterNetworkPolicy with ONLY ingress rules on the gryffindor +# subject (and deliberately NO egress section). Per the API contract — "CNPs with +# no egress rules do not affect egress traffic" — the subject's egress must remain +# fully open even though ingress from slytherin is denied. +# Baseline-tier sibling; used by CNPBaselineTierIngressRulesEgressUnaffected (issue #103). +apiVersion: policy.networking.k8s.io/v1alpha2 +kind: ClusterNetworkPolicy +metadata: + name: baseline-ingress-isolation +spec: + tier: Baseline + priority: 20 + subject: + namespaces: + matchLabels: + kubernetes.io/metadata.name: network-policy-conformance-gryffindor + ingress: + - name: "deny-from-slytherin" + action: "Deny" + from: + - namespaces: + matchLabels: + kubernetes.io/metadata.name: network-policy-conformance-slytherin diff --git a/conformance/tests/admin-network-policy-standard-ingress-egress-isolation.go b/conformance/tests/admin-network-policy-standard-ingress-egress-isolation.go new file mode 100644 index 00000000..c7926e47 --- /dev/null +++ b/conformance/tests/admin-network-policy-standard-ingress-egress-isolation.go @@ -0,0 +1,112 @@ +/* +Copyright 2025 The Kubernetes 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 tests + +import ( + "testing" + + "sigs.k8s.io/network-policy-api/conformance/utils/kubernetes" + "sigs.k8s.io/network-policy-api/conformance/utils/suite" +) + +func init() { + ConformanceTests = append(ConformanceTests, + CNPAdminTierIngressRulesEgressUnaffected, + CNPAdminTierEgressRulesIngressUnaffected, + ) +} + +// CNPAdminTierIngressRulesEgressUnaffected verifies that an Admin-tier +// ClusterNetworkPolicy which declares only ingress rules has no side-effect on the +// subject's egress. The v1alpha2 API contract states "CNPs with no egress rules do +// not affect egress traffic", so even though ingress from slytherin is denied, the +// gryffindor subject must still egress freely. The existing ingress-rule tests only +// probe traffic *toward* the subject and never check this. See issue +// kubernetes-sigs/network-policy-api#103. +var CNPAdminTierIngressRulesEgressUnaffected = suite.ConformanceTest{ + ShortName: "CNPAdminTierIngressRulesEgressUnaffected", + Description: "An Admin CNP with only ingress rules must not affect the subject's egress", + Features: []suite.SupportedFeature{ + suite.SupportClusterNetworkPolicy, + }, + Manifests: []string{"base/admin_tier/standard-ingress-isolation.yaml"}, + Test: func(t *testing.T, s *suite.ConformanceTestSuite) { + + t.Run("Should enforce the ingress rule: deny ingress from slytherin to gryffindor", func(t *testing.T) { + // Positive control: confirms the ingress-only policy is actually active, + // so the egress checks below are meaningful (not just an inert policy). + serverPod := kubernetes.GetPod(t, s.Client, "network-policy-conformance-gryffindor", "harry-potter-0", s.TimeoutConfig.GetTimeout) + kubernetes.PokeServer(t, s.ClientSet, &s.KubeConfig, "network-policy-conformance-slytherin", "draco-malfoy-0", "tcp", + serverPod.Status.PodIP, int32(80), s.TimeoutConfig, false) + }) + + t.Run("Should not affect egress: gryffindor can still egress to ravenclaw", func(t *testing.T) { + // ravenclaw is not referenced by the policy, so the reply path + // (ravenclaw->gryffindor ingress) is unaffected and cannot confound this + // egress check. With zero egress rules, this egress must be allowed. + serverPod := kubernetes.GetPod(t, s.Client, "network-policy-conformance-ravenclaw", "luna-lovegood-0", s.TimeoutConfig.GetTimeout) + kubernetes.PokeServer(t, s.ClientSet, &s.KubeConfig, "network-policy-conformance-gryffindor", "harry-potter-0", "tcp", + serverPod.Status.PodIP, int32(80), s.TimeoutConfig, true) + }) + + t.Run("Should not affect egress: gryffindor can still egress to hufflepuff", func(t *testing.T) { + serverPod := kubernetes.GetPod(t, s.Client, "network-policy-conformance-hufflepuff", "cedric-diggory-0", s.TimeoutConfig.GetTimeout) + kubernetes.PokeServer(t, s.ClientSet, &s.KubeConfig, "network-policy-conformance-gryffindor", "harry-potter-0", "tcp", + serverPod.Status.PodIP, int32(80), s.TimeoutConfig, true) + }) + }, +} + +// CNPAdminTierEgressRulesIngressUnaffected verifies that an Admin-tier +// ClusterNetworkPolicy which declares only egress rules has no side-effect on the +// subject's ingress. The v1alpha2 API contract states "CNPs with no ingress rules do +// not affect ingress traffic", so even though egress to slytherin is denied, other +// pods must still reach the gryffindor subject. The existing egress-rule tests only +// probe traffic *from* the subject and never check this. See issue +// kubernetes-sigs/network-policy-api#103. +var CNPAdminTierEgressRulesIngressUnaffected = suite.ConformanceTest{ + ShortName: "CNPAdminTierEgressRulesIngressUnaffected", + Description: "An Admin CNP with only egress rules must not affect the subject's ingress", + Features: []suite.SupportedFeature{ + suite.SupportClusterNetworkPolicy, + }, + Manifests: []string{"base/admin_tier/standard-egress-isolation.yaml"}, + Test: func(t *testing.T, s *suite.ConformanceTestSuite) { + + t.Run("Should enforce the egress rule: deny egress from gryffindor to slytherin", func(t *testing.T) { + // Positive control: confirms the egress-only policy is actually active. + serverPod := kubernetes.GetPod(t, s.Client, "network-policy-conformance-slytherin", "draco-malfoy-0", s.TimeoutConfig.GetTimeout) + kubernetes.PokeServer(t, s.ClientSet, &s.KubeConfig, "network-policy-conformance-gryffindor", "harry-potter-0", "tcp", + serverPod.Status.PodIP, int32(80), s.TimeoutConfig, false) + }) + + t.Run("Should not affect ingress: ravenclaw can still reach gryffindor", func(t *testing.T) { + // ravenclaw is not referenced by the policy, so the reply path + // (gryffindor->ravenclaw egress) is unaffected and cannot confound this + // ingress check. With zero ingress rules, this ingress must be allowed. + serverPod := kubernetes.GetPod(t, s.Client, "network-policy-conformance-gryffindor", "harry-potter-0", s.TimeoutConfig.GetTimeout) + kubernetes.PokeServer(t, s.ClientSet, &s.KubeConfig, "network-policy-conformance-ravenclaw", "luna-lovegood-0", "tcp", + serverPod.Status.PodIP, int32(80), s.TimeoutConfig, true) + }) + + t.Run("Should not affect ingress: hufflepuff can still reach gryffindor", func(t *testing.T) { + serverPod := kubernetes.GetPod(t, s.Client, "network-policy-conformance-gryffindor", "harry-potter-0", s.TimeoutConfig.GetTimeout) + kubernetes.PokeServer(t, s.ClientSet, &s.KubeConfig, "network-policy-conformance-hufflepuff", "cedric-diggory-0", "tcp", + serverPod.Status.PodIP, int32(80), s.TimeoutConfig, true) + }) + }, +} diff --git a/conformance/tests/baseline-admin-network-policy-standard-ingress-egress-isolation.go b/conformance/tests/baseline-admin-network-policy-standard-ingress-egress-isolation.go new file mode 100644 index 00000000..61a86527 --- /dev/null +++ b/conformance/tests/baseline-admin-network-policy-standard-ingress-egress-isolation.go @@ -0,0 +1,106 @@ +/* +Copyright 2025 The Kubernetes 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 tests + +import ( + "testing" + + "sigs.k8s.io/network-policy-api/conformance/utils/kubernetes" + "sigs.k8s.io/network-policy-api/conformance/utils/suite" +) + +func init() { + ConformanceTests = append(ConformanceTests, + CNPBaselineTierIngressRulesEgressUnaffected, + CNPBaselineTierEgressRulesIngressUnaffected, + ) +} + +// CNPBaselineTierIngressRulesEgressUnaffected is the Baseline-tier sibling of +// CNPAdminTierIngressRulesEgressUnaffected: a Baseline-tier ClusterNetworkPolicy that +// declares only ingress rules must not affect the subject's egress ("CNPs with no +// egress rules do not affect egress traffic"). This verifies the directional +// isolation guarantee holds in the Baseline code path too. See issue +// kubernetes-sigs/network-policy-api#103. +var CNPBaselineTierIngressRulesEgressUnaffected = suite.ConformanceTest{ + ShortName: "CNPBaselineTierIngressRulesEgressUnaffected", + Description: "A Baseline CNP with only ingress rules must not affect the subject's egress", + Features: []suite.SupportedFeature{ + suite.SupportClusterNetworkPolicy, + }, + Manifests: []string{"base/baseline_tier/standard-ingress-isolation.yaml"}, + Test: func(t *testing.T, s *suite.ConformanceTestSuite) { + + t.Run("Should enforce the ingress rule: deny ingress from slytherin to gryffindor", func(t *testing.T) { + // Positive control: confirms the ingress-only baseline policy is active. + serverPod := kubernetes.GetPod(t, s.Client, "network-policy-conformance-gryffindor", "harry-potter-0", s.TimeoutConfig.GetTimeout) + kubernetes.PokeServer(t, s.ClientSet, &s.KubeConfig, "network-policy-conformance-slytherin", "draco-malfoy-0", "tcp", + serverPod.Status.PodIP, int32(80), s.TimeoutConfig, false) + }) + + t.Run("Should not affect egress: gryffindor can still egress to ravenclaw", func(t *testing.T) { + // ravenclaw is not referenced by the policy, so its reply path cannot + // confound this check. With zero egress rules, this egress must be allowed. + serverPod := kubernetes.GetPod(t, s.Client, "network-policy-conformance-ravenclaw", "luna-lovegood-0", s.TimeoutConfig.GetTimeout) + kubernetes.PokeServer(t, s.ClientSet, &s.KubeConfig, "network-policy-conformance-gryffindor", "harry-potter-0", "tcp", + serverPod.Status.PodIP, int32(80), s.TimeoutConfig, true) + }) + + t.Run("Should not affect egress: gryffindor can still egress to hufflepuff", func(t *testing.T) { + serverPod := kubernetes.GetPod(t, s.Client, "network-policy-conformance-hufflepuff", "cedric-diggory-0", s.TimeoutConfig.GetTimeout) + kubernetes.PokeServer(t, s.ClientSet, &s.KubeConfig, "network-policy-conformance-gryffindor", "harry-potter-0", "tcp", + serverPod.Status.PodIP, int32(80), s.TimeoutConfig, true) + }) + }, +} + +// CNPBaselineTierEgressRulesIngressUnaffected is the Baseline-tier sibling of +// CNPAdminTierEgressRulesIngressUnaffected: a Baseline-tier ClusterNetworkPolicy that +// declares only egress rules must not affect the subject's ingress ("CNPs with no +// ingress rules do not affect ingress traffic"). See issue +// kubernetes-sigs/network-policy-api#103. +var CNPBaselineTierEgressRulesIngressUnaffected = suite.ConformanceTest{ + ShortName: "CNPBaselineTierEgressRulesIngressUnaffected", + Description: "A Baseline CNP with only egress rules must not affect the subject's ingress", + Features: []suite.SupportedFeature{ + suite.SupportClusterNetworkPolicy, + }, + Manifests: []string{"base/baseline_tier/standard-egress-isolation.yaml"}, + Test: func(t *testing.T, s *suite.ConformanceTestSuite) { + + t.Run("Should enforce the egress rule: deny egress from gryffindor to slytherin", func(t *testing.T) { + // Positive control: confirms the egress-only baseline policy is active. + serverPod := kubernetes.GetPod(t, s.Client, "network-policy-conformance-slytherin", "draco-malfoy-0", s.TimeoutConfig.GetTimeout) + kubernetes.PokeServer(t, s.ClientSet, &s.KubeConfig, "network-policy-conformance-gryffindor", "harry-potter-0", "tcp", + serverPod.Status.PodIP, int32(80), s.TimeoutConfig, false) + }) + + t.Run("Should not affect ingress: ravenclaw can still reach gryffindor", func(t *testing.T) { + // ravenclaw is not referenced by the policy, so its reply path cannot + // confound this check. With zero ingress rules, this ingress must be allowed. + serverPod := kubernetes.GetPod(t, s.Client, "network-policy-conformance-gryffindor", "harry-potter-0", s.TimeoutConfig.GetTimeout) + kubernetes.PokeServer(t, s.ClientSet, &s.KubeConfig, "network-policy-conformance-ravenclaw", "luna-lovegood-0", "tcp", + serverPod.Status.PodIP, int32(80), s.TimeoutConfig, true) + }) + + t.Run("Should not affect ingress: hufflepuff can still reach gryffindor", func(t *testing.T) { + serverPod := kubernetes.GetPod(t, s.Client, "network-policy-conformance-gryffindor", "harry-potter-0", s.TimeoutConfig.GetTimeout) + kubernetes.PokeServer(t, s.ClientSet, &s.KubeConfig, "network-policy-conformance-hufflepuff", "cedric-diggory-0", "tcp", + serverPod.Status.PodIP, int32(80), s.TimeoutConfig, true) + }) + }, +}