Skip to content

Commit ec22c0a

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

File tree

1 file changed

+336
-0
lines changed

1 file changed

+336
-0
lines changed

test/e2e/network_policy_test.go

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

0 commit comments

Comments
 (0)