feat: Add NodePort namespace range policy#713
feat: Add NodePort namespace range policy#713elhamafzalizadeh wants to merge 5 commits intoopen-policy-agent:masterfrom
Conversation
JaydipGabani
left a comment
There was a problem hiding this comment.
@elhamafzalizadeh you will need to restructure and add the policy here - https://github.com/open-policy-agent/gatekeeper-library/tree/master/src/general/allowedreposv2, while keeping the examples and suits in the library/general. Please follow the structure similar to other policies.
There was a problem hiding this comment.
Pull request overview
This PR adds a new Gatekeeper policy that restricts NodePort assignments based on namespace patterns. The policy enforces that services in specific namespaces can only use NodePort values within configured ranges, preventing port conflicts across team boundaries.
- Implements a ConstraintTemplate with Rego logic to validate NodePort ranges against namespace patterns
- Provides test suite with allowed and disallowed examples demonstrating the policy behavior
- Integrates the new policy into the library's kustomization structure
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| library/general/kustomization.yaml | Adds reference to the new enforce-nodeport-range policy |
| library/general/enforce-nodeport-range/template.yaml | Defines the K8sNodePortNamespaceRange ConstraintTemplate with Rego logic for validating NodePort values against namespace-specific ranges |
| library/general/enforce-nodeport-range/suite.yaml | Configures test suite with allowed and disallowed test cases |
| library/general/enforce-nodeport-range/samples/enforce-nodeport-range/constraint.yaml | Provides example constraint configuration with team-a and team-b namespace patterns and port ranges |
| library/general/enforce-nodeport-range/samples/enforce-nodeport-range/example_allowed.yaml | Sample Service in team-a-service namespace with port 30007 that should be allowed |
| library/general/enforce-nodeport-range/samples/enforce-nodeport-range/example_disallowed.yaml | Sample Service in team-b-service namespace with port 30007 that should be rejected |
| library/general/enforce-nodeport-range/kustomization.yaml | Kustomization file that references the template |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| apiVersion: templates.gatekeeper.sh/v1 | ||
| kind: ConstraintTemplate | ||
| metadata: | ||
| name: k8snodeportnamespacerange |
There was a problem hiding this comment.
The ConstraintTemplate is missing standard annotations that are consistently used across all other templates in this codebase. Based on the established convention (see library/general/block-nodeport-services/template.yaml:5-9, library/general/externalip/template.yaml:5-11), the metadata should include:
metadata.gatekeeper.sh/title: A human-readable title for the policymetadata.gatekeeper.sh/version: A version number (e.g., 1.0.0)description: A description of what the policy does and optionally a link to relevant Kubernetes documentation
These annotations are important for documentation and for tools that consume Gatekeeper policies.
| ranges: | ||
| type: object | ||
| additionalProperties: | ||
| type: object | ||
| properties: | ||
| namespacePattern: | ||
| type: string | ||
| portRange: | ||
| type: string |
There was a problem hiding this comment.
The schema properties lack description fields. Following the convention established in similar policies (e.g., library/general/externalip/template.yaml:22-26), each property in the openAPIV3Schema should include a description field explaining its purpose. This improves documentation and helps users understand how to configure the constraint correctly.
| some key | ||
| rule := input.parameters.ranges[key] | ||
|
|
||
| glob.match(rule.namespacePattern, [], namespace) | ||
| not port_in_range(port, rule.portRange) | ||
|
|
||
| msg := sprintf( | ||
| "NodePort %v is not allowed for namespace %v. Allowed range: %v", | ||
| [port, namespace, rule.portRange] | ||
| ) |
There was a problem hiding this comment.
The current Rego logic has a potential issue when a namespace matches multiple patterns. The violation rule iterates through all configured ranges and generates a violation for each rule where the namespace matches the pattern but the port is outside the range. This means if a namespace matches multiple patterns, a NodePort must be valid for ALL matching patterns to avoid violations, effectively requiring the port to be in the intersection of all matching ranges.
For example, with patterns "team-a-" (range 30000-30100) and "team-" (range 30000-30200), a service in "team-a-service" with port 30150 would violate the first rule even though it satisfies the second.
The more typical expectation would be to allow a port if it's valid for ANY of the matching patterns. Consider restructuring the logic to check if at least one matching rule allows the port, rather than requiring all matching rules to allow it.
| some key | |
| rule := input.parameters.ranges[key] | |
| glob.match(rule.namespacePattern, [], namespace) | |
| not port_in_range(port, rule.portRange) | |
| msg := sprintf( | |
| "NodePort %v is not allowed for namespace %v. Allowed range: %v", | |
| [port, namespace, rule.portRange] | |
| ) | |
| namespace_has_matching_rule(namespace) | |
| not port_allowed_for_some_matching_rule(port, namespace) | |
| msg := sprintf( | |
| "NodePort %v is not allowed for namespace %v.", | |
| [port, namespace] | |
| ) | |
| } | |
| namespace_has_matching_rule(namespace) { | |
| some key | |
| rule := input.parameters.ranges[key] | |
| glob.match(rule.namespacePattern, [], namespace) | |
| } | |
| port_allowed_for_some_matching_rule(port, namespace) { | |
| some key | |
| rule := input.parameters.ranges[key] | |
| glob.match(rule.namespacePattern, [], namespace) | |
| port_in_range(port, rule.portRange) |
| port_in_range(port, range) { | ||
| cleaned := trim(range, "[]") | ||
| parts := split(cleaned, ":") | ||
| start := to_number(parts[0]) | ||
| end := to_number(parts[1]) | ||
| port >= start | ||
| port <= end | ||
| } |
There was a problem hiding this comment.
The port_in_range function does not validate the portRange format before processing. If a portRange is misconfigured (e.g., using a delimiter other than ":", or malformed numbers), the to_number conversion will fail, causing the function to not match and effectively reject all services. While this might alert administrators to the misconfiguration, it would be more user-friendly to add explicit validation or at least document the expected format "[start:end]" in the parameter schema description.
| violation[{"msg": msg}] { | ||
| input.review.object.spec.type == "NodePort" |
There was a problem hiding this comment.
Following the convention established in similar Service policies (e.g., library/general/block-nodeport-services/template.yaml:23), it's recommended to add a check for input.review.kind.kind == "Service" before accessing Service-specific fields. While the constraint match configuration already filters to Services, adding this check is a defensive programming practice that makes the policy more explicit and consistent with the codebase.
|
Thanks for the clarification. |
|
@elhamafzalizadeh can you fix CI? |
|
Thanks for the feedback. |
|
Hi, just checking in if there's anything else needed for this PR and if we could run the CI again. Thanks! |
|
@elhamafzalizadeh you need to fix DCO, please sign your commit to fix it. |
|
@elhamafzalizadeh please resolve the CI error and address copilot feedback as well. |
|
This issue/PR has been automatically marked as stale because it has not had recent activity. It will be closed in 14 days if no further activity occurs. Thank you for your contributions. |
Gatekeeper Policy: K8sNodePortNamespaceRange
This is a Gatekeeper constraint that restricts the NodePort ranges based on team namespaces. Its purpose is to prevent accidental or uncoordinated NodePort assignments in the cluster.
Policy Details
constraints.gatekeeper.sh/v1beta1K8sNodePortNamespaceRangeenforce-nodeport-namespace-rangedeny(any Service violating this policy will be denied)