Skip to content

Commit 7506ef0

Browse files
committed
OPRUN-3873: Add e2e tests for NetworkPolicies
1 parent 8f81c23 commit 7506ef0

File tree

1 file changed

+342
-0
lines changed

1 file changed

+342
-0
lines changed

test/e2e/network_policy_test.go

Lines changed: 342 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,342 @@
1+
package e2e
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
"testing"
8+
9+
"github.com/operator-framework/operator-controller/test/utils"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
corev1 "k8s.io/api/core/v1"
13+
networkingv1 "k8s.io/api/networking/v1"
14+
"k8s.io/apimachinery/pkg/api/equality"
15+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
16+
"k8s.io/apimachinery/pkg/util/intstr"
17+
"k8s.io/utils/ptr"
18+
"sigs.k8s.io/controller-runtime/pkg/client"
19+
)
20+
21+
const (
22+
minJustificationLength = 40
23+
catalogdManagerSelector = "control-plane=catalogd-controller-manager"
24+
operatorManagerSelector = "control-plane=operator-controller-controller-manager"
25+
catalogdMetricsPort = 7443
26+
catalogdWebhookPort = 9443
27+
catalogServerPort = 8443
28+
operatorControllerMetricsPort = 8443
29+
)
30+
31+
type portWithJustification struct {
32+
port []networkingv1.NetworkPolicyPort
33+
justification string
34+
}
35+
36+
// ingressRule defines a k8s IngressRule, along with a justification.
37+
type ingressRule struct {
38+
ports []portWithJustification
39+
from []networkingv1.NetworkPolicyPeer
40+
}
41+
42+
// egressRule defines a k8s egressRule, along with a justification.
43+
type egressRule struct {
44+
ports []portWithJustification
45+
to []networkingv1.NetworkPolicyPeer
46+
}
47+
48+
// AllowedPolicyDefinition defines the expected structure and justifications for a NetworkPolicy.
49+
type allowedPolicyDefinition struct {
50+
selector metav1.LabelSelector
51+
policyTypes []networkingv1.PolicyType
52+
ingressRule ingressRule
53+
egressRule egressRule
54+
denyAllIngressJustification string // Justification if Ingress is in PolicyTypes and IngressRules is empty
55+
denyAllEgressJustification string // Justification if Egress is in PolicyTypes and EgressRules is empty
56+
}
57+
58+
// Ref: https://docs.google.com/document/d/1bHEEWzA65u-kjJFQRUY1iBuMIIM1HbPy4MeDLX4NI3o/edit?usp=sharing
59+
var allowedNetworkPolicies = map[string]allowedPolicyDefinition{
60+
"catalogd-controller-manager": {
61+
selector: metav1.LabelSelector{MatchLabels: map[string]string{"control-plane": "catalogd-controller-manager"}},
62+
policyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress},
63+
ingressRule: ingressRule{
64+
ports: []portWithJustification{
65+
{
66+
port: []networkingv1.NetworkPolicyPort{{Protocol: ptr.To(corev1.ProtocolTCP), Port: intOrStrPtr(catalogdMetricsPort)}},
67+
justification: "Allows Prometheus to scrape metrics from catalogd, which is essential for monitoring its performance and health.",
68+
},
69+
{
70+
port: []networkingv1.NetworkPolicyPort{{Protocol: ptr.To(corev1.ProtocolTCP), Port: intOrStrPtr(catalogdWebhookPort)}},
71+
justification: "Permits Kubernetes API server to reach catalogd's mutating admission webhook, ensuring integrity of catalog resources.",
72+
},
73+
{
74+
port: []networkingv1.NetworkPolicyPort{{Protocol: ptr.To(corev1.ProtocolTCP), Port: intOrStrPtr(catalogServerPort)}},
75+
justification: "Enables clients (eg. operator-controller) to query catalog metadata from catalogd, which is a core function for bundle resolution and operator discovery.",
76+
},
77+
},
78+
},
79+
egressRule: egressRule{
80+
ports: []portWithJustification{
81+
{
82+
port: nil, // Empty Ports means allow all egress
83+
justification: "Permits catalogd to fetch catalog images from arbitrary container registries and communicate with the Kubernetes API server for its operational needs.",
84+
},
85+
},
86+
},
87+
},
88+
"operator-controller-controller-manager": {
89+
selector: metav1.LabelSelector{MatchLabels: map[string]string{"control-plane": "operator-controller-controller-manager"}},
90+
policyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress},
91+
ingressRule: ingressRule{
92+
ports: []portWithJustification{
93+
{
94+
port: []networkingv1.NetworkPolicyPort{{Protocol: ptr.To(corev1.ProtocolTCP), Port: intOrStrPtr(operatorControllerMetricsPort)}},
95+
justification: "Allows Prometheus to scrape metrics from operator-controller, which is crucial for monitoring its activity, reconciliations, and overall health.",
96+
},
97+
},
98+
},
99+
egressRule: egressRule{
100+
ports: []portWithJustification{
101+
{
102+
port: nil, // Empty Ports means allow all egress
103+
justification: "Enables operator-controller to pull bundle images from arbitrary image registries, connect to catalogd's HTTPS server for metadata, and interact with the Kubernetes API server.",
104+
},
105+
},
106+
},
107+
},
108+
"default-deny-all-traffic": {
109+
selector: metav1.LabelSelector{}, // Empty selector, matches all pods
110+
policyTypes: []networkingv1.PolicyType{networkingv1.PolicyTypeIngress, networkingv1.PolicyTypeEgress},
111+
// No IngressRules means deny all ingress if PolicyTypeIngress is present
112+
// No EgressRules means deny all egress if PolicyTypeEgress is present
113+
denyAllIngressJustification: "Denies all ingress traffic to pods selected by this policy by default, unless explicitly allowed by other policy rules, ensuring a baseline secure posture.",
114+
denyAllEgressJustification: "Denies all egress traffic from pods selected by this policy by default, unless explicitly allowed by other policy rules, minimizing potential exfiltration paths.",
115+
},
116+
}
117+
118+
func TestNetworkPolicyJustifications(t *testing.T) {
119+
ctx := context.Background()
120+
121+
// Validate justifications have min length in the allowedNetworkPolicies definition
122+
for name, policyDef := range allowedNetworkPolicies {
123+
for i, pwj := range policyDef.ingressRule.ports {
124+
assert.GreaterOrEqualf(t, len(pwj.justification), minJustificationLength,
125+
"Justification for ingress PortWithJustification entry %d in policy %q is too short: %q", i, name, pwj.justification)
126+
}
127+
for i, pwj := range policyDef.egressRule.ports { // Corrected variable name from 'rule' to 'pwj'
128+
assert.GreaterOrEqualf(t, len(pwj.justification), minJustificationLength,
129+
"Justification for egress PortWithJustification entry %d in policy %q is too short: %q", i, name, pwj.justification)
130+
}
131+
if policyDef.denyAllIngressJustification != "" {
132+
assert.GreaterOrEqualf(t, len(policyDef.denyAllIngressJustification), minJustificationLength,
133+
"DenyAllIngressJustification for policy %q is too short: %q", name, policyDef.denyAllIngressJustification)
134+
}
135+
if policyDef.denyAllEgressJustification != "" {
136+
assert.GreaterOrEqualf(t, len(policyDef.denyAllEgressJustification), minJustificationLength,
137+
"DenyAllEgressJustification for policy %q is too short: %q", name, policyDef.denyAllEgressJustification)
138+
}
139+
}
140+
141+
clientForComponent := utils.FindK8sClient(t)
142+
componentNamespace := getComponentNamespace(t, clientForComponent, operatorManagerSelector)
143+
clusterPolicies := &networkingv1.NetworkPolicyList{}
144+
err := c.List(ctx, clusterPolicies, client.InNamespace(componentNamespace))
145+
require.NoError(t, err, "Failed to list NetworkPolicies in namespace %q", componentNamespace)
146+
147+
validatedRegistryPolicies := make(map[string]bool)
148+
149+
for _, policy := range clusterPolicies.Items {
150+
t.Run(fmt.Sprintf("Policy_%s", strings.ReplaceAll(policy.Name, "-", "_")), func(t *testing.T) {
151+
expectedPolicy, found := allowedNetworkPolicies[policy.Name]
152+
require.Truef(t, found, "NetworkPolicy %q found in cluster but not in allowed registry. Namespace: %s", policy.Name, policy.Namespace)
153+
validatedRegistryPolicies[policy.Name] = true
154+
155+
// 1. Compare PodSelector
156+
assert.True(t, equality.Semantic.DeepEqual(expectedPolicy.selector, policy.Spec.PodSelector),
157+
"PodSelector mismatch for policy %q. Expected: %+v, Got: %+v", policy.Name, expectedPolicy.selector, policy.Spec.PodSelector)
158+
159+
// 2. Compare PolicyTypes
160+
require.ElementsMatchf(t, expectedPolicy.policyTypes, policy.Spec.PolicyTypes,
161+
"PolicyTypes mismatch for policy %q.", policy.Name)
162+
163+
// 3. Validate Ingress Rules
164+
hasIngressPolicyType := false
165+
for _, pt := range policy.Spec.PolicyTypes {
166+
if pt == networkingv1.PolicyTypeIngress {
167+
hasIngressPolicyType = true
168+
break
169+
}
170+
}
171+
172+
if hasIngressPolicyType {
173+
switch len(policy.Spec.Ingress) {
174+
case 0:
175+
validateDenyAllIngress(t, policy.Name, expectedPolicy)
176+
case 1:
177+
validateSingleIngressRule(t, policy.Name, policy.Spec.Ingress[0], expectedPolicy)
178+
default:
179+
assert.Failf(t, "Policy %q in cluster has %d ingress rules. Allowed definition supports at most 1 explicit ingress rule.", policy.Name, len(policy.Spec.Ingress))
180+
}
181+
} else {
182+
validateNoIngress(t, policy.Name, policy, expectedPolicy)
183+
}
184+
185+
// 4. Validate Egress Rules
186+
hasEgressPolicyType := false
187+
for _, pt := range policy.Spec.PolicyTypes {
188+
if pt == networkingv1.PolicyTypeEgress {
189+
hasEgressPolicyType = true
190+
break
191+
}
192+
}
193+
194+
if hasEgressPolicyType {
195+
switch len(policy.Spec.Egress) {
196+
case 0:
197+
validateDenyAllEgress(t, policy.Name, expectedPolicy)
198+
case 1:
199+
validateSingleEgressRule(t, policy.Name, policy.Spec.Egress[0], expectedPolicy)
200+
default:
201+
assert.Failf(t, "Policy %q in cluster has %d egress rules. Allowed definition supports at most 1 explicit egress rule.", policy.Name, len(policy.Spec.Egress))
202+
}
203+
} else {
204+
validateNoEgress(t, policy, expectedPolicy)
205+
}
206+
})
207+
}
208+
209+
// 5. Ensure all policies in the registry were found in the cluster
210+
assert.Equal(t, len(allowedNetworkPolicies), len(validatedRegistryPolicies),
211+
"Mismatch between number of expected policies in registry (%d) and number of policies found & validated in cluster (%d). Missing policies from registry: %v", len(allowedNetworkPolicies), len(validatedRegistryPolicies), missingPolicies(allowedNetworkPolicies, validatedRegistryPolicies))
212+
}
213+
214+
// Helper function to create a pointer to intstr.IntOrString from an int
215+
func intOrStrPtr(port int32) *intstr.IntOrString {
216+
val := intstr.FromInt(int(port))
217+
return &val
218+
}
219+
220+
func missingPolicies(expected map[string]allowedPolicyDefinition, actual map[string]bool) []string {
221+
missing := []string{}
222+
for k := range expected {
223+
if !actual[k] {
224+
missing = append(missing, k)
225+
}
226+
}
227+
return missing
228+
}
229+
230+
// validateNoEgress confirms that a policy which does not have spec.PolicyType=Egress specified
231+
// has no corresponding egress rules or expectations defined.
232+
func validateNoEgress(t *testing.T, policy networkingv1.NetworkPolicy, expectedPolicy allowedPolicyDefinition) {
233+
// Policy is NOT expected to affect Egress traffic (no Egress in PolicyTypes)
234+
// Expected: Cluster has no egress rules; Registry has no DenyAllEgressJustification and empty EgressRule.
235+
require.Emptyf(t, policy.Spec.Egress,
236+
"Policy %q: Cluster does not have Egress PolicyType, but has Egress rules defined.", policy.Name)
237+
require.Emptyf(t, expectedPolicy.denyAllEgressJustification,
238+
"Policy %q: Cluster does not have Egress PolicyType. Registry's DenyAllEgressJustification is not empty.", policy.Name)
239+
require.Emptyf(t, expectedPolicy.egressRule.ports,
240+
"Policy %q: Cluster does not have Egress PolicyType. Registry's EgressRule.Ports is not empty.", policy.Name)
241+
require.Emptyf(t, expectedPolicy.egressRule.to,
242+
"Policy %q: Cluster does not have Egress PolicyType. Registry's EgressRule.To is not empty.", policy.Name)
243+
}
244+
245+
// validateDenyAllEgress confirms that a policy with Egress PolicyType but no explicit rules
246+
// correctly corresponds to a "deny all" expectation.
247+
func validateDenyAllEgress(t *testing.T, policyName string, expectedPolicy allowedPolicyDefinition) {
248+
// Cluster: PolicyType Egress is present, but no explicit egress rules -> Deny All Egress by this policy.
249+
// Expected: DenyAllEgressJustification is set; EgressRule.Ports and .To are empty.
250+
require.NotEmptyf(t, expectedPolicy.denyAllEgressJustification,
251+
"Policy %q: Cluster has Egress PolicyType but no rules (deny all). Registry's DenyAllEgressJustification is empty.", policyName)
252+
require.Emptyf(t, expectedPolicy.egressRule.ports,
253+
"Policy %q: Cluster has Egress PolicyType but no rules (deny all). Registry's EgressRule.Ports is not empty.", policyName)
254+
require.Emptyf(t, expectedPolicy.egressRule.to,
255+
"Policy %q: Cluster has Egress PolicyType but no rules (deny all). Registry's EgressRule.To is not empty.", policyName)
256+
}
257+
258+
// validateSingleEgressRule validates a policy that has exactly one explicit egress rule,
259+
// distinguishing between "allow-all" and more specific rules.
260+
func validateSingleEgressRule(t *testing.T, policyName string, clusterEgressRule networkingv1.NetworkPolicyEgressRule, expectedPolicy allowedPolicyDefinition) {
261+
// Cluster: PolicyType Egress is present, and there's one explicit egress rule.
262+
// Expected: DenyAllEgressJustification is empty; EgressRule matches the cluster's rule.
263+
expectedEgressRule := expectedPolicy.egressRule
264+
265+
require.Emptyf(t, expectedPolicy.denyAllEgressJustification,
266+
"Policy %q: Cluster has a specific Egress rule. Registry's DenyAllEgressJustification should be empty.", policyName)
267+
268+
isClusterRuleAllowAllPorts := len(clusterEgressRule.Ports) == 0
269+
isClusterRuleAllowAllPeers := len(clusterEgressRule.To) == 0
270+
271+
if isClusterRuleAllowAllPorts && isClusterRuleAllowAllPeers { // Handles egress: [{}] - allow all ports to all peers
272+
require.Lenf(t, expectedEgressRule.ports, 1,
273+
"Policy %q (allow-all egress): Expected EgressRule.Ports to have 1 justification entry, got %d", policyName, len(expectedEgressRule.ports))
274+
if len(expectedEgressRule.ports) == 1 { // Guard against panic
275+
assert.Nilf(t, expectedEgressRule.ports[0].port,
276+
"Policy %q (allow-all egress): Expected EgressRule.Ports[0].Port to be nil, got %+v", policyName, expectedEgressRule.ports[0].port)
277+
}
278+
assert.Conditionf(t, func() bool { return len(expectedEgressRule.to) == 0 },
279+
"Policy %q (allow-all egress): Expected EgressRule.To to be empty for allow-all peers, got %+v", policyName, expectedEgressRule.to)
280+
} else {
281+
// Specific egress rule (not the simple allow-all ports and allow-all peers)
282+
assert.True(t, equality.Semantic.DeepEqual(expectedEgressRule.to, clusterEgressRule.To),
283+
"Policy %q, Egress Rule: 'To' mismatch.\nExpected: %+v\nGot: %+v", policyName, expectedEgressRule.to, clusterEgressRule.To)
284+
285+
var allExpectedPortsFromPwJ []networkingv1.NetworkPolicyPort
286+
for _, pwj := range expectedEgressRule.ports {
287+
allExpectedPortsFromPwJ = append(allExpectedPortsFromPwJ, pwj.port...)
288+
}
289+
require.ElementsMatchf(t, allExpectedPortsFromPwJ, clusterEgressRule.Ports,
290+
"Policy %q, Egress Rule: 'Ports' mismatch (aggregated from PortWithJustification). Expected: %+v, Got: %+v", policyName, allExpectedPortsFromPwJ, clusterEgressRule.Ports)
291+
}
292+
}
293+
294+
// validateNoIngress confirms that a policy which does not have the Ingress PolicyType
295+
// has no corresponding ingress rules or expectations defined.
296+
func validateNoIngress(t *testing.T, policyName string, clusterPolicy networkingv1.NetworkPolicy, expectedPolicy allowedPolicyDefinition) {
297+
// Policy is NOT expected to affect Ingress traffic (no Ingress in PolicyTypes)
298+
// Expected: Cluster has no ingress rules; Registry has no DenyAllIngressJustification and empty IngressRule.
299+
require.Emptyf(t, clusterPolicy.Spec.Ingress,
300+
"Policy %q: Cluster does not have Ingress PolicyType, but has Ingress rules defined.", policyName)
301+
require.Emptyf(t, expectedPolicy.denyAllIngressJustification,
302+
"Policy %q: Cluster does not have Ingress PolicyType. Registry's DenyAllIngressJustification is not empty.", policyName)
303+
require.Emptyf(t, expectedPolicy.ingressRule.ports,
304+
"Policy %q: Cluster does not have Ingress PolicyType. Registry's IngressRule.Ports is not empty.", policyName)
305+
require.Emptyf(t, expectedPolicy.ingressRule.from,
306+
"Policy %q: Cluster does not have Ingress PolicyType. Registry's IngressRule.From is not empty.", policyName)
307+
}
308+
309+
// validateDenyAllIngress confirms that a policy with Ingress PolicyType but no explicit rules
310+
// correctly corresponds to a "deny all" expectation.
311+
func validateDenyAllIngress(t *testing.T, policyName string, expectedPolicy allowedPolicyDefinition) {
312+
// Cluster: PolicyType Ingress is present, but no explicit ingress rules -> Deny All Ingress by this policy.
313+
// Expected: DenyAllIngressJustification is set; IngressRule.Ports and .From are empty.
314+
require.NotEmptyf(t, expectedPolicy.denyAllIngressJustification,
315+
"Policy %q: Cluster has Ingress PolicyType but no rules (deny all). Registry's DenyAllIngressJustification is empty.", policyName)
316+
require.Emptyf(t, expectedPolicy.ingressRule.ports,
317+
"Policy %q: Cluster has Ingress PolicyType but no rules (deny all). Registry's IngressRule.Ports is not empty.", policyName)
318+
require.Emptyf(t, expectedPolicy.ingressRule.from,
319+
"Policy %q: Cluster has Ingress PolicyType but no rules (deny all). Registry's IngressRule.From is not empty.", policyName)
320+
}
321+
322+
// validateSingleIngressRule validates a policy that has exactly one explicit ingress rule.
323+
func validateSingleIngressRule(t *testing.T, policyName string, clusterIngressRule networkingv1.NetworkPolicyIngressRule, expectedPolicy allowedPolicyDefinition) {
324+
// Cluster: PolicyType Ingress is present, and there's one explicit ingress rule.
325+
// Expected: DenyAllIngressJustification is empty; IngressRule matches the cluster's rule.
326+
expectedIngressRule := expectedPolicy.ingressRule
327+
328+
require.Emptyf(t, expectedPolicy.denyAllIngressJustification,
329+
"Policy %q: Cluster has a specific Ingress rule. Registry's DenyAllIngressJustification should be empty.", policyName)
330+
331+
// Compare 'From'
332+
assert.True(t, equality.Semantic.DeepEqual(expectedIngressRule.from, clusterIngressRule.From),
333+
"Policy %q, Ingress Rule: 'From' mismatch.\nExpected: %+v\nGot: %+v", policyName, expectedIngressRule.from, clusterIngressRule.From)
334+
335+
// Compare 'Ports' by aggregating the ports from our justified structure
336+
var allExpectedPortsFromPwJ []networkingv1.NetworkPolicyPort
337+
for _, pwj := range expectedIngressRule.ports {
338+
allExpectedPortsFromPwJ = append(allExpectedPortsFromPwJ, pwj.port...)
339+
}
340+
require.ElementsMatchf(t, allExpectedPortsFromPwJ, clusterIngressRule.Ports,
341+
"Policy %q, Ingress Rule: 'Ports' mismatch (aggregated from PortWithJustification). Expected: %+v, Got: %+v", policyName, allExpectedPortsFromPwJ, clusterIngressRule.Ports)
342+
}

0 commit comments

Comments
 (0)