diff --git a/api/v1alpha1/authorization_types.go b/api/v1alpha1/authorization_types.go
index 628cda7c773..d7e3b847577 100644
--- a/api/v1alpha1/authorization_types.go
+++ b/api/v1alpha1/authorization_types.go
@@ -132,14 +132,17 @@ type Principal struct {
// You can use the `ClientIPDetection` or the `ProxyProtocol` field in
// the `ClientTrafficPolicy` to configure how the client IP is detected.
//
- // For TCPRoute targets (raw TCP connections), HTTP headers such as
+ // For TCPRoute and UDPRoute targets (raw L4 traffic), HTTP headers such as
// X-Forwarded-For are not available. The client IP is obtained from the
- // TCP connection's peer address. If intermediaries (load balancers, NAT)
- // terminate or proxy TCP, the original client IP will only be available
- // if the intermediary preserves the source address (for example by
- // enabling the PROXY protocol or avoiding SNAT). Ensure your L4 proxy is
+ // TCP connection's peer address, or from the source address of the UDP
+ // datagram. If intermediaries (load balancers, NAT) terminate or proxy the
+ // traffic, the original client IP will only be available if the
+ // intermediary preserves the source address (for example by enabling the
+ // PROXY protocol or avoiding SNAT). Note that the PROXY protocol is not
+ // available on the UDP path, so a UDPRoute target relies entirely on the
+ // datagram source address being preserved. Ensure your L4 proxy is
// configured to preserve the source IP to enable correct client-IP
- // matching for TCPRoute targets.
+ // matching for TCPRoute and UDPRoute targets.
// +optional
// +kubebuilder:validation:MinItems=1
ClientCIDRs []CIDR `json:"clientCIDRs,omitempty"`
@@ -160,7 +163,7 @@ type Principal struct {
// ClientIPGeoLocations authorizes the request based on geolocation metadata derived from the client IP.
// This field is supported for HTTPRoute and GRPCRoute authorization.
- // It is not supported for TCPRoute targets.
+ // It is not supported for TCPRoute or UDPRoute targets.
//
// If multiple entries are specified, one of the ClientIPGeoLocation entries must match for the rule to match.
//
diff --git a/api/v1alpha1/securitypolicy_types.go b/api/v1alpha1/securitypolicy_types.go
index bf8f4f8fe89..768b37ba0d3 100644
--- a/api/v1alpha1/securitypolicy_types.go
+++ b/api/v1alpha1/securitypolicy_types.go
@@ -37,19 +37,20 @@ type SecurityPolicy struct {
// SecurityPolicySpec defines the desired state of SecurityPolicy.
//
-// NOTE: SecurityPolicy can target Gateway, ListenerSet, HTTPRoute, GRPCRoute, and TCPRoute.
-// When a SecurityPolicy targets a TCPRoute, only client-IP CIDR based authorization
-// (Authorization rules that use Principal.ClientCIDRs) is applied. Other
+// NOTE: SecurityPolicy can target Gateway, ListenerSet, HTTPRoute, GRPCRoute, TCPRoute
+// and UDPRoute.
+// When a SecurityPolicy targets a TCPRoute or a UDPRoute, only client-IP CIDR based
+// authorization (Authorization rules that use Principal.ClientCIDRs) is applied. Other
// authentication/authorization features such as JWT, API Key, Basic Auth,
// OIDC, External Authorization, or GeoIP based authorization are not applicable
-// to TCPRoute targets.
+// to TCPRoute or UDPRoute targets.
//
// +kubebuilder:validation:XValidation:rule="(has(self.targetRef) && !has(self.targetRefs)) || (!has(self.targetRef) && has(self.targetRefs)) || (has(self.targetSelectors) && self.targetSelectors.size() > 0) ", message="either targetRef or targetRefs must be used"
// +kubebuilder:validation:XValidation:rule="has(self.targetRef) ? self.targetRef.group == 'gateway.networking.k8s.io' : true", message="this policy can only have a targetRef.group of gateway.networking.k8s.io"
-// +kubebuilder:validation:XValidation:rule="has(self.targetRef) ? self.targetRef.kind in ['Gateway', 'ListenerSet', 'HTTPRoute', 'GRPCRoute', 'TCPRoute'] : true", message="this policy can only have a targetRef.kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute"
+// +kubebuilder:validation:XValidation:rule="has(self.targetRef) ? self.targetRef.kind in ['Gateway', 'ListenerSet', 'HTTPRoute', 'GRPCRoute', 'TCPRoute', 'UDPRoute'] : true", message="this policy can only have a targetRef.kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute/UDPRoute"
// +kubebuilder:validation:XValidation:rule="has(self.targetRefs) ? self.targetRefs.all(ref, ref.group == 'gateway.networking.k8s.io') : true ", message="this policy can only have a targetRefs[*].group of gateway.networking.k8s.io"
-// +kubebuilder:validation:XValidation:rule="has(self.targetRefs) ? self.targetRefs.all(ref, ref.kind in ['Gateway', 'ListenerSet', 'HTTPRoute', 'GRPCRoute', 'TCPRoute']) : true ", message="this policy can only have a targetRefs[*].kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute"
-// +kubebuilder:validation:XValidation:rule="!has(self.mergeType) || ((!has(self.targetRef) || self.targetRef.kind in ['HTTPRoute', 'GRPCRoute', 'TCPRoute']) && (!has(self.targetRefs) || self.targetRefs.all(ref, ref.kind in ['HTTPRoute', 'GRPCRoute', 'TCPRoute'])) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, sel.kind in ['HTTPRoute', 'GRPCRoute', 'TCPRoute'])))", message="mergeType can only be used with xRoute targets"
+// +kubebuilder:validation:XValidation:rule="has(self.targetRefs) ? self.targetRefs.all(ref, ref.kind in ['Gateway', 'ListenerSet', 'HTTPRoute', 'GRPCRoute', 'TCPRoute', 'UDPRoute']) : true ", message="this policy can only have a targetRefs[*].kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute/UDPRoute"
+// +kubebuilder:validation:XValidation:rule="!has(self.mergeType) || ((!has(self.targetRef) || self.targetRef.kind in ['HTTPRoute', 'GRPCRoute', 'TCPRoute', 'UDPRoute']) && (!has(self.targetRefs) || self.targetRefs.all(ref, ref.kind in ['HTTPRoute', 'GRPCRoute', 'TCPRoute', 'UDPRoute'])) && (!has(self.targetSelectors) || self.targetSelectors.all(sel, sel.kind in ['HTTPRoute', 'GRPCRoute', 'TCPRoute', 'UDPRoute'])))", message="mergeType can only be used with xRoute targets"
// +kubebuilder:validation:XValidation:rule="(has(self.authorization) && has(self.authorization.rules) && self.authorization.rules.exists(r, has(r.principal) ? has(r.principal.jwt) : false)) ? has(self.jwt) : true", message="if authorization.rules.principal.jwt is used, jwt must be defined"
type SecurityPolicySpec struct {
PolicyTargetReferences `json:",inline"`
diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_securitypolicies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_securitypolicies.yaml
index 44bfdd1c2f0..32871799653 100644
--- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_securitypolicies.yaml
+++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_securitypolicies.yaml
@@ -325,14 +325,17 @@ spec:
You can use the `ClientIPDetection` or the `ProxyProtocol` field in
the `ClientTrafficPolicy` to configure how the client IP is detected.
- For TCPRoute targets (raw TCP connections), HTTP headers such as
+ For TCPRoute and UDPRoute targets (raw L4 traffic), HTTP headers such as
X-Forwarded-For are not available. The client IP is obtained from the
- TCP connection's peer address. If intermediaries (load balancers, NAT)
- terminate or proxy TCP, the original client IP will only be available
- if the intermediary preserves the source address (for example by
- enabling the PROXY protocol or avoiding SNAT). Ensure your L4 proxy is
+ TCP connection's peer address, or from the source address of the UDP
+ datagram. If intermediaries (load balancers, NAT) terminate or proxy the
+ traffic, the original client IP will only be available if the
+ intermediary preserves the source address (for example by enabling the
+ PROXY protocol or avoiding SNAT). Note that the PROXY protocol is not
+ available on the UDP path, so a UDPRoute target relies entirely on the
+ datagram source address being preserved. Ensure your L4 proxy is
configured to preserve the source IP to enable correct client-IP
- matching for TCPRoute targets.
+ matching for TCPRoute and UDPRoute targets.
items:
description: |-
CIDR defines a CIDR Address range.
@@ -345,7 +348,7 @@ spec:
description: |-
ClientIPGeoLocations authorizes the request based on geolocation metadata derived from the client IP.
This field is supported for HTTPRoute and GRPCRoute authorization.
- It is not supported for TCPRoute targets.
+ It is not supported for TCPRoute or UDPRoute targets.
If multiple entries are specified, one of the ClientIPGeoLocation entries must match for the rule to match.
@@ -8024,21 +8027,23 @@ spec:
- message: this policy can only have a targetRef.group of gateway.networking.k8s.io
rule: 'has(self.targetRef) ? self.targetRef.group == ''gateway.networking.k8s.io''
: true'
- - message: this policy can only have a targetRef.kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute
+ - message: this policy can only have a targetRef.kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute/UDPRoute
rule: 'has(self.targetRef) ? self.targetRef.kind in [''Gateway'', ''ListenerSet'',
- ''HTTPRoute'', ''GRPCRoute'', ''TCPRoute''] : true'
+ ''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'', ''UDPRoute''] : true'
- message: this policy can only have a targetRefs[*].group of gateway.networking.k8s.io
rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.group ==
''gateway.networking.k8s.io'') : true '
- - message: this policy can only have a targetRefs[*].kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute
+ - message: this policy can only have a targetRefs[*].kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute/UDPRoute
rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.kind in [''Gateway'',
- ''ListenerSet'', ''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'']) : true '
+ ''ListenerSet'', ''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'', ''UDPRoute''])
+ : true '
- message: mergeType can only be used with xRoute targets
rule: '!has(self.mergeType) || ((!has(self.targetRef) || self.targetRef.kind
- in [''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'']) && (!has(self.targetRefs)
- || self.targetRefs.all(ref, ref.kind in [''HTTPRoute'', ''GRPCRoute'',
- ''TCPRoute''])) && (!has(self.targetSelectors) || self.targetSelectors.all(sel,
- sel.kind in [''HTTPRoute'', ''GRPCRoute'', ''TCPRoute''])))'
+ in [''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'', ''UDPRoute'']) &&
+ (!has(self.targetRefs) || self.targetRefs.all(ref, ref.kind in [''HTTPRoute'',
+ ''GRPCRoute'', ''TCPRoute'', ''UDPRoute''])) && (!has(self.targetSelectors)
+ || self.targetSelectors.all(sel, sel.kind in [''HTTPRoute'', ''GRPCRoute'',
+ ''TCPRoute'', ''UDPRoute''])))'
- message: if authorization.rules.principal.jwt is used, jwt must be defined
rule: '(has(self.authorization) && has(self.authorization.rules) &&
self.authorization.rules.exists(r, has(r.principal) ? has(r.principal.jwt)
diff --git a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml
index 2dd2c416450..c5d34350e02 100644
--- a/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml
+++ b/charts/gateway-helm/charts/crds/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml
@@ -324,14 +324,17 @@ spec:
You can use the `ClientIPDetection` or the `ProxyProtocol` field in
the `ClientTrafficPolicy` to configure how the client IP is detected.
- For TCPRoute targets (raw TCP connections), HTTP headers such as
+ For TCPRoute and UDPRoute targets (raw L4 traffic), HTTP headers such as
X-Forwarded-For are not available. The client IP is obtained from the
- TCP connection's peer address. If intermediaries (load balancers, NAT)
- terminate or proxy TCP, the original client IP will only be available
- if the intermediary preserves the source address (for example by
- enabling the PROXY protocol or avoiding SNAT). Ensure your L4 proxy is
+ TCP connection's peer address, or from the source address of the UDP
+ datagram. If intermediaries (load balancers, NAT) terminate or proxy the
+ traffic, the original client IP will only be available if the
+ intermediary preserves the source address (for example by enabling the
+ PROXY protocol or avoiding SNAT). Note that the PROXY protocol is not
+ available on the UDP path, so a UDPRoute target relies entirely on the
+ datagram source address being preserved. Ensure your L4 proxy is
configured to preserve the source IP to enable correct client-IP
- matching for TCPRoute targets.
+ matching for TCPRoute and UDPRoute targets.
items:
description: |-
CIDR defines a CIDR Address range.
@@ -344,7 +347,7 @@ spec:
description: |-
ClientIPGeoLocations authorizes the request based on geolocation metadata derived from the client IP.
This field is supported for HTTPRoute and GRPCRoute authorization.
- It is not supported for TCPRoute targets.
+ It is not supported for TCPRoute or UDPRoute targets.
If multiple entries are specified, one of the ClientIPGeoLocation entries must match for the rule to match.
@@ -8023,21 +8026,23 @@ spec:
- message: this policy can only have a targetRef.group of gateway.networking.k8s.io
rule: 'has(self.targetRef) ? self.targetRef.group == ''gateway.networking.k8s.io''
: true'
- - message: this policy can only have a targetRef.kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute
+ - message: this policy can only have a targetRef.kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute/UDPRoute
rule: 'has(self.targetRef) ? self.targetRef.kind in [''Gateway'', ''ListenerSet'',
- ''HTTPRoute'', ''GRPCRoute'', ''TCPRoute''] : true'
+ ''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'', ''UDPRoute''] : true'
- message: this policy can only have a targetRefs[*].group of gateway.networking.k8s.io
rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.group ==
''gateway.networking.k8s.io'') : true '
- - message: this policy can only have a targetRefs[*].kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute
+ - message: this policy can only have a targetRefs[*].kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute/UDPRoute
rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.kind in [''Gateway'',
- ''ListenerSet'', ''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'']) : true '
+ ''ListenerSet'', ''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'', ''UDPRoute''])
+ : true '
- message: mergeType can only be used with xRoute targets
rule: '!has(self.mergeType) || ((!has(self.targetRef) || self.targetRef.kind
- in [''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'']) && (!has(self.targetRefs)
- || self.targetRefs.all(ref, ref.kind in [''HTTPRoute'', ''GRPCRoute'',
- ''TCPRoute''])) && (!has(self.targetSelectors) || self.targetSelectors.all(sel,
- sel.kind in [''HTTPRoute'', ''GRPCRoute'', ''TCPRoute''])))'
+ in [''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'', ''UDPRoute'']) &&
+ (!has(self.targetRefs) || self.targetRefs.all(ref, ref.kind in [''HTTPRoute'',
+ ''GRPCRoute'', ''TCPRoute'', ''UDPRoute''])) && (!has(self.targetSelectors)
+ || self.targetSelectors.all(sel, sel.kind in [''HTTPRoute'', ''GRPCRoute'',
+ ''TCPRoute'', ''UDPRoute''])))'
- message: if authorization.rules.principal.jwt is used, jwt must be defined
rule: '(has(self.authorization) && has(self.authorization.rules) &&
self.authorization.rules.exists(r, has(r.principal) ? has(r.principal.jwt)
diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go
index c22bc65ffe8..4cb72bbc53b 100644
--- a/internal/gatewayapi/securitypolicy.go
+++ b/internal/gatewayapi/securitypolicy.go
@@ -512,9 +512,13 @@ func (t *Translator) processSecurityPolicyForRoute(
// then run it once to keep the flow linear and easier to read.
validator := validateSecurityPolicy
errMsg := "invalid SecurityPolicy"
- if currTarget.Kind == resource.KindTCPRoute {
- validator = validateSecurityPolicyForTCP
+ switch currTarget.Kind {
+ case resource.KindTCPRoute:
+ validator = func(p *egv1a1.SecurityPolicy) error { return validateSecurityPolicyForL4(p, "TCP") }
errMsg = "invalid SecurityPolicy for TCP route"
+ case resource.KindUDPRoute:
+ validator = func(p *egv1a1.SecurityPolicy) error { return validateSecurityPolicyForL4(p, "UDP") }
+ errMsg = "invalid SecurityPolicy for UDP route"
}
if err := validator(policy); err != nil {
status.SetTranslationErrorForPolicyAncestors(&policy.Status,
@@ -968,16 +972,19 @@ func validateSecurityPolicy(p *egv1a1.SecurityPolicy) error {
return nil
}
-// validateSecurityPolicyForTCP ensures SecurityPolicy usage on TCP is compatible.
+// validateSecurityPolicyForL4 ensures SecurityPolicy usage on an L4 protocol
+// (TCP or UDP) is compatible. proto names the protocol in the returned errors.
//
-// TCP supports Authorization with ClientCIDRs ONLY.
+// L4 supports Authorization with ClientCIDRs ONLY, because there is no HTTP
+// request to inspect:
// - Principals.JWT => invalid (HTTP-only)
// - Principals.Headers => invalid (HTTP-only)
-// - Empty/no Authorization is allowed and results in no-op on TCP.
+// - Operation => invalid (HTTP methods/paths)
+// - Empty/no Authorization is allowed and results in no-op on L4.
// Returns an error when any HTTP-only field is present or CIDRs are invalid.
-func validateSecurityPolicyForTCP(p *egv1a1.SecurityPolicy) error {
+func validateSecurityPolicyForL4(p *egv1a1.SecurityPolicy, proto string) error {
if p.Spec.CORS != nil || p.Spec.CSRF != nil || p.Spec.JWT != nil || p.Spec.OIDC != nil || p.Spec.APIKeyAuth != nil || p.Spec.BasicAuth != nil || p.Spec.ExtAuth != nil {
- return fmt.Errorf("only authorization is supported for TCP (routes/listeners)")
+ return fmt.Errorf("only authorization is supported for %s (routes/listeners)", proto)
}
if p.Spec.Authorization == nil || len(p.Spec.Authorization.Rules) == 0 {
return nil
@@ -985,19 +992,25 @@ func validateSecurityPolicyForTCP(p *egv1a1.SecurityPolicy) error {
for i := range p.Spec.Authorization.Rules {
rule := &p.Spec.Authorization.Rules[i]
if rule.CEL != nil {
- return fmt.Errorf("rule %d: CEL not supported for TCP", i)
+ return fmt.Errorf("rule %d: CEL not supported for %s", i, proto)
+ }
+ // Methods and paths are HTTP concepts. The L4 matcher only looks at the
+ // client CIDRs, so accepting an operation here would silently widen the
+ // rule to every datagram or connection from those CIDRs.
+ if rule.Operation != nil {
+ return fmt.Errorf("rule %d: operation not supported for %s", i, proto)
}
if rule.Principal == nil {
continue
}
if rule.Principal.JWT != nil {
- return fmt.Errorf("rule %d: JWT not supported for TCP", i)
+ return fmt.Errorf("rule %d: JWT not supported for %s", i, proto)
}
if len(rule.Principal.Headers) > 0 {
- return fmt.Errorf("rule %d: headers not supported for TCP", i)
+ return fmt.Errorf("rule %d: headers not supported for %s", i, proto)
}
if len(rule.Principal.ClientIPGeoLocations) > 0 {
- return fmt.Errorf("rule %d: clientIPGeoLocations not supported for TCP", i)
+ return fmt.Errorf("rule %d: clientIPGeoLocations not supported for %s", i, proto)
}
if err := validateCIDRs(rule.Principal.ClientCIDRs); err != nil {
return fmt.Errorf("rule %d: %w", i, err)
@@ -1006,7 +1019,7 @@ func validateSecurityPolicyForTCP(p *egv1a1.SecurityPolicy) error {
return nil
}
-// validateCIDRs validates CIDR strings for TCP authorization rules.
+// validateCIDRs validates CIDR strings for L4 authorization rules.
func validateCIDRs(cidrs []egv1a1.CIDR) error {
for _, c := range cidrs {
if _, _, err := net.ParseCIDR(string(c)); err != nil {
@@ -1385,7 +1398,18 @@ func (t *Translator) translateSecurityPolicyForRoute(
continue
}
tl := xdsIR[irKey].GetTCPListener(irListenerName(listener))
+ if tl == nil {
+ continue
+ }
for _, r := range tl.Routes {
+ // Only the IR route built from the targeted TCPRoute may be
+ // modified. Several TCPRoutes can name the same listener while
+ // only one of them is attached, so without this check a policy
+ // targeting an unattached route would silently apply to
+ // whichever route is actually serving traffic.
+ if r.Name != irTCPRouteName(route) {
+ continue
+ }
// If target.SectionName is specified it must match the route-rule section name
// in the IR. For HTTP/GRPC routes this is r.Metadata.SectionName; for TCP
// routes the section name is currently stored on r.Destination.Metadata.SectionName.
@@ -1403,6 +1427,35 @@ func (t *Translator) translateSecurityPolicyForRoute(
}
}
}
+ case resource.KindUDPRoute:
+ for _, listener := range parentRefCtx.listeners {
+ // If targetListener is set, only apply to that exact listener.
+ if targetListener != nil && targetListenerName != irListenerName(listener) {
+ continue
+ }
+ ul := xdsIR[irKey].GetUDPListener(irListenerName(listener))
+ // A UDP listener holds at most one route: when several UDPRoutes name the
+ // same listener only the oldest one is attached, so a policy targeting any
+ // of the others has nothing to apply to. Comparing the names keeps such a
+ // policy from restricting the route that is actually serving traffic.
+ if ul == nil || ul.Route == nil || ul.Route.Name != irUDPRouteName(route) {
+ continue
+ }
+ r := ul.Route
+ // As with TCP, the route-rule section name lives on the destination metadata.
+ if target.SectionName != nil && string(*target.SectionName) != r.Destination.Metadata.SectionName {
+ continue
+ }
+
+ if r.Authorization != nil {
+ continue
+ }
+ // Only authorization for UDP
+ if authorization != nil {
+ authCopy := *authorization
+ r.Authorization = &authCopy
+ }
+ }
case resource.KindHTTPRoute, resource.KindGRPCRoute:
var (
hasBaseErrs = errs != nil
@@ -1695,15 +1748,15 @@ func (t *Translator) translateSecurityPolicyForListeners(
)
}
- // Pre-create a TCP-only authorization object to avoid re-allocation
- var tcpAuthorization *ir.Authorization
+ // Pre-create an L4-only authorization object to avoid re-allocation
+ var l4Authorization *ir.Authorization
if authorization != nil {
authCopy := *authorization
- tcpAuthorization = &authCopy
+ l4Authorization = &authCopy
}
// Apply to TCP listeners (Authorization only).
- if tcpAuthorization != nil {
+ if l4Authorization != nil {
for _, tl := range x.TCP {
if tl == nil || len(tl.Routes) == 0 {
continue
@@ -1718,8 +1771,27 @@ func (t *Translator) translateSecurityPolicyForListeners(
if r.Authorization != nil {
continue
}
- r.Authorization = tcpAuthorization
+ r.Authorization = l4Authorization
+ }
+ }
+ }
+
+ // Apply to UDP listeners (Authorization only).
+ if l4Authorization != nil {
+ for _, ul := range x.UDP {
+ // A UDP listener holds at most one route.
+ if ul == nil || ul.Route == nil {
+ continue
+ }
+ if !listenerNames.Has(ul.Name) {
+ continue
+ }
+ // A Policy targeting the specific scope(xRoute rule, xRoute, Gateway listener) wins over a policy
+ // targeting a lesser specific scope(Gateway).
+ if ul.Route.Authorization != nil {
+ continue
}
+ ul.Route.Authorization = l4Authorization
}
}
diff --git a/internal/gatewayapi/securitypolicy_test.go b/internal/gatewayapi/securitypolicy_test.go
index 5ff44c8d4c5..73a8e0f2bba 100644
--- a/internal/gatewayapi/securitypolicy_test.go
+++ b/internal/gatewayapi/securitypolicy_test.go
@@ -1056,7 +1056,7 @@ func SetRouteParentContext(route RouteContext, parentRef gwapiv1.ParentReference
route.SetRouteParentContext(parentRef, ctx)
}
-// --- TCP branch: validateSecurityPolicyForTCP(...) returns err -> SetTranslationErrorForPolicyAncestors(...) + return
+// --- L4 branch: validateSecurityPolicyForL4(...) returns err -> SetTranslationErrorForPolicyAncestors(...) + return
func Test_SecurityPolicy_TCP_Invalid_setsStatus_and_returns(t *testing.T) {
tr := &Translator{GatewayControllerName: "gateway.envoyproxy.io/gatewayclass-controller"}
trContext := &TranslatorContext{}
@@ -1208,7 +1208,7 @@ func Test_SecurityPolicy_HTTP_Invalid_setsStatus_and_returns(t *testing.T) {
require.True(t, hasParentFalseCondition(policy))
}
-func Test_validateSecurityPolicyForTCP_Table(t *testing.T) {
+func Test_validateSecurityPolicyForL4_Table(t *testing.T) {
tests := []struct {
name string
spec egv1a1.SecurityPolicySpec
@@ -1287,6 +1287,44 @@ func Test_validateSecurityPolicyForTCP_Table(t *testing.T) {
},
wantErr: true,
},
+ {
+ // The L4 matcher only looks at client CIDRs, so accepting an operation
+ // would silently widen the rule to all traffic from those CIDRs.
+ name: "operation with methods is rejected",
+ spec: egv1a1.SecurityPolicySpec{
+ Authorization: &egv1a1.Authorization{
+ Rules: []egv1a1.AuthorizationRule{
+ {
+ Action: egv1a1.AuthorizationActionAllow,
+ Operation: &egv1a1.Operation{Methods: []gwapiv1.HTTPMethod{gwapiv1.HTTPMethodGet}},
+ Principal: &egv1a1.Principal{
+ ClientCIDRs: []egv1a1.CIDR{"10.0.0.0/8"},
+ },
+ },
+ },
+ },
+ },
+ wantErr: true,
+ },
+ {
+ name: "operation with path is rejected",
+ spec: egv1a1.SecurityPolicySpec{
+ Authorization: &egv1a1.Authorization{
+ Rules: []egv1a1.AuthorizationRule{
+ {
+ Action: egv1a1.AuthorizationActionAllow,
+ Operation: &egv1a1.Operation{Path: &egv1a1.PathMatch{
+ Value: "/admin",
+ }},
+ Principal: &egv1a1.Principal{
+ ClientCIDRs: []egv1a1.CIDR{"10.0.0.0/8"},
+ },
+ },
+ },
+ },
+ },
+ wantErr: true,
+ },
{
name: "jwt principal not supported on tcp",
spec: egv1a1.SecurityPolicySpec{
@@ -1375,17 +1413,36 @@ func Test_validateSecurityPolicyForTCP_Table(t *testing.T) {
},
}
- for _, tc := range tests {
- t.Run(tc.name, func(t *testing.T) {
- p := &egv1a1.SecurityPolicy{Spec: tc.spec}
- err := validateSecurityPolicyForTCP(p)
- if tc.wantErr {
- require.Error(t, err)
- } else {
- require.NoError(t, err)
- }
- })
+ // TCP and UDP share the same rules, so every case must hold for both.
+ for _, proto := range []string{"TCP", "UDP"} {
+ for _, tc := range tests {
+ t.Run(proto+"/"+tc.name, func(t *testing.T) {
+ p := &egv1a1.SecurityPolicy{Spec: tc.spec}
+ err := validateSecurityPolicyForL4(p, proto)
+ if tc.wantErr {
+ require.Error(t, err)
+ } else {
+ require.NoError(t, err)
+ }
+ })
+ }
}
+
+ // Rejections that are about the protocol rather than the input name it, so the
+ // status condition tells the user which listener protocol refused the field.
+ t.Run("protocol named in protocol-specific errors", func(t *testing.T) {
+ p := &egv1a1.SecurityPolicy{Spec: egv1a1.SecurityPolicySpec{
+ Authorization: &egv1a1.Authorization{
+ Rules: []egv1a1.AuthorizationRule{{
+ Action: egv1a1.AuthorizationActionAllow,
+ Principal: &egv1a1.Principal{Headers: []egv1a1.AuthorizationHeaderMatch{{Name: "x-user", Values: []string{"foo"}}}},
+ }},
+ },
+ }}
+
+ require.ErrorContains(t, validateSecurityPolicyForL4(p, "TCP"), "headers not supported for TCP")
+ require.ErrorContains(t, validateSecurityPolicyForL4(p, "UDP"), "headers not supported for UDP")
+ })
}
func Test_validateAuthorizationGeoIPForHTTP(t *testing.T) {
diff --git a/internal/gatewayapi/testdata/tcproute-securitypolicy-targeting-unattached-route.in.yaml b/internal/gatewayapi/testdata/tcproute-securitypolicy-targeting-unattached-route.in.yaml
new file mode 100644
index 00000000000..fce6ece8f3b
--- /dev/null
+++ b/internal/gatewayapi/testdata/tcproute-securitypolicy-targeting-unattached-route.in.yaml
@@ -0,0 +1,66 @@
+# Two TCPRoutes name the same listener, so only the oldest is attached to the IR.
+# A SecurityPolicy targeting the other one must not restrict the route that is
+# actually serving traffic.
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ namespace: envoy-gateway
+ name: gateway-tcp
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - name: tcp
+ protocol: TCP
+ port: 8088
+ allowedRoutes:
+ namespaces:
+ from: All
+tcpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: TCPRoute
+ metadata:
+ namespace: default
+ name: tcpr-attached
+ spec:
+ parentRefs:
+ - namespace: envoy-gateway
+ name: gateway-tcp
+ sectionName: tcp
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8163
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: TCPRoute
+ metadata:
+ namespace: default
+ name: tcpr-unattached
+ spec:
+ parentRefs:
+ - namespace: envoy-gateway
+ name: gateway-tcp
+ sectionName: tcp
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8163
+securityPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ namespace: default
+ name: sp-tcpr-unattached
+ spec:
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: TCPRoute
+ name: tcpr-unattached
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ name: allow-unattached-only
+ principal:
+ clientCIDRs:
+ - 10.99.0.0/16
diff --git a/internal/gatewayapi/testdata/tcproute-securitypolicy-targeting-unattached-route.out.yaml b/internal/gatewayapi/testdata/tcproute-securitypolicy-targeting-unattached-route.out.yaml
new file mode 100644
index 00000000000..f3c4655a6e2
--- /dev/null
+++ b/internal/gatewayapi/testdata/tcproute-securitypolicy-targeting-unattached-route.out.yaml
@@ -0,0 +1,222 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ name: gateway-tcp
+ namespace: envoy-gateway
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - allowedRoutes:
+ namespaces:
+ from: All
+ name: tcp
+ port: 8088
+ protocol: TCP
+ status:
+ listeners:
+ - attachedRoutes: 2
+ conditions:
+ - lastTransitionTime: null
+ message: Sending translated listener configuration to the data plane
+ reason: Programmed
+ status: "True"
+ type: Programmed
+ - lastTransitionTime: null
+ message: Listener has been successfully translated
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: Listener references have been resolved
+ reason: ResolvedRefs
+ status: "True"
+ type: ResolvedRefs
+ name: tcp
+ supportedKinds:
+ - group: gateway.networking.k8s.io
+ kind: TCPRoute
+infraIR:
+ envoy-gateway/gateway-tcp:
+ proxy:
+ listeners:
+ - name: envoy-gateway/gateway-tcp/tcp
+ ports:
+ - containerPort: 8088
+ name: tcp-8088
+ protocol: TCP
+ servicePort: 8088
+ metadata:
+ labels:
+ gateway.envoyproxy.io/owning-gateway-name: gateway-tcp
+ gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+ ownerReference:
+ kind: GatewayClass
+ name: envoy-gateway-class
+ name: envoy-gateway/gateway-tcp
+ namespace: envoy-gateway-system
+securityPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ name: sp-tcpr-unattached
+ namespace: default
+ spec:
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ name: allow-unattached-only
+ principal:
+ clientCIDRs:
+ - 10.99.0.0/16
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: TCPRoute
+ name: tcpr-unattached
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-tcp
+ namespace: envoy-gateway
+ sectionName: tcp
+ conditions:
+ - lastTransitionTime: null
+ message: Policy has been accepted.
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+tcpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: TCPRoute
+ metadata:
+ name: tcpr-attached
+ namespace: default
+ spec:
+ parentRefs:
+ - name: gateway-tcp
+ namespace: envoy-gateway
+ sectionName: tcp
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8163
+ status:
+ parents:
+ - conditions:
+ - lastTransitionTime: null
+ message: Route is accepted
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: Resolved all the Object references for the Route
+ reason: ResolvedRefs
+ status: "True"
+ type: ResolvedRefs
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+ parentRef:
+ name: gateway-tcp
+ namespace: envoy-gateway
+ sectionName: tcp
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: TCPRoute
+ metadata:
+ name: tcpr-unattached
+ namespace: default
+ spec:
+ parentRefs:
+ - name: gateway-tcp
+ namespace: envoy-gateway
+ sectionName: tcp
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8163
+ status:
+ parents:
+ - conditions:
+ - lastTransitionTime: null
+ message: Route is accepted
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: Resolved all the Object references for the Route
+ reason: ResolvedRefs
+ status: "True"
+ type: ResolvedRefs
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+ parentRef:
+ name: gateway-tcp
+ namespace: envoy-gateway
+ sectionName: tcp
+xdsIR:
+ envoy-gateway/gateway-tcp:
+ accessLog:
+ json:
+ - path: /dev/stdout
+ globalResources:
+ proxyServiceCluster:
+ metadata:
+ kind: Service
+ name: envoy-envoy-gateway-gateway-tcp-093cb215
+ namespace: envoy-gateway-system
+ sectionName: "8080"
+ name: envoy-gateway/gateway-tcp
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.6.5.4
+ port: 8080
+ zone: zone1
+ metadata:
+ kind: Service
+ name: envoy-envoy-gateway-gateway-tcp-093cb215
+ namespace: envoy-gateway-system
+ sectionName: "8080"
+ name: envoy-gateway/gateway-tcp
+ protocol: TCP
+ readyListener:
+ address: 0.0.0.0
+ ipFamily: IPv4
+ path: /ready
+ port: 19003
+ tcp:
+ - address: 0.0.0.0
+ externalPort: 8088
+ metadata:
+ kind: Gateway
+ name: gateway-tcp
+ namespace: envoy-gateway
+ sectionName: tcp
+ name: envoy-gateway/gateway-tcp/tcp
+ port: 8088
+ routes:
+ - destination:
+ metadata:
+ kind: TCPRoute
+ name: tcpr-attached
+ namespace: default
+ name: tcproute/default/tcpr-attached/rule/-1
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.7.7.7
+ port: 8163
+ metadata:
+ kind: Service
+ name: service-1
+ namespace: default
+ sectionName: "8163"
+ name: tcproute/default/tcpr-attached/rule/-1/backend/0
+ protocol: TCP
+ weight: 1
+ metadata:
+ kind: TCPRoute
+ name: tcpr-attached
+ namespace: default
+ name: tcproute/default/tcpr-attached
diff --git a/internal/gatewayapi/testdata/udproute-securitypolicy-targeting-unattached-route.in.yaml b/internal/gatewayapi/testdata/udproute-securitypolicy-targeting-unattached-route.in.yaml
new file mode 100644
index 00000000000..7e90ec3c224
--- /dev/null
+++ b/internal/gatewayapi/testdata/udproute-securitypolicy-targeting-unattached-route.in.yaml
@@ -0,0 +1,66 @@
+# Two UDPRoutes name the same listener, so only the oldest is attached to the IR.
+# A SecurityPolicy targeting the other one must not restrict the route that is
+# actually serving traffic.
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ namespace: envoy-gateway
+ name: gateway-udp
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - name: udp
+ protocol: UDP
+ port: 8088
+ allowedRoutes:
+ namespaces:
+ from: All
+udpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: UDPRoute
+ metadata:
+ namespace: default
+ name: udpr-attached
+ spec:
+ parentRefs:
+ - namespace: envoy-gateway
+ name: gateway-udp
+ sectionName: udp
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8163
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: UDPRoute
+ metadata:
+ namespace: default
+ name: udpr-unattached
+ spec:
+ parentRefs:
+ - namespace: envoy-gateway
+ name: gateway-udp
+ sectionName: udp
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8163
+securityPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ namespace: default
+ name: sp-udpr-unattached
+ spec:
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: UDPRoute
+ name: udpr-unattached
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ name: allow-unattached-only
+ principal:
+ clientCIDRs:
+ - 10.99.0.0/16
diff --git a/internal/gatewayapi/testdata/udproute-securitypolicy-targeting-unattached-route.out.yaml b/internal/gatewayapi/testdata/udproute-securitypolicy-targeting-unattached-route.out.yaml
new file mode 100644
index 00000000000..6a8b04c3da9
--- /dev/null
+++ b/internal/gatewayapi/testdata/udproute-securitypolicy-targeting-unattached-route.out.yaml
@@ -0,0 +1,205 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ name: gateway-udp
+ namespace: envoy-gateway
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - allowedRoutes:
+ namespaces:
+ from: All
+ name: udp
+ port: 8088
+ protocol: UDP
+ status:
+ listeners:
+ - attachedRoutes: 2
+ conditions:
+ - lastTransitionTime: null
+ message: Sending translated listener configuration to the data plane
+ reason: Programmed
+ status: "True"
+ type: Programmed
+ - lastTransitionTime: null
+ message: Listener has been successfully translated
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: Listener references have been resolved
+ reason: ResolvedRefs
+ status: "True"
+ type: ResolvedRefs
+ name: udp
+ supportedKinds:
+ - group: gateway.networking.k8s.io
+ kind: UDPRoute
+infraIR:
+ envoy-gateway/gateway-udp:
+ proxy:
+ listeners:
+ - name: envoy-gateway/gateway-udp/udp
+ ports:
+ - containerPort: 8088
+ name: udp-8088
+ protocol: UDP
+ servicePort: 8088
+ metadata:
+ labels:
+ gateway.envoyproxy.io/owning-gateway-name: gateway-udp
+ gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+ ownerReference:
+ kind: GatewayClass
+ name: envoy-gateway-class
+ name: envoy-gateway/gateway-udp
+ namespace: envoy-gateway-system
+securityPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ name: sp-udpr-unattached
+ namespace: default
+ spec:
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ name: allow-unattached-only
+ principal:
+ clientCIDRs:
+ - 10.99.0.0/16
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: UDPRoute
+ name: udpr-unattached
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-udp
+ namespace: envoy-gateway
+ sectionName: udp
+ conditions:
+ - lastTransitionTime: null
+ message: Policy has been accepted.
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+udpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: UDPRoute
+ metadata:
+ name: udpr-attached
+ namespace: default
+ spec:
+ parentRefs:
+ - name: gateway-udp
+ namespace: envoy-gateway
+ sectionName: udp
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8163
+ status:
+ parents:
+ - conditions:
+ - lastTransitionTime: null
+ message: Route is accepted
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: UDP Port 8163 not found on Service default/service-1
+ reason: PortNotFound
+ status: "False"
+ type: ResolvedRefs
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+ parentRef:
+ name: gateway-udp
+ namespace: envoy-gateway
+ sectionName: udp
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: UDPRoute
+ metadata:
+ name: udpr-unattached
+ namespace: default
+ spec:
+ parentRefs:
+ - name: gateway-udp
+ namespace: envoy-gateway
+ sectionName: udp
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8163
+ status:
+ parents:
+ - conditions:
+ - lastTransitionTime: null
+ message: Route is accepted
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: UDP Port 8163 not found on Service default/service-1
+ reason: PortNotFound
+ status: "False"
+ type: ResolvedRefs
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+ parentRef:
+ name: gateway-udp
+ namespace: envoy-gateway
+ sectionName: udp
+xdsIR:
+ envoy-gateway/gateway-udp:
+ accessLog:
+ json:
+ - path: /dev/stdout
+ globalResources:
+ proxyServiceCluster:
+ metadata:
+ kind: Service
+ name: envoy-envoy-gateway-gateway-udp-2934226e
+ namespace: envoy-gateway-system
+ sectionName: "8080"
+ name: envoy-gateway/gateway-udp
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.6.5.4
+ port: 8080
+ zone: zone1
+ metadata:
+ kind: Service
+ name: envoy-envoy-gateway-gateway-udp-2934226e
+ namespace: envoy-gateway-system
+ sectionName: "8080"
+ name: envoy-gateway/gateway-udp
+ protocol: TCP
+ readyListener:
+ address: 0.0.0.0
+ ipFamily: IPv4
+ path: /ready
+ port: 19003
+ udp:
+ - address: 0.0.0.0
+ externalPort: 8088
+ metadata:
+ kind: Gateway
+ name: gateway-udp
+ namespace: envoy-gateway
+ sectionName: udp
+ name: envoy-gateway/gateway-udp/udp
+ port: 8088
+ route:
+ destination:
+ metadata:
+ kind: UDPRoute
+ name: udpr-attached
+ namespace: default
+ name: udproute/default/udpr-attached/rule/-1
+ name: udproute/default/udpr-attached
diff --git a/internal/gatewayapi/testdata/udproute-securitypolicy-with-authorization-client-cidr.in.yaml b/internal/gatewayapi/testdata/udproute-securitypolicy-with-authorization-client-cidr.in.yaml
new file mode 100644
index 00000000000..22a0e764896
--- /dev/null
+++ b/internal/gatewayapi/testdata/udproute-securitypolicy-with-authorization-client-cidr.in.yaml
@@ -0,0 +1,259 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ namespace: envoy-gateway
+ name: gateway-udp
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - name: foo
+ protocol: UDP
+ port: 8088
+ allowedRoutes:
+ namespaces:
+ from: All
+ - name: bar
+ protocol: UDP
+ port: 8089
+ allowedRoutes:
+ namespaces:
+ from: All
+ - name: baz
+ protocol: UDP
+ port: 8090
+ allowedRoutes:
+ namespaces:
+ from: All
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ namespace: envoy-gateway
+ name: gateway-mixed
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - name: http
+ protocol: HTTP
+ port: 80
+ allowedRoutes:
+ namespaces:
+ from: All
+ - name: foo
+ protocol: UDP
+ port: 8443
+ allowedRoutes:
+ namespaces:
+ from: All
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ namespace: default
+ name: hr-app-mixed
+ spec:
+ parentRefs:
+ - namespace: envoy-gateway
+ name: gateway-mixed
+ sectionName: http
+ rules:
+ - matches:
+ - path:
+ type: PathPrefix
+ value: /
+ backendRefs:
+ - name: service-1
+ port: 8080
+udpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: UDPRoute
+ metadata:
+ namespace: default
+ name: udpr-app-foo
+ spec:
+ parentRefs:
+ - namespace: envoy-gateway
+ name: gateway-udp
+ sectionName: foo
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8163
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: UDPRoute
+ metadata:
+ namespace: default
+ name: udpr-app-bar
+ spec:
+ parentRefs:
+ - namespace: envoy-gateway
+ name: gateway-udp
+ sectionName: bar
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8163
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: UDPRoute
+ metadata:
+ namespace: default
+ name: udpr-app-baz
+ spec:
+ parentRefs:
+ - namespace: envoy-gateway
+ name: gateway-udp
+ sectionName: baz
+ rules:
+ - name: rule-baz
+ backendRefs:
+ - name: service-1
+ port: 8163
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: UDPRoute
+ metadata:
+ namespace: default
+ name: udpr-app-mixed
+ spec:
+ parentRefs:
+ - namespace: envoy-gateway
+ name: gateway-mixed
+ sectionName: foo
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8163
+securityPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ namespace: envoy-gateway
+ name: sp-gw-udp-whole
+ spec:
+ targetRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-udp
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ name: allow-gateway
+ principal:
+ clientCIDRs:
+ - 10.10.0.0/24
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ namespace: envoy-gateway
+ name: sp-gw-udp-section-foo # This policy should attach sectionName foo
+ spec:
+ targetRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-udp
+ sectionName: foo
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ name: allow-section-foo
+ principal:
+ clientCIDRs:
+ - 10.10.1.0/24
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ namespace: default
+ name: sp-udpr-app-bar # This policy should attach udproute bar
+ spec:
+ targetRef:
+ group: gateway.networking.k8s.io
+ kind: UDPRoute
+ name: udpr-app-bar
+ authorization:
+ defaultAction: Allow
+ rules:
+ - action: Deny
+ name: deny-route-bar
+ principal:
+ clientCIDRs:
+ - 10.10.2.0/24
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ namespace: default
+ name: sp-udpr-app-conflict # This policy should NOT attach udproute bar due to conflict with above policy
+ spec:
+ targetRef:
+ group: gateway.networking.k8s.io
+ kind: UDPRoute
+ name: udpr-app-bar
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ name: allow-route-conflict
+ principal:
+ clientCIDRs:
+ - 10.10.3.0/24
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ namespace: default
+ name: sp-udpr-app-baz-section # This policy should attach udproute baz via its rule name
+ spec:
+ targetRef:
+ group: gateway.networking.k8s.io
+ kind: UDPRoute
+ name: udpr-app-baz
+ sectionName: rule-baz
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ name: allow-route-baz
+ principal:
+ clientCIDRs:
+ - 10.10.6.0/24
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ namespace: envoy-gateway
+ name: sp-udpr-app-invalid-section # This policy should NOT attach due to invalid section
+ spec:
+ targetRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-udp
+ sectionName: bogus
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ name: allow-invalid-section
+ principal:
+ clientCIDRs:
+ - 10.10.4.0/24
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ namespace: envoy-gateway
+ name: sp-mixed-gateway # This policy should attach to gateway-mixed, applying to all listeners, but only whats supported
+ spec:
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-mixed
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ principal:
+ clientCIDRs:
+ - 10.10.5.0/24
+ cors:
+ allowOrigins:
+ - https://example.com
+ allowMethods:
+ - GET
+ - POST
diff --git a/internal/gatewayapi/testdata/udproute-securitypolicy-with-authorization-client-cidr.out.yaml b/internal/gatewayapi/testdata/udproute-securitypolicy-with-authorization-client-cidr.out.yaml
new file mode 100644
index 00000000000..92e47c74af6
--- /dev/null
+++ b/internal/gatewayapi/testdata/udproute-securitypolicy-with-authorization-client-cidr.out.yaml
@@ -0,0 +1,893 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ name: gateway-udp
+ namespace: envoy-gateway
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - allowedRoutes:
+ namespaces:
+ from: All
+ name: foo
+ port: 8088
+ protocol: UDP
+ - allowedRoutes:
+ namespaces:
+ from: All
+ name: bar
+ port: 8089
+ protocol: UDP
+ - allowedRoutes:
+ namespaces:
+ from: All
+ name: baz
+ port: 8090
+ protocol: UDP
+ status:
+ listeners:
+ - attachedRoutes: 1
+ conditions:
+ - lastTransitionTime: null
+ message: Sending translated listener configuration to the data plane
+ reason: Programmed
+ status: "True"
+ type: Programmed
+ - lastTransitionTime: null
+ message: Listener has been successfully translated
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: Listener references have been resolved
+ reason: ResolvedRefs
+ status: "True"
+ type: ResolvedRefs
+ name: foo
+ supportedKinds:
+ - group: gateway.networking.k8s.io
+ kind: UDPRoute
+ - attachedRoutes: 1
+ conditions:
+ - lastTransitionTime: null
+ message: Sending translated listener configuration to the data plane
+ reason: Programmed
+ status: "True"
+ type: Programmed
+ - lastTransitionTime: null
+ message: Listener has been successfully translated
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: Listener references have been resolved
+ reason: ResolvedRefs
+ status: "True"
+ type: ResolvedRefs
+ name: bar
+ supportedKinds:
+ - group: gateway.networking.k8s.io
+ kind: UDPRoute
+ - attachedRoutes: 1
+ conditions:
+ - lastTransitionTime: null
+ message: Sending translated listener configuration to the data plane
+ reason: Programmed
+ status: "True"
+ type: Programmed
+ - lastTransitionTime: null
+ message: Listener has been successfully translated
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: Listener references have been resolved
+ reason: ResolvedRefs
+ status: "True"
+ type: ResolvedRefs
+ name: baz
+ supportedKinds:
+ - group: gateway.networking.k8s.io
+ kind: UDPRoute
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ name: gateway-mixed
+ namespace: envoy-gateway
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - allowedRoutes:
+ namespaces:
+ from: All
+ name: http
+ port: 80
+ protocol: HTTP
+ - allowedRoutes:
+ namespaces:
+ from: All
+ name: foo
+ port: 8443
+ protocol: UDP
+ status:
+ listeners:
+ - attachedRoutes: 1
+ conditions:
+ - lastTransitionTime: null
+ message: Sending translated listener configuration to the data plane
+ reason: Programmed
+ status: "True"
+ type: Programmed
+ - lastTransitionTime: null
+ message: Listener has been successfully translated
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: Listener references have been resolved
+ reason: ResolvedRefs
+ status: "True"
+ type: ResolvedRefs
+ name: http
+ supportedKinds:
+ - group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ - group: gateway.networking.k8s.io
+ kind: GRPCRoute
+ - attachedRoutes: 1
+ conditions:
+ - lastTransitionTime: null
+ message: Sending translated listener configuration to the data plane
+ reason: Programmed
+ status: "True"
+ type: Programmed
+ - lastTransitionTime: null
+ message: Listener has been successfully translated
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: Listener references have been resolved
+ reason: ResolvedRefs
+ status: "True"
+ type: ResolvedRefs
+ name: foo
+ supportedKinds:
+ - group: gateway.networking.k8s.io
+ kind: UDPRoute
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: HTTPRoute
+ metadata:
+ name: hr-app-mixed
+ namespace: default
+ spec:
+ parentRefs:
+ - name: gateway-mixed
+ namespace: envoy-gateway
+ sectionName: http
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8080
+ matches:
+ - path:
+ type: PathPrefix
+ value: /
+ status:
+ parents:
+ - conditions:
+ - lastTransitionTime: null
+ message: Route is accepted
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: Resolved all the Object references for the Route
+ reason: ResolvedRefs
+ status: "True"
+ type: ResolvedRefs
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+ parentRef:
+ name: gateway-mixed
+ namespace: envoy-gateway
+ sectionName: http
+infraIR:
+ envoy-gateway/gateway-mixed:
+ proxy:
+ listeners:
+ - name: envoy-gateway/gateway-mixed/http
+ ports:
+ - containerPort: 10080
+ name: http-80
+ protocol: HTTP
+ servicePort: 80
+ - name: envoy-gateway/gateway-mixed/foo
+ ports:
+ - containerPort: 8443
+ name: udp-8443
+ protocol: UDP
+ servicePort: 8443
+ metadata:
+ labels:
+ gateway.envoyproxy.io/owning-gateway-name: gateway-mixed
+ gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+ ownerReference:
+ kind: GatewayClass
+ name: envoy-gateway-class
+ name: envoy-gateway/gateway-mixed
+ namespace: envoy-gateway-system
+ envoy-gateway/gateway-udp:
+ proxy:
+ listeners:
+ - name: envoy-gateway/gateway-udp/foo
+ ports:
+ - containerPort: 8088
+ name: udp-8088
+ protocol: UDP
+ servicePort: 8088
+ - name: envoy-gateway/gateway-udp/bar
+ ports:
+ - containerPort: 8089
+ name: udp-8089
+ protocol: UDP
+ servicePort: 8089
+ - name: envoy-gateway/gateway-udp/baz
+ ports:
+ - containerPort: 8090
+ name: udp-8090
+ protocol: UDP
+ servicePort: 8090
+ metadata:
+ labels:
+ gateway.envoyproxy.io/owning-gateway-name: gateway-udp
+ gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+ ownerReference:
+ kind: GatewayClass
+ name: envoy-gateway-class
+ name: envoy-gateway/gateway-udp
+ namespace: envoy-gateway-system
+securityPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ name: sp-udpr-app-baz-section
+ namespace: default
+ spec:
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ name: allow-route-baz
+ principal:
+ clientCIDRs:
+ - 10.10.6.0/24
+ targetRef:
+ group: gateway.networking.k8s.io
+ kind: UDPRoute
+ name: udpr-app-baz
+ sectionName: rule-baz
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-udp
+ namespace: envoy-gateway
+ sectionName: baz
+ conditions:
+ - lastTransitionTime: null
+ message: Policy has been accepted.
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: spec.targetRef is deprecated, use spec.targetRefs instead
+ reason: DeprecatedField
+ status: "True"
+ type: Warning
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ name: sp-udpr-app-bar
+ namespace: default
+ spec:
+ authorization:
+ defaultAction: Allow
+ rules:
+ - action: Deny
+ name: deny-route-bar
+ principal:
+ clientCIDRs:
+ - 10.10.2.0/24
+ targetRef:
+ group: gateway.networking.k8s.io
+ kind: UDPRoute
+ name: udpr-app-bar
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-udp
+ namespace: envoy-gateway
+ sectionName: bar
+ conditions:
+ - lastTransitionTime: null
+ message: Policy has been accepted.
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: spec.targetRef is deprecated, use spec.targetRefs instead
+ reason: DeprecatedField
+ status: "True"
+ type: Warning
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ name: sp-udpr-app-conflict
+ namespace: default
+ spec:
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ name: allow-route-conflict
+ principal:
+ clientCIDRs:
+ - 10.10.3.0/24
+ targetRef:
+ group: gateway.networking.k8s.io
+ kind: UDPRoute
+ name: udpr-app-bar
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-udp
+ namespace: envoy-gateway
+ sectionName: bar
+ conditions:
+ - lastTransitionTime: null
+ message: Unable to target UDPRoute udpr-app-bar, another SecurityPolicy has
+ already attached to it
+ reason: Conflicted
+ status: "False"
+ type: Accepted
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ name: sp-gw-udp-section-foo
+ namespace: envoy-gateway
+ spec:
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ name: allow-section-foo
+ principal:
+ clientCIDRs:
+ - 10.10.1.0/24
+ targetRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-udp
+ sectionName: foo
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-udp
+ namespace: envoy-gateway
+ sectionName: foo
+ conditions:
+ - lastTransitionTime: null
+ message: Policy has been accepted.
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: spec.targetRef is deprecated, use spec.targetRefs instead
+ reason: DeprecatedField
+ status: "True"
+ type: Warning
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ name: sp-udpr-app-invalid-section
+ namespace: envoy-gateway
+ spec:
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ name: allow-invalid-section
+ principal:
+ clientCIDRs:
+ - 10.10.4.0/24
+ targetRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-udp
+ sectionName: bogus
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-udp
+ namespace: envoy-gateway
+ sectionName: bogus
+ conditions:
+ - lastTransitionTime: null
+ message: No section name bogus found for Gateway envoy-gateway/gateway-udp
+ reason: TargetNotFound
+ status: "False"
+ type: Accepted
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ name: sp-gw-udp-whole
+ namespace: envoy-gateway
+ spec:
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ name: allow-gateway
+ principal:
+ clientCIDRs:
+ - 10.10.0.0/24
+ targetRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-udp
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-udp
+ namespace: envoy-gateway
+ conditions:
+ - lastTransitionTime: null
+ message: Policy has been accepted.
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: 'This policy is being overridden by other securityPolicies for these
+ gateway listeners: [envoy-gateway/gateway-udp/foo] and these routes: [default/udpr-app-bar
+ default/udpr-app-baz]'
+ reason: Overridden
+ status: "True"
+ type: Overridden
+ - lastTransitionTime: null
+ message: spec.targetRef is deprecated, use spec.targetRefs instead
+ reason: DeprecatedField
+ status: "True"
+ type: Warning
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ name: sp-mixed-gateway
+ namespace: envoy-gateway
+ spec:
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ principal:
+ clientCIDRs:
+ - 10.10.5.0/24
+ cors:
+ allowMethods:
+ - GET
+ - POST
+ allowOrigins:
+ - https://example.com
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-mixed
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-mixed
+ namespace: envoy-gateway
+ conditions:
+ - lastTransitionTime: null
+ message: Policy has been accepted.
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+udpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: UDPRoute
+ metadata:
+ name: udpr-app-foo
+ namespace: default
+ spec:
+ parentRefs:
+ - name: gateway-udp
+ namespace: envoy-gateway
+ sectionName: foo
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8163
+ status:
+ parents:
+ - conditions:
+ - lastTransitionTime: null
+ message: Route is accepted
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: UDP Port 8163 not found on Service default/service-1
+ reason: PortNotFound
+ status: "False"
+ type: ResolvedRefs
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+ parentRef:
+ name: gateway-udp
+ namespace: envoy-gateway
+ sectionName: foo
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: UDPRoute
+ metadata:
+ name: udpr-app-bar
+ namespace: default
+ spec:
+ parentRefs:
+ - name: gateway-udp
+ namespace: envoy-gateway
+ sectionName: bar
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8163
+ status:
+ parents:
+ - conditions:
+ - lastTransitionTime: null
+ message: Route is accepted
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: UDP Port 8163 not found on Service default/service-1
+ reason: PortNotFound
+ status: "False"
+ type: ResolvedRefs
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+ parentRef:
+ name: gateway-udp
+ namespace: envoy-gateway
+ sectionName: bar
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: UDPRoute
+ metadata:
+ name: udpr-app-baz
+ namespace: default
+ spec:
+ parentRefs:
+ - name: gateway-udp
+ namespace: envoy-gateway
+ sectionName: baz
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8163
+ name: rule-baz
+ status:
+ parents:
+ - conditions:
+ - lastTransitionTime: null
+ message: Route is accepted
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: UDP Port 8163 not found on Service default/service-1
+ reason: PortNotFound
+ status: "False"
+ type: ResolvedRefs
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+ parentRef:
+ name: gateway-udp
+ namespace: envoy-gateway
+ sectionName: baz
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: UDPRoute
+ metadata:
+ name: udpr-app-mixed
+ namespace: default
+ spec:
+ parentRefs:
+ - name: gateway-mixed
+ namespace: envoy-gateway
+ sectionName: foo
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8163
+ status:
+ parents:
+ - conditions:
+ - lastTransitionTime: null
+ message: Route is accepted
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: UDP Port 8163 not found on Service default/service-1
+ reason: PortNotFound
+ status: "False"
+ type: ResolvedRefs
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+ parentRef:
+ name: gateway-mixed
+ namespace: envoy-gateway
+ sectionName: foo
+xdsIR:
+ envoy-gateway/gateway-mixed:
+ accessLog:
+ json:
+ - path: /dev/stdout
+ globalResources:
+ proxyServiceCluster:
+ metadata:
+ kind: Service
+ name: envoy-envoy-gateway-gateway-mixed-94aaf8eb
+ namespace: envoy-gateway-system
+ sectionName: "8080"
+ name: envoy-gateway/gateway-mixed
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.6.5.4
+ port: 8080
+ zone: zone1
+ metadata:
+ kind: Service
+ name: envoy-envoy-gateway-gateway-mixed-94aaf8eb
+ namespace: envoy-gateway-system
+ sectionName: "8080"
+ name: envoy-gateway/gateway-mixed
+ protocol: TCP
+ http:
+ - address: 0.0.0.0
+ externalPort: 80
+ hostnames:
+ - '*'
+ metadata:
+ kind: Gateway
+ name: gateway-mixed
+ namespace: envoy-gateway
+ sectionName: http
+ name: envoy-gateway/gateway-mixed/http
+ path:
+ escapedSlashesAction: UnescapeAndRedirect
+ mergeSlashes: true
+ port: 10080
+ routes:
+ - destination:
+ metadata:
+ kind: HTTPRoute
+ name: hr-app-mixed
+ namespace: default
+ name: httproute/default/hr-app-mixed/rule/0
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.7.7.7
+ port: 8080
+ metadata:
+ kind: Service
+ name: service-1
+ namespace: default
+ sectionName: "8080"
+ name: httproute/default/hr-app-mixed/rule/0/backend/0
+ protocol: HTTP
+ weight: 1
+ hostname: '*'
+ isHTTP2: false
+ metadata:
+ kind: HTTPRoute
+ name: hr-app-mixed
+ namespace: default
+ name: httproute/default/hr-app-mixed/rule/0/match/0/*
+ pathMatch:
+ distinct: false
+ name: ""
+ prefix: /
+ security:
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ name: securitypolicy/envoy-gateway/sp-mixed-gateway/authorization/rule/0
+ principal:
+ clientCIDRs:
+ - cidr: 10.10.5.0/24
+ distinct: false
+ invert: false
+ isIPv6: false
+ maskLen: 24
+ cors:
+ allowMethods:
+ - GET
+ - POST
+ allowOrigins:
+ - distinct: false
+ exact: https://example.com
+ name: ""
+ readyListener:
+ address: 0.0.0.0
+ ipFamily: IPv4
+ path: /ready
+ port: 19003
+ udp:
+ - address: 0.0.0.0
+ externalPort: 8443
+ metadata:
+ kind: Gateway
+ name: gateway-mixed
+ namespace: envoy-gateway
+ sectionName: foo
+ name: envoy-gateway/gateway-mixed/foo
+ port: 8443
+ route:
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ name: securitypolicy/envoy-gateway/sp-mixed-gateway/authorization/rule/0
+ principal:
+ clientCIDRs:
+ - cidr: 10.10.5.0/24
+ distinct: false
+ invert: false
+ isIPv6: false
+ maskLen: 24
+ destination:
+ metadata:
+ kind: UDPRoute
+ name: udpr-app-mixed
+ namespace: default
+ name: udproute/default/udpr-app-mixed/rule/-1
+ name: udproute/default/udpr-app-mixed
+ envoy-gateway/gateway-udp:
+ accessLog:
+ json:
+ - path: /dev/stdout
+ globalResources:
+ proxyServiceCluster:
+ metadata:
+ kind: Service
+ name: envoy-envoy-gateway-gateway-udp-2934226e
+ namespace: envoy-gateway-system
+ sectionName: "8080"
+ name: envoy-gateway/gateway-udp
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.6.5.4
+ port: 8080
+ zone: zone1
+ metadata:
+ kind: Service
+ name: envoy-envoy-gateway-gateway-udp-2934226e
+ namespace: envoy-gateway-system
+ sectionName: "8080"
+ name: envoy-gateway/gateway-udp
+ protocol: TCP
+ readyListener:
+ address: 0.0.0.0
+ ipFamily: IPv4
+ path: /ready
+ port: 19003
+ udp:
+ - address: 0.0.0.0
+ externalPort: 8088
+ metadata:
+ kind: Gateway
+ name: gateway-udp
+ namespace: envoy-gateway
+ sectionName: foo
+ name: envoy-gateway/gateway-udp/foo
+ port: 8088
+ route:
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ name: allow-section-foo
+ principal:
+ clientCIDRs:
+ - cidr: 10.10.1.0/24
+ distinct: false
+ invert: false
+ isIPv6: false
+ maskLen: 24
+ destination:
+ metadata:
+ kind: UDPRoute
+ name: udpr-app-foo
+ namespace: default
+ name: udproute/default/udpr-app-foo/rule/-1
+ name: udproute/default/udpr-app-foo
+ - address: 0.0.0.0
+ externalPort: 8089
+ metadata:
+ kind: Gateway
+ name: gateway-udp
+ namespace: envoy-gateway
+ sectionName: bar
+ name: envoy-gateway/gateway-udp/bar
+ port: 8089
+ route:
+ authorization:
+ defaultAction: Allow
+ rules:
+ - action: Deny
+ name: deny-route-bar
+ principal:
+ clientCIDRs:
+ - cidr: 10.10.2.0/24
+ distinct: false
+ invert: false
+ isIPv6: false
+ maskLen: 24
+ destination:
+ metadata:
+ kind: UDPRoute
+ name: udpr-app-bar
+ namespace: default
+ name: udproute/default/udpr-app-bar/rule/-1
+ name: udproute/default/udpr-app-bar
+ - address: 0.0.0.0
+ externalPort: 8090
+ metadata:
+ kind: Gateway
+ name: gateway-udp
+ namespace: envoy-gateway
+ sectionName: baz
+ name: envoy-gateway/gateway-udp/baz
+ port: 8090
+ route:
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ name: allow-route-baz
+ principal:
+ clientCIDRs:
+ - cidr: 10.10.6.0/24
+ distinct: false
+ invert: false
+ isIPv6: false
+ maskLen: 24
+ destination:
+ metadata:
+ kind: UDPRoute
+ name: udpr-app-baz
+ namespace: default
+ sectionName: rule-baz
+ name: udproute/default/udpr-app-baz/rule/-1
+ name: udproute/default/udpr-app-baz
diff --git a/internal/gatewayapi/testdata/udproute-securitypolicy-without-authorization.in.yaml b/internal/gatewayapi/testdata/udproute-securitypolicy-without-authorization.in.yaml
new file mode 100644
index 00000000000..21d0d3892c5
--- /dev/null
+++ b/internal/gatewayapi/testdata/udproute-securitypolicy-without-authorization.in.yaml
@@ -0,0 +1,41 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ namespace: envoy-gateway
+ name: gateway-udp
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - name: udp
+ protocol: UDP
+ port: 8088
+ allowedRoutes:
+ namespaces:
+ from: All
+udpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: UDPRoute
+ metadata:
+ namespace: default
+ name: udpr-app
+ spec:
+ parentRefs:
+ - namespace: envoy-gateway
+ name: gateway-udp
+ sectionName: udp
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8163
+securityPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ namespace: default
+ name: sp-udpr-no-auth
+ spec:
+ targetRef:
+ group: gateway.networking.k8s.io
+ kind: UDPRoute
+ name: udpr-app
diff --git a/internal/gatewayapi/testdata/udproute-securitypolicy-without-authorization.out.yaml b/internal/gatewayapi/testdata/udproute-securitypolicy-without-authorization.out.yaml
new file mode 100644
index 00000000000..f2a4351ecee
--- /dev/null
+++ b/internal/gatewayapi/testdata/udproute-securitypolicy-without-authorization.out.yaml
@@ -0,0 +1,170 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: Gateway
+ metadata:
+ name: gateway-udp
+ namespace: envoy-gateway
+ spec:
+ gatewayClassName: envoy-gateway-class
+ listeners:
+ - allowedRoutes:
+ namespaces:
+ from: All
+ name: udp
+ port: 8088
+ protocol: UDP
+ status:
+ listeners:
+ - attachedRoutes: 1
+ conditions:
+ - lastTransitionTime: null
+ message: Sending translated listener configuration to the data plane
+ reason: Programmed
+ status: "True"
+ type: Programmed
+ - lastTransitionTime: null
+ message: Listener has been successfully translated
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: Listener references have been resolved
+ reason: ResolvedRefs
+ status: "True"
+ type: ResolvedRefs
+ name: udp
+ supportedKinds:
+ - group: gateway.networking.k8s.io
+ kind: UDPRoute
+infraIR:
+ envoy-gateway/gateway-udp:
+ proxy:
+ listeners:
+ - name: envoy-gateway/gateway-udp/udp
+ ports:
+ - containerPort: 8088
+ name: udp-8088
+ protocol: UDP
+ servicePort: 8088
+ metadata:
+ labels:
+ gateway.envoyproxy.io/owning-gateway-name: gateway-udp
+ gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+ ownerReference:
+ kind: GatewayClass
+ name: envoy-gateway-class
+ name: envoy-gateway/gateway-udp
+ namespace: envoy-gateway-system
+securityPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+ kind: SecurityPolicy
+ metadata:
+ name: sp-udpr-no-auth
+ namespace: default
+ spec:
+ targetRef:
+ group: gateway.networking.k8s.io
+ kind: UDPRoute
+ name: udpr-app
+ status:
+ ancestors:
+ - ancestorRef:
+ group: gateway.networking.k8s.io
+ kind: Gateway
+ name: gateway-udp
+ namespace: envoy-gateway
+ sectionName: udp
+ conditions:
+ - lastTransitionTime: null
+ message: Policy has been accepted.
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: spec.targetRef is deprecated, use spec.targetRefs instead
+ reason: DeprecatedField
+ status: "True"
+ type: Warning
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+udpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+ kind: UDPRoute
+ metadata:
+ name: udpr-app
+ namespace: default
+ spec:
+ parentRefs:
+ - name: gateway-udp
+ namespace: envoy-gateway
+ sectionName: udp
+ rules:
+ - backendRefs:
+ - name: service-1
+ port: 8163
+ status:
+ parents:
+ - conditions:
+ - lastTransitionTime: null
+ message: Route is accepted
+ reason: Accepted
+ status: "True"
+ type: Accepted
+ - lastTransitionTime: null
+ message: UDP Port 8163 not found on Service default/service-1
+ reason: PortNotFound
+ status: "False"
+ type: ResolvedRefs
+ controllerName: gateway.envoyproxy.io/gatewayclass-controller
+ parentRef:
+ name: gateway-udp
+ namespace: envoy-gateway
+ sectionName: udp
+xdsIR:
+ envoy-gateway/gateway-udp:
+ accessLog:
+ json:
+ - path: /dev/stdout
+ globalResources:
+ proxyServiceCluster:
+ metadata:
+ kind: Service
+ name: envoy-envoy-gateway-gateway-udp-2934226e
+ namespace: envoy-gateway-system
+ sectionName: "8080"
+ name: envoy-gateway/gateway-udp
+ settings:
+ - addressType: IP
+ endpoints:
+ - host: 7.6.5.4
+ port: 8080
+ zone: zone1
+ metadata:
+ kind: Service
+ name: envoy-envoy-gateway-gateway-udp-2934226e
+ namespace: envoy-gateway-system
+ sectionName: "8080"
+ name: envoy-gateway/gateway-udp
+ protocol: TCP
+ readyListener:
+ address: 0.0.0.0
+ ipFamily: IPv4
+ path: /ready
+ port: 19003
+ udp:
+ - address: 0.0.0.0
+ externalPort: 8088
+ metadata:
+ kind: Gateway
+ name: gateway-udp
+ namespace: envoy-gateway
+ sectionName: udp
+ name: envoy-gateway/gateway-udp/udp
+ port: 8088
+ route:
+ destination:
+ metadata:
+ kind: UDPRoute
+ name: udpr-app
+ namespace: default
+ name: udproute/default/udpr-app/rule/-1
+ name: udproute/default/udpr-app
diff --git a/internal/ir/xds.go b/internal/ir/xds.go
index 07c493967fb..552fb1e0b9a 100644
--- a/internal/ir/xds.go
+++ b/internal/ir/xds.go
@@ -2815,6 +2815,8 @@ type UDPRoute struct {
LoadBalancer *LoadBalancer `json:"loadBalancer,omitempty" yaml:"loadBalancer,omitempty"`
// DNS is used to configure how DNS resolution is handled by the Envoy Proxy cluster
DNS *DNS `json:"dns,omitempty" yaml:"dns,omitempty"`
+ // Authorization defines the schema for the authorization.
+ Authorization *Authorization `json:"authorization,omitempty" yaml:"authorization,omitempty"`
}
// Validate the fields within the UDPListener structure
diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go
index 141f3f81efc..c5c649c8953 100644
--- a/internal/ir/zz_generated.deepcopy.go
+++ b/internal/ir/zz_generated.deepcopy.go
@@ -5560,6 +5560,11 @@ func (in *UDPRoute) DeepCopyInto(out *UDPRoute) {
*out = new(DNS)
(*in).DeepCopyInto(*out)
}
+ if in.Authorization != nil {
+ in, out := &in.Authorization, &out.Authorization
+ *out = new(Authorization)
+ (*in).DeepCopyInto(*out)
+ }
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UDPRoute.
diff --git a/internal/provider/kubernetes/controller.go b/internal/provider/kubernetes/controller.go
index d10482b3ab3..688578da2ca 100644
--- a/internal/provider/kubernetes/controller.go
+++ b/internal/provider/kubernetes/controller.go
@@ -1776,6 +1776,7 @@ func (r *gatewayAPIReconciler) processPolicyTargetReferenceGrants(
resource.KindHTTPRoute,
resource.KindGRPCRoute,
resource.KindTCPRoute,
+ resource.KindUDPRoute,
),
}
diff --git a/internal/xds/translator/authorization_udp.go b/internal/xds/translator/authorization_udp.go
new file mode 100644
index 00000000000..8cd8c168d6e
--- /dev/null
+++ b/internal/xds/translator/authorization_udp.go
@@ -0,0 +1,141 @@
+// Copyright Envoy Gateway Authors
+// SPDX-License-Identifier: Apache-2.0
+// The full text of the Apache license is available in the LICENSE file at
+// the root of the repo.
+
+package translator
+
+import (
+ cncfv3 "github.com/cncf/xds/go/xds/core/v3"
+ matcherv3 "github.com/cncf/xds/go/xds/type/matcher/v3"
+
+ egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
+ "github.com/envoyproxy/gateway/internal/ir"
+)
+
+// buildUDPProxyMatcher builds the matcher that udp_proxy uses to pick a route,
+// folding client-IP authorization into it.
+//
+// A UDP listener has no network filter chain, so the network RBAC filter used for
+// TCPRoutes cannot be reused here. Instead the authorization decision rides along
+// with route selection: a datagram whose source IP matches is routed to the
+// cluster, and a datagram that matches nothing is dropped by udp_proxy, which
+// counts it as downstream_sess_no_route.
+//
+// Since a denial can only be expressed as the absence of a match, the ordered
+// Allow/Deny rules are compiled into allow-only predicates. A Deny rule never
+// becomes an entry of its own; it subtracts from the Allow rules that follow it
+// and from a permissive default action.
+func buildUDPProxyMatcher(routeAction *cncfv3.TypedExtensionConfig, authorization *ir.Authorization) (*matcherv3.Matcher, error) {
+ onRoute := &matcherv3.Matcher_OnMatch{
+ OnMatch: &matcherv3.Matcher_OnMatch_Action{Action: routeAction},
+ }
+
+ if authorization == nil {
+ return &matcherv3.Matcher{OnNoMatch: onRoute}, nil
+ }
+
+ var (
+ matchers []*matcherv3.Matcher_MatcherList_FieldMatcher
+ // Predicates of the Deny rules seen so far, in rule order.
+ denied []*matcherv3.Matcher_MatcherList_Predicate
+ )
+
+ for _, rule := range authorization.Rules {
+ // Only client CIDRs are enforceable on the UDP path. The Gateway API layer
+ // rejects every other principal for L4 targets, so anything else here is a
+ // rule that cannot match rather than one that matches everything.
+ if len(rule.Principal.ClientCIDRs) == 0 {
+ continue
+ }
+
+ predicate, err := buildIPPredicate(rule.Principal.ClientCIDRs)
+ if err != nil {
+ return nil, err
+ }
+
+ if rule.Action == egv1a1.AuthorizationActionDeny {
+ denied = append(denied, predicate)
+ continue
+ }
+
+ // Rules are first-match-wins, so an Allow rule only covers the sources that
+ // no preceding Deny rule already claimed. Preceding Allow rules need no such
+ // exclusion: had one matched, the datagram would already have been routed.
+ conjuncts := []*matcherv3.Matcher_MatcherList_Predicate{predicate}
+ for _, d := range denied {
+ conjuncts = append(conjuncts, notPredicate(d))
+ }
+
+ matchers = append(matchers, &matcherv3.Matcher_MatcherList_FieldMatcher{
+ Predicate: andPredicates(conjuncts),
+ OnMatch: onRoute,
+ })
+ }
+
+ if authorization.DefaultAction == egv1a1.AuthorizationActionAllow {
+ // Nothing is denied, so every datagram is routed either way and the Allow
+ // rules are redundant. Emit the same matcher as an unauthorized listener.
+ if len(denied) == 0 {
+ return &matcherv3.Matcher{OnNoMatch: onRoute}, nil
+ }
+
+ // Route whatever no Deny rule claimed. This goes last so the Allow rules
+ // above keep their precedence.
+ matchers = append(matchers, &matcherv3.Matcher_MatcherList_FieldMatcher{
+ Predicate: notPredicate(orPredicates(denied)),
+ OnMatch: onRoute,
+ })
+ }
+
+ matcher := &matcherv3.Matcher{}
+ // An empty matcher list fails proto validation, so the matcher type is left
+ // unset when nothing can be allowed.
+ if len(matchers) > 0 {
+ matcher.MatcherType = &matcherv3.Matcher_MatcherList_{
+ MatcherList: &matcherv3.Matcher_MatcherList{Matchers: matchers},
+ }
+ }
+ // on_no_match is deliberately left unset: that absence is what makes udp_proxy
+ // drop a datagram no Allow rule accounted for.
+ return matcher, nil
+}
+
+// notPredicate negates a predicate.
+func notPredicate(predicate *matcherv3.Matcher_MatcherList_Predicate) *matcherv3.Matcher_MatcherList_Predicate {
+ return &matcherv3.Matcher_MatcherList_Predicate{
+ MatchType: &matcherv3.Matcher_MatcherList_Predicate_NotMatcher{
+ NotMatcher: predicate,
+ },
+ }
+}
+
+// andPredicates conjoins predicates. A predicate list must hold at least two
+// entries, so a lone predicate is returned as-is.
+func andPredicates(predicates []*matcherv3.Matcher_MatcherList_Predicate) *matcherv3.Matcher_MatcherList_Predicate {
+ if len(predicates) == 1 {
+ return predicates[0]
+ }
+ return &matcherv3.Matcher_MatcherList_Predicate{
+ MatchType: &matcherv3.Matcher_MatcherList_Predicate_AndMatcher{
+ AndMatcher: &matcherv3.Matcher_MatcherList_Predicate_PredicateList{
+ Predicate: predicates,
+ },
+ },
+ }
+}
+
+// orPredicates disjoins predicates. A predicate list must hold at least two
+// entries, so a lone predicate is returned as-is.
+func orPredicates(predicates []*matcherv3.Matcher_MatcherList_Predicate) *matcherv3.Matcher_MatcherList_Predicate {
+ if len(predicates) == 1 {
+ return predicates[0]
+ }
+ return &matcherv3.Matcher_MatcherList_Predicate{
+ MatchType: &matcherv3.Matcher_MatcherList_Predicate_OrMatcher{
+ OrMatcher: &matcherv3.Matcher_MatcherList_Predicate_PredicateList{
+ Predicate: predicates,
+ },
+ },
+ }
+}
diff --git a/internal/xds/translator/authorization_udp_test.go b/internal/xds/translator/authorization_udp_test.go
new file mode 100644
index 00000000000..92c1053109a
--- /dev/null
+++ b/internal/xds/translator/authorization_udp_test.go
@@ -0,0 +1,166 @@
+// Copyright Envoy Gateway Authors
+// SPDX-License-Identifier: Apache-2.0
+// The full text of the Apache license is available in the LICENSE file at
+// the root of the repo.
+
+package translator
+
+import (
+ "testing"
+
+ cncfv3 "github.com/cncf/xds/go/xds/core/v3"
+ matcherv3 "github.com/cncf/xds/go/xds/type/matcher/v3"
+ "github.com/stretchr/testify/require"
+
+ egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
+ "github.com/envoyproxy/gateway/internal/ir"
+)
+
+func cidrRule(action egv1a1.AuthorizationAction, cidr string, maskLen uint32) *ir.AuthorizationRule {
+ return &ir.AuthorizationRule{
+ Action: action,
+ Principal: ir.Principal{
+ ClientCIDRs: []*ir.CIDRMatch{{CIDR: cidr, MaskLen: maskLen}},
+ },
+ }
+}
+
+// udp_proxy can only route or not route, so a denial is expressed as the absence of a
+// match. These cases pin down how the ordered Allow/Deny rules collapse into
+// allow-only predicates, and in particular that on_no_match is set only when the
+// default action routes unconditionally.
+func TestBuildUDPProxyMatcher(t *testing.T) {
+ routeAction := &cncfv3.TypedExtensionConfig{Name: "route"}
+
+ tests := []struct {
+ name string
+ authorization *ir.Authorization
+ wantMatchers int
+ wantOnNoMatch bool
+ }{
+ {
+ name: "no authorization routes everything",
+ authorization: nil,
+ wantMatchers: 0,
+ wantOnNoMatch: true,
+ },
+ {
+ name: "allowlist emits one matcher per allow rule and drops the rest",
+ authorization: &ir.Authorization{
+ DefaultAction: egv1a1.AuthorizationActionDeny,
+ Rules: []*ir.AuthorizationRule{
+ cidrRule(egv1a1.AuthorizationActionAllow, "192.168.100.0/24", 24),
+ cidrRule(egv1a1.AuthorizationActionAllow, "10.1.0.0/16", 16),
+ },
+ },
+ wantMatchers: 2,
+ wantOnNoMatch: false,
+ },
+ {
+ name: "denylist collapses to a single negated matcher",
+ authorization: &ir.Authorization{
+ DefaultAction: egv1a1.AuthorizationActionAllow,
+ Rules: []*ir.AuthorizationRule{
+ cidrRule(egv1a1.AuthorizationActionDeny, "10.0.0.0/24", 24),
+ cidrRule(egv1a1.AuthorizationActionDeny, "172.16.0.0/12", 12),
+ },
+ },
+ wantMatchers: 1,
+ wantOnNoMatch: false,
+ },
+ {
+ name: "a preceding deny rule narrows the allow rule and the default",
+ authorization: &ir.Authorization{
+ DefaultAction: egv1a1.AuthorizationActionAllow,
+ Rules: []*ir.AuthorizationRule{
+ cidrRule(egv1a1.AuthorizationActionDeny, "10.0.0.0/8", 8),
+ cidrRule(egv1a1.AuthorizationActionAllow, "10.1.0.0/16", 16),
+ },
+ },
+ // The narrowed allow rule, plus the catch-all for the permissive default.
+ wantMatchers: 2,
+ wantOnNoMatch: false,
+ },
+ {
+ name: "an allow rule before a deny rule keeps its own entry",
+ authorization: &ir.Authorization{
+ DefaultAction: egv1a1.AuthorizationActionAllow,
+ Rules: []*ir.AuthorizationRule{
+ cidrRule(egv1a1.AuthorizationActionAllow, "10.1.0.0/16", 16),
+ cidrRule(egv1a1.AuthorizationActionDeny, "10.0.0.0/8", 8),
+ cidrRule(egv1a1.AuthorizationActionAllow, "10.0.0.0/7", 7),
+ },
+ },
+ // The first allow rule needs no exclusion, the second is narrowed by
+ // the deny between them, and the permissive default adds a catch-all.
+ wantMatchers: 3,
+ wantOnNoMatch: false,
+ },
+ {
+ name: "an allow default with nothing denied is the same as no authorization",
+ authorization: &ir.Authorization{
+ DefaultAction: egv1a1.AuthorizationActionAllow,
+ Rules: []*ir.AuthorizationRule{
+ cidrRule(egv1a1.AuthorizationActionAllow, "192.168.100.0/24", 24),
+ },
+ },
+ wantMatchers: 0,
+ wantOnNoMatch: true,
+ },
+ {
+ name: "a deny default with no allow rules drops everything",
+ authorization: &ir.Authorization{
+ DefaultAction: egv1a1.AuthorizationActionDeny,
+ },
+ wantMatchers: 0,
+ wantOnNoMatch: false,
+ },
+ {
+ name: "rules without client CIDRs cannot match and are skipped",
+ authorization: &ir.Authorization{
+ DefaultAction: egv1a1.AuthorizationActionDeny,
+ Rules: []*ir.AuthorizationRule{
+ {Action: egv1a1.AuthorizationActionAllow},
+ cidrRule(egv1a1.AuthorizationActionAllow, "192.168.100.0/24", 24),
+ },
+ },
+ wantMatchers: 1,
+ wantOnNoMatch: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ m, err := buildUDPProxyMatcher(routeAction, tt.authorization)
+ require.NoError(t, err)
+
+ require.Len(t, m.GetMatcherList().GetMatchers(), tt.wantMatchers)
+ require.Equal(t, tt.wantOnNoMatch, m.GetOnNoMatch() != nil)
+
+ // An empty matcher list fails proto validation, so it must be left unset
+ // rather than emitted empty.
+ if tt.wantMatchers == 0 {
+ require.Nil(t, m.GetMatcherList())
+ }
+ for _, fm := range m.GetMatcherList().GetMatchers() {
+ require.NotNil(t, fm.GetPredicate())
+ require.NotNil(t, fm.GetOnMatch())
+ }
+ })
+ }
+}
+
+// A predicate list must hold at least two entries, so a lone predicate has to be
+// returned bare rather than wrapped.
+func TestPredicateCombinatorsSkipSingletonLists(t *testing.T) {
+ single := &matcherv3.Matcher_MatcherList_Predicate{
+ MatchType: &matcherv3.Matcher_MatcherList_Predicate_SinglePredicate_{},
+ }
+
+ require.Same(t, single, andPredicates([]*matcherv3.Matcher_MatcherList_Predicate{single}))
+ require.Same(t, single, orPredicates([]*matcherv3.Matcher_MatcherList_Predicate{single}))
+
+ require.Len(t, andPredicates([]*matcherv3.Matcher_MatcherList_Predicate{single, single}).GetAndMatcher().GetPredicate(), 2)
+ require.Len(t, orPredicates([]*matcherv3.Matcher_MatcherList_Predicate{single, single}).GetOrMatcher().GetPredicate(), 2)
+ require.Same(t, single, notPredicate(single).GetNotMatcher())
+}
diff --git a/internal/xds/translator/listener.go b/internal/xds/translator/listener.go
index 0bbf0eb7ba6..ca93bfd9924 100644
--- a/internal/xds/translator/listener.go
+++ b/internal/xds/translator/listener.go
@@ -13,7 +13,6 @@ import (
"strings"
xdscore "github.com/cncf/xds/go/xds/core/v3"
- matcher "github.com/cncf/xds/go/xds/type/matcher/v3"
corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
listenerv3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
tls_inspectorv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/listener/tls_inspector/v3"
@@ -1183,6 +1182,21 @@ func buildXdsUDPListener(
return nil, err
}
+ var authorization *ir.Authorization
+ if udpListener.Route != nil {
+ authorization = udpListener.Route.Authorization
+ }
+ routeMatcher, err := buildUDPProxyMatcher(
+ &xdscore.TypedExtensionConfig{
+ Name: "route",
+ TypedConfig: routeAny,
+ },
+ authorization,
+ )
+ if err != nil {
+ return nil, err
+ }
+
al, error := buildXdsAccessLog(accesslog, ir.ProxyAccessLogTypeRoute)
if error != nil {
return nil, error
@@ -1197,16 +1211,7 @@ func buildXdsUDPListener(
AccessLog: al,
HashPolicies: udpProxyHashPolicies,
RouteSpecifier: &udpv3.UdpProxyConfig_Matcher{
- Matcher: &matcher.Matcher{
- OnNoMatch: &matcher.Matcher_OnMatch{
- OnMatch: &matcher.Matcher_OnMatch_Action{
- Action: &xdscore.TypedExtensionConfig{
- Name: "route",
- TypedConfig: routeAny,
- },
- },
- },
- },
+ Matcher: routeMatcher,
},
}
udpProxyAny, err := proto.ToAnyWithValidation(udpProxy)
diff --git a/internal/xds/translator/testdata/in/xds-ir/udp-route-authorization.yaml b/internal/xds/translator/testdata/in/xds-ir/udp-route-authorization.yaml
new file mode 100644
index 00000000000..dddf58661cb
--- /dev/null
+++ b/internal/xds/translator/testdata/in/xds-ir/udp-route-authorization.yaml
@@ -0,0 +1,141 @@
+udp:
+# Allowlist: a Deny default with Allow rules, so only the listed sources are routed
+# and everything else falls through to no match and is dropped.
+- name: "udp-listener-allowlist"
+ address: "::"
+ port: 10080
+ route:
+ name: "udp-route-allowlist"
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ name: allow-corp
+ principal:
+ clientCIDRs:
+ - cidr: 192.168.100.0/24
+ maskLen: 24
+ - action: Allow
+ name: allow-branch
+ principal:
+ clientCIDRs:
+ - cidr: 10.1.0.0/16
+ maskLen: 16
+ - cidr: 2001:db8::/64
+ isIPv6: true
+ maskLen: 64
+ destination:
+ name: "udp-route-allowlist-dest"
+ settings:
+ - endpoints:
+ - host: "10.2.3.4"
+ port: 50000
+ name: "udp-route-allowlist-dest/backend/0"
+# Denylist: an Allow default with Deny rules, which compiles to a single negated
+# predicate covering every denied source.
+- name: "udp-listener-denylist"
+ address: "::"
+ port: 10081
+ route:
+ name: "udp-route-denylist"
+ authorization:
+ defaultAction: Allow
+ rules:
+ - action: Deny
+ name: deny-office
+ principal:
+ clientCIDRs:
+ - cidr: 10.0.0.0/24
+ maskLen: 24
+ - action: Deny
+ name: deny-lab
+ principal:
+ clientCIDRs:
+ - cidr: 172.16.0.0/12
+ maskLen: 12
+ destination:
+ name: "udp-route-denylist-dest"
+ settings:
+ - endpoints:
+ - host: "10.2.3.5"
+ port: 50000
+ name: "udp-route-denylist-dest/backend/0"
+# Mixed ordering, covering both directions in one policy. Rules are
+# first-match-wins, so with an Allow default this must route 10.1.x (the first
+# Allow beats the later Deny), drop the rest of 10.x, route 11.x (matched by the
+# third rule, which the Deny only partly covers), and route everything else.
+- name: "udp-listener-mixed"
+ address: "::"
+ port: 10082
+ route:
+ name: "udp-route-mixed"
+ authorization:
+ defaultAction: Allow
+ rules:
+ # Precedes the Deny below, so it needs no exclusion and stays reachable.
+ - action: Allow
+ name: allow-branch
+ principal:
+ clientCIDRs:
+ - cidr: 10.1.0.0/16
+ maskLen: 16
+ - action: Deny
+ name: deny-office
+ principal:
+ clientCIDRs:
+ - cidr: 10.0.0.0/8
+ maskLen: 8
+ # Follows the Deny and only partly overlaps it, so the emitted predicate is
+ # the Allow range minus the denied one — 11.0.0.0/8 in effect.
+ - action: Allow
+ name: allow-partner
+ principal:
+ clientCIDRs:
+ - cidr: 10.0.0.0/7
+ maskLen: 7
+ destination:
+ name: "udp-route-mixed-dest"
+ settings:
+ - endpoints:
+ - host: "10.2.3.6"
+ port: 50000
+ name: "udp-route-mixed-dest/backend/0"
+# Deny all: a Deny default with no Allow rules leaves nothing to match on, so the
+# matcher has neither a matcher list nor an on_no_match and every datagram is dropped.
+- name: "udp-listener-deny-all"
+ address: "::"
+ port: 10083
+ route:
+ name: "udp-route-deny-all"
+ authorization:
+ defaultAction: Deny
+ destination:
+ name: "udp-route-deny-all-dest"
+ settings:
+ - endpoints:
+ - host: "10.2.3.7"
+ port: 50000
+ name: "udp-route-deny-all-dest/backend/0"
+# Allow all: an Allow default with no Deny rules routes everything, so the matcher
+# is identical to a listener without authorization.
+- name: "udp-listener-allow-all"
+ address: "::"
+ port: 10084
+ route:
+ name: "udp-route-allow-all"
+ authorization:
+ defaultAction: Allow
+ rules:
+ - action: Allow
+ name: allow-corp
+ principal:
+ clientCIDRs:
+ - cidr: 192.168.100.0/24
+ maskLen: 24
+ destination:
+ name: "udp-route-allow-all-dest"
+ settings:
+ - endpoints:
+ - host: "10.2.3.8"
+ port: 50000
+ name: "udp-route-allow-all-dest/backend/0"
diff --git a/internal/xds/translator/testdata/out/xds-ir/udp-route-authorization.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/udp-route-authorization.clusters.yaml
new file mode 100644
index 00000000000..608368a5025
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/udp-route-authorization.clusters.yaml
@@ -0,0 +1,115 @@
+- circuitBreakers:
+ thresholds:
+ - maxRetries: 1024
+ commonLbConfig: {}
+ connectTimeout: 10s
+ dnsLookupFamily: V4_PREFERRED
+ edsClusterConfig:
+ edsConfig:
+ ads: {}
+ resourceApiVersion: V3
+ serviceName: udp-route-allowlist-dest
+ ignoreHealthOnHostRemoval: true
+ loadBalancingPolicy:
+ policies:
+ - typedExtensionConfig:
+ name: envoy.load_balancing_policies.least_request
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest
+ localityLbConfig:
+ localityWeightedLbConfig: {}
+ name: udp-route-allowlist-dest
+ perConnectionBufferLimitBytes: 32768
+ type: EDS
+- circuitBreakers:
+ thresholds:
+ - maxRetries: 1024
+ commonLbConfig: {}
+ connectTimeout: 10s
+ dnsLookupFamily: V4_PREFERRED
+ edsClusterConfig:
+ edsConfig:
+ ads: {}
+ resourceApiVersion: V3
+ serviceName: udp-route-denylist-dest
+ ignoreHealthOnHostRemoval: true
+ loadBalancingPolicy:
+ policies:
+ - typedExtensionConfig:
+ name: envoy.load_balancing_policies.least_request
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest
+ localityLbConfig:
+ localityWeightedLbConfig: {}
+ name: udp-route-denylist-dest
+ perConnectionBufferLimitBytes: 32768
+ type: EDS
+- circuitBreakers:
+ thresholds:
+ - maxRetries: 1024
+ commonLbConfig: {}
+ connectTimeout: 10s
+ dnsLookupFamily: V4_PREFERRED
+ edsClusterConfig:
+ edsConfig:
+ ads: {}
+ resourceApiVersion: V3
+ serviceName: udp-route-mixed-dest
+ ignoreHealthOnHostRemoval: true
+ loadBalancingPolicy:
+ policies:
+ - typedExtensionConfig:
+ name: envoy.load_balancing_policies.least_request
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest
+ localityLbConfig:
+ localityWeightedLbConfig: {}
+ name: udp-route-mixed-dest
+ perConnectionBufferLimitBytes: 32768
+ type: EDS
+- circuitBreakers:
+ thresholds:
+ - maxRetries: 1024
+ commonLbConfig: {}
+ connectTimeout: 10s
+ dnsLookupFamily: V4_PREFERRED
+ edsClusterConfig:
+ edsConfig:
+ ads: {}
+ resourceApiVersion: V3
+ serviceName: udp-route-deny-all-dest
+ ignoreHealthOnHostRemoval: true
+ loadBalancingPolicy:
+ policies:
+ - typedExtensionConfig:
+ name: envoy.load_balancing_policies.least_request
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest
+ localityLbConfig:
+ localityWeightedLbConfig: {}
+ name: udp-route-deny-all-dest
+ perConnectionBufferLimitBytes: 32768
+ type: EDS
+- circuitBreakers:
+ thresholds:
+ - maxRetries: 1024
+ commonLbConfig: {}
+ connectTimeout: 10s
+ dnsLookupFamily: V4_PREFERRED
+ edsClusterConfig:
+ edsConfig:
+ ads: {}
+ resourceApiVersion: V3
+ serviceName: udp-route-allow-all-dest
+ ignoreHealthOnHostRemoval: true
+ loadBalancingPolicy:
+ policies:
+ - typedExtensionConfig:
+ name: envoy.load_balancing_policies.least_request
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.load_balancing_policies.least_request.v3.LeastRequest
+ localityLbConfig:
+ localityWeightedLbConfig: {}
+ name: udp-route-allow-all-dest
+ perConnectionBufferLimitBytes: 32768
+ type: EDS
diff --git a/internal/xds/translator/testdata/out/xds-ir/udp-route-authorization.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/udp-route-authorization.endpoints.yaml
new file mode 100644
index 00000000000..a05b26d41f5
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/udp-route-authorization.endpoints.yaml
@@ -0,0 +1,60 @@
+- clusterName: udp-route-allowlist-dest
+ endpoints:
+ - lbEndpoints:
+ - endpoint:
+ address:
+ socketAddress:
+ address: 10.2.3.4
+ portValue: 50000
+ loadBalancingWeight: 1
+ loadBalancingWeight: 1
+ locality:
+ region: udp-route-allowlist-dest/backend/0
+- clusterName: udp-route-denylist-dest
+ endpoints:
+ - lbEndpoints:
+ - endpoint:
+ address:
+ socketAddress:
+ address: 10.2.3.5
+ portValue: 50000
+ loadBalancingWeight: 1
+ loadBalancingWeight: 1
+ locality:
+ region: udp-route-denylist-dest/backend/0
+- clusterName: udp-route-mixed-dest
+ endpoints:
+ - lbEndpoints:
+ - endpoint:
+ address:
+ socketAddress:
+ address: 10.2.3.6
+ portValue: 50000
+ loadBalancingWeight: 1
+ loadBalancingWeight: 1
+ locality:
+ region: udp-route-mixed-dest/backend/0
+- clusterName: udp-route-deny-all-dest
+ endpoints:
+ - lbEndpoints:
+ - endpoint:
+ address:
+ socketAddress:
+ address: 10.2.3.7
+ portValue: 50000
+ loadBalancingWeight: 1
+ loadBalancingWeight: 1
+ locality:
+ region: udp-route-deny-all-dest/backend/0
+- clusterName: udp-route-allow-all-dest
+ endpoints:
+ - lbEndpoints:
+ - endpoint:
+ address:
+ socketAddress:
+ address: 10.2.3.8
+ portValue: 50000
+ loadBalancingWeight: 1
+ loadBalancingWeight: 1
+ locality:
+ region: udp-route-allow-all-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/udp-route-authorization.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/udp-route-authorization.listeners.yaml
new file mode 100644
index 00000000000..81a635db35b
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/udp-route-authorization.listeners.yaml
@@ -0,0 +1,227 @@
+- address:
+ socketAddress:
+ address: '::'
+ portValue: 10080
+ protocol: UDP
+ listenerFilters:
+ - name: envoy.filters.udp_listener.udp_proxy
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.filters.udp.udp_proxy.v3.UdpProxyConfig
+ matcher:
+ matcherList:
+ matchers:
+ - onMatch:
+ action:
+ name: route
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.filters.udp.udp_proxy.v3.Route
+ cluster: udp-route-allowlist-dest
+ predicate:
+ singlePredicate:
+ customMatch:
+ name: ip_matcher
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.matching.input_matchers.ip.v3.Ip
+ cidrRanges:
+ - addressPrefix: 192.168.100.0
+ prefixLen: 24
+ statPrefix: client_ip
+ input:
+ name: client_ip
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.matching.common_inputs.network.v3.SourceIPInput
+ - onMatch:
+ action:
+ name: route
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.filters.udp.udp_proxy.v3.Route
+ cluster: udp-route-allowlist-dest
+ predicate:
+ singlePredicate:
+ customMatch:
+ name: ip_matcher
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.matching.input_matchers.ip.v3.Ip
+ cidrRanges:
+ - addressPrefix: 10.1.0.0
+ prefixLen: 16
+ - addressPrefix: '2001:db8::'
+ prefixLen: 64
+ statPrefix: client_ip
+ input:
+ name: client_ip
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.matching.common_inputs.network.v3.SourceIPInput
+ statPrefix: service
+ name: udp-listener-allowlist
+- address:
+ socketAddress:
+ address: '::'
+ portValue: 10081
+ protocol: UDP
+ listenerFilters:
+ - name: envoy.filters.udp_listener.udp_proxy
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.filters.udp.udp_proxy.v3.UdpProxyConfig
+ matcher:
+ matcherList:
+ matchers:
+ - onMatch:
+ action:
+ name: route
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.filters.udp.udp_proxy.v3.Route
+ cluster: udp-route-denylist-dest
+ predicate:
+ notMatcher:
+ orMatcher:
+ predicate:
+ - singlePredicate:
+ customMatch:
+ name: ip_matcher
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.matching.input_matchers.ip.v3.Ip
+ cidrRanges:
+ - addressPrefix: 10.0.0.0
+ prefixLen: 24
+ statPrefix: client_ip
+ input:
+ name: client_ip
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.matching.common_inputs.network.v3.SourceIPInput
+ - singlePredicate:
+ customMatch:
+ name: ip_matcher
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.matching.input_matchers.ip.v3.Ip
+ cidrRanges:
+ - addressPrefix: 172.16.0.0
+ prefixLen: 12
+ statPrefix: client_ip
+ input:
+ name: client_ip
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.matching.common_inputs.network.v3.SourceIPInput
+ statPrefix: service
+ name: udp-listener-denylist
+- address:
+ socketAddress:
+ address: '::'
+ portValue: 10082
+ protocol: UDP
+ listenerFilters:
+ - name: envoy.filters.udp_listener.udp_proxy
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.filters.udp.udp_proxy.v3.UdpProxyConfig
+ matcher:
+ matcherList:
+ matchers:
+ - onMatch:
+ action:
+ name: route
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.filters.udp.udp_proxy.v3.Route
+ cluster: udp-route-mixed-dest
+ predicate:
+ singlePredicate:
+ customMatch:
+ name: ip_matcher
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.matching.input_matchers.ip.v3.Ip
+ cidrRanges:
+ - addressPrefix: 10.1.0.0
+ prefixLen: 16
+ statPrefix: client_ip
+ input:
+ name: client_ip
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.matching.common_inputs.network.v3.SourceIPInput
+ - onMatch:
+ action:
+ name: route
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.filters.udp.udp_proxy.v3.Route
+ cluster: udp-route-mixed-dest
+ predicate:
+ andMatcher:
+ predicate:
+ - singlePredicate:
+ customMatch:
+ name: ip_matcher
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.matching.input_matchers.ip.v3.Ip
+ cidrRanges:
+ - addressPrefix: 10.0.0.0
+ prefixLen: 7
+ statPrefix: client_ip
+ input:
+ name: client_ip
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.matching.common_inputs.network.v3.SourceIPInput
+ - notMatcher:
+ singlePredicate:
+ customMatch:
+ name: ip_matcher
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.matching.input_matchers.ip.v3.Ip
+ cidrRanges:
+ - addressPrefix: 10.0.0.0
+ prefixLen: 8
+ statPrefix: client_ip
+ input:
+ name: client_ip
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.matching.common_inputs.network.v3.SourceIPInput
+ - onMatch:
+ action:
+ name: route
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.filters.udp.udp_proxy.v3.Route
+ cluster: udp-route-mixed-dest
+ predicate:
+ notMatcher:
+ singlePredicate:
+ customMatch:
+ name: ip_matcher
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.matching.input_matchers.ip.v3.Ip
+ cidrRanges:
+ - addressPrefix: 10.0.0.0
+ prefixLen: 8
+ statPrefix: client_ip
+ input:
+ name: client_ip
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.matching.common_inputs.network.v3.SourceIPInput
+ statPrefix: service
+ name: udp-listener-mixed
+- address:
+ socketAddress:
+ address: '::'
+ portValue: 10083
+ protocol: UDP
+ listenerFilters:
+ - name: envoy.filters.udp_listener.udp_proxy
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.filters.udp.udp_proxy.v3.UdpProxyConfig
+ matcher: {}
+ statPrefix: service
+ name: udp-listener-deny-all
+- address:
+ socketAddress:
+ address: '::'
+ portValue: 10084
+ protocol: UDP
+ listenerFilters:
+ - name: envoy.filters.udp_listener.udp_proxy
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.filters.udp.udp_proxy.v3.UdpProxyConfig
+ matcher:
+ onNoMatch:
+ action:
+ name: route
+ typedConfig:
+ '@type': type.googleapis.com/envoy.extensions.filters.udp.udp_proxy.v3.Route
+ cluster: udp-route-allow-all-dest
+ statPrefix: service
+ name: udp-listener-allow-all
diff --git a/internal/xds/translator/testdata/out/xds-ir/udp-route-authorization.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/udp-route-authorization.routes.yaml
new file mode 100644
index 00000000000..fe51488c706
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/udp-route-authorization.routes.yaml
@@ -0,0 +1 @@
+[]
diff --git a/release-notes/current/new_features/9833-securitypolicy-udproute-client-ip.md b/release-notes/current/new_features/9833-securitypolicy-udproute-client-ip.md
new file mode 100644
index 00000000000..ad8dd4f914e
--- /dev/null
+++ b/release-notes/current/new_features/9833-securitypolicy-udproute-client-ip.md
@@ -0,0 +1 @@
+Added `UDPRoute` as a valid `SecurityPolicy` target, so client IP allowlists and denylists can now be applied to UDP services such as DNS, syslog, NTP, telemetry collectors and game servers. As with `TCPRoute`, only `authorization` rules using `principal.clientCIDRs` apply; the client IP is the source address of the datagram, and a denied source is dropped without a reply rather than receiving an error.
diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md
index a1474c39e68..b0610b7f666 100644
--- a/site/content/en/latest/api/extension_types.md
+++ b/site/content/en/latest/api/extension_types.md
@@ -4867,10 +4867,10 @@ _Appears in:_
| Field | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
-| `clientCIDRs` | _[CIDR](#cidr) array_ | false | | ClientCIDRs are the IP CIDR ranges of the client.
Valid examples are "192.168.1.0/24" or "2001:db8::/64"
If multiple CIDR ranges are specified, one of the CIDR ranges must match
the client IP for the rule to match.
The client IP is inferred from the X-Forwarded-For header, a custom header,
or the proxy protocol.
You can use the `ClientIPDetection` or the `ProxyProtocol` field in
the `ClientTrafficPolicy` to configure how the client IP is detected.
For TCPRoute targets (raw TCP connections), HTTP headers such as
X-Forwarded-For are not available. The client IP is obtained from the
TCP connection's peer address. If intermediaries (load balancers, NAT)
terminate or proxy TCP, the original client IP will only be available
if the intermediary preserves the source address (for example by
enabling the PROXY protocol or avoiding SNAT). Ensure your L4 proxy is
configured to preserve the source IP to enable correct client-IP
matching for TCPRoute targets. |
+| `clientCIDRs` | _[CIDR](#cidr) array_ | false | | ClientCIDRs are the IP CIDR ranges of the client.
Valid examples are "192.168.1.0/24" or "2001:db8::/64"
If multiple CIDR ranges are specified, one of the CIDR ranges must match
the client IP for the rule to match.
The client IP is inferred from the X-Forwarded-For header, a custom header,
or the proxy protocol.
You can use the `ClientIPDetection` or the `ProxyProtocol` field in
the `ClientTrafficPolicy` to configure how the client IP is detected.
For TCPRoute and UDPRoute targets (raw L4 traffic), HTTP headers such as
X-Forwarded-For are not available. The client IP is obtained from the
TCP connection's peer address, or from the source address of the UDP
datagram. If intermediaries (load balancers, NAT) terminate or proxy the
traffic, the original client IP will only be available if the
intermediary preserves the source address (for example by enabling the
PROXY protocol or avoiding SNAT). Note that the PROXY protocol is not
available on the UDP path, so a UDPRoute target relies entirely on the
datagram source address being preserved. Ensure your L4 proxy is
configured to preserve the source IP to enable correct client-IP
matching for TCPRoute and UDPRoute targets. |
| `jwt` | _[JWTPrincipal](#jwtprincipal)_ | false | | JWT authorize the request based on the JWT claims and scopes.
Note: in order to use JWT claims for authorization, you must configure the
JWT authentication in the same `SecurityPolicy`. |
| `headers` | _[AuthorizationHeaderMatch](#authorizationheadermatch) array_ | false | | Headers authorize the request based on user identity extracted from custom headers.
If multiple headers are specified, all headers must match for the rule to match. |
-| `clientIPGeoLocations` | _[ClientIPGeoLocation](#clientipgeolocation) array_ | false | | ClientIPGeoLocations authorizes the request based on geolocation metadata derived from the client IP.
This field is supported for HTTPRoute and GRPCRoute authorization.
It is not supported for TCPRoute targets.
If multiple entries are specified, one of the ClientIPGeoLocation entries must match for the rule to match.
The client IP is inferred from the X-Forwarded-For header, a custom header, or the
direct downstream connection source address (the TCP peer of the connection terminated by Envoy).
You can use the `ClientIPDetection` field in the `ClientTrafficPolicy` to configure the client IP detection. |
+| `clientIPGeoLocations` | _[ClientIPGeoLocation](#clientipgeolocation) array_ | false | | ClientIPGeoLocations authorizes the request based on geolocation metadata derived from the client IP.
This field is supported for HTTPRoute and GRPCRoute authorization.
It is not supported for TCPRoute or UDPRoute targets.
If multiple entries are specified, one of the ClientIPGeoLocation entries must match for the rule to match.
The client IP is inferred from the X-Forwarded-For header, a custom header, or the
direct downstream connection source address (the TCP peer of the connection terminated by Envoy).
You can use the `ClientIPDetection` field in the `ClientTrafficPolicy` to configure the client IP detection. |
#### ProcessingModeOptions
@@ -6122,12 +6122,13 @@ Gateway.
SecurityPolicySpec defines the desired state of SecurityPolicy.
-NOTE: SecurityPolicy can target Gateway, ListenerSet, HTTPRoute, GRPCRoute, and TCPRoute.
-When a SecurityPolicy targets a TCPRoute, only client-IP CIDR based authorization
-(Authorization rules that use Principal.ClientCIDRs) is applied. Other
+NOTE: SecurityPolicy can target Gateway, ListenerSet, HTTPRoute, GRPCRoute, TCPRoute
+and UDPRoute.
+When a SecurityPolicy targets a TCPRoute or a UDPRoute, only client-IP CIDR based
+authorization (Authorization rules that use Principal.ClientCIDRs) is applied. Other
authentication/authorization features such as JWT, API Key, Basic Auth,
OIDC, External Authorization, or GeoIP based authorization are not applicable
-to TCPRoute targets.
+to TCPRoute or UDPRoute targets.
_Appears in:_
- [SecurityPolicy](#securitypolicy)
diff --git a/site/content/en/latest/concepts/gateway_api_extensions/security-policy.md b/site/content/en/latest/concepts/gateway_api_extensions/security-policy.md
index 9b373855067..83b134c24f3 100644
--- a/site/content/en/latest/concepts/gateway_api_extensions/security-policy.md
+++ b/site/content/en/latest/concepts/gateway_api_extensions/security-policy.md
@@ -36,13 +36,13 @@ SecurityPolicy can be attached to Gateway API resources using two targeting mech
1. **Direct Reference (`targetRefs`)**: Explicitly reference specific resources by name and kind.
2. **Label Selection (`targetSelectors`)**: Match resources based on their labels (see [targetSelectors API reference](../../api/extension_types#targetselectors))
-The policy applies to all resources that match either targeting method. You can target various Gateway API resource types including `Gateway`, `ListenerSet`, `HTTPRoute`, `GRPCRoute`, and `TCPRoute`.
+The policy applies to all resources that match either targeting method. You can target various Gateway API resource types including `Gateway`, `ListenerSet`, `HTTPRoute`, `GRPCRoute`, `TCPRoute`, and `UDPRoute`.
When a SecurityPolicy targets a `ListenerSet`, it applies only to listeners in that ListenerSet. It does not apply to listeners owned directly by the parent Gateway. A `ListenerSet` target can also use `sectionName` to apply the policy to a single listener in the ListenerSet.
Route-level policies apply to the targeted route regardless of whether that route is attached directly to a `Gateway` or through a `ListenerSet`.
-Note: TCPRoute support is limited to authorization using client IP allow/deny lists (IP-based authorization). Other SecurityPolicy features such as JWT, API Key, Basic Auth, or OIDC are not applicable to TCPRoute targets.
+Note: TCPRoute and UDPRoute support is limited to authorization using client IP allow/deny lists (IP-based authorization). Other SecurityPolicy features such as JWT, API Key, Basic Auth, or OIDC are not applicable to L4 targets, since there is no HTTP request to inspect. For a UDPRoute the client IP is the source address of the datagram, and a denied source is dropped without a reply.
**Important**: A SecurityPolicy can only target resources in the same namespace as the policy itself.
@@ -52,8 +52,8 @@ When multiple SecurityPolicies apply to the same resource, Envoy Gateway resolve
Route-specific policies take precedence first:
-1. **Route rule-level policies** (HTTPRoute, GRPCRoute, or TCPRoute with `sectionName` targeting specific rules)
-2. **Route-level policies** (HTTPRoute, GRPCRoute, or TCPRoute without `sectionName`)
+1. **Route rule-level policies** (HTTPRoute, GRPCRoute, TCPRoute, or UDPRoute with `sectionName` targeting specific rules)
+2. **Route-level policies** (HTTPRoute, GRPCRoute, TCPRoute, or UDPRoute without `sectionName`)
After route-specific policies, parent policy precedence depends on how the route is attached.
diff --git a/site/content/en/latest/tasks/security/restrict-ip-access.md b/site/content/en/latest/tasks/security/restrict-ip-access.md
index 8d094fd5972..999c4c07851 100644
--- a/site/content/en/latest/tasks/security/restrict-ip-access.md
+++ b/site/content/en/latest/tasks/security/restrict-ip-access.md
@@ -6,7 +6,7 @@ This task provides instructions for configuring IP allowlist/denylist on Envoy G
checks if an incoming request is from an allowed IP address before routing the request to a backend service.
Envoy Gateway introduces a new CRD called [SecurityPolicy][SecurityPolicy] that allows the user to configure IP allowlist/denylist.
-This instantiated resource can be linked to a [Gateway][Gateway], [HTTPRoute][HTTPRoute], [GRPCRoute][GRPCRoute] or [TCPRoute][TCPRoute] resource.
+This instantiated resource can be linked to a [Gateway][Gateway], [HTTPRoute][HTTPRoute], [GRPCRoute][GRPCRoute], [TCPRoute][TCPRoute] or [UDPRoute][UDPRoute] resource.
## Prerequisites
@@ -22,6 +22,18 @@ In this example, the default action is set to `Deny`, which means that only requ
action are allowed, and all other requests are denied. You can also change the default action to `Allow` to allow all requests
except those from the specified IP addresses with `Deny` action.
+{{% alert title="Note" color="primary" %}}
+
+This example targets an `HTTPRoute`, but IP allowlists and denylists work the same way at L4: point `targetRefs` at a
+`TCPRoute` or a `UDPRoute` instead, or at a `Gateway` listener serving either protocol.
+
+On an L4 route only `clientCIDRs` authorization applies, because there is no HTTP request to inspect. JWT, header and
+geolocation principals, CEL expressions and `operation` matches are HTTP-only and a policy using them on a `TCPRoute`
+or `UDPRoute` is rejected. A denied connection is closed and a denied datagram is dropped without a reply, rather than
+receiving an error response.
+
+{{% /alert %}}
+
{{< tabpane text=true >}}
{{% tab header="Apply from stdin" %}}
@@ -84,6 +96,12 @@ kubectl get securitypolicy/authorization-client-ip -o yaml
It's important to note that the IP address used for allowlist/denylist is the original source IP address of the request.
You can use a [ClientTrafficPolicy] to configure how Envoy Gateway should determine the original source IP address.
+This section applies to `HTTPRoute` and `GRPCRoute`. A `TCPRoute` or `UDPRoute` carries no HTTP headers, so the client
+IP is always taken from the connection peer or the source address of the datagram, and the `X-Forwarded-For` settings
+below have no effect on it. If a load balancer or NAT sits in front of the gateway, the original client IP survives
+only when that intermediary preserves the source address — for example by enabling the PROXY protocol, which is not
+available on the UDP path.
+
For example, the below ClientTrafficPolicy configures Envoy Gateway to use the `X-Forwarded-For` header to determine the original source IP address.
The `numTrustedHops` field specifies the number of trusted hops in the `X-Forwarded-For` header. In this example, the `numTrustedHops` is set to `1`,
which means that the first rightmost IP address in the `X-Forwarded-For` header is used as the original source IP address.
@@ -195,3 +213,4 @@ Checkout the [Developer Guide](/community/develop) to get involved in the projec
[HTTPRoute]: https://gateway-api.sigs.k8s.io/reference/api-types/httproute/
[GRPCRoute]: https://gateway-api.sigs.k8s.io/reference/api-types/grpcroute/
[TCPRoute]: https://gateway-api.sigs.k8s.io/guides/tcp/
+[UDPRoute]: https://gateway-api.sigs.k8s.io/reference/api-types/udproute/
diff --git a/test/cel-validation/securitypolicy_test.go b/test/cel-validation/securitypolicy_test.go
index 63f08897e08..037e9e1d0a7 100644
--- a/test/cel-validation/securitypolicy_test.go
+++ b/test/cel-validation/securitypolicy_test.go
@@ -93,6 +93,42 @@ func TestSecurityPolicyTarget(t *testing.T) {
},
wantErrors: []string{},
},
+ {
+ desc: "valid UDPRoute targetRef",
+ mutate: func(sp *egv1a1.SecurityPolicy) {
+ sp.Spec = egv1a1.SecurityPolicySpec{
+ PolicyTargetReferences: egv1a1.PolicyTargetReferences{
+ TargetRef: &gwapiv1.LocalPolicyTargetReferenceWithSectionName{
+ LocalPolicyTargetReference: gwapiv1.LocalPolicyTargetReference{
+ Group: gwapiv1.Group("gateway.networking.k8s.io"),
+ Kind: gwapiv1.Kind("UDPRoute"),
+ Name: gwapiv1.ObjectName("udp-backend"),
+ },
+ },
+ },
+ }
+ },
+ wantErrors: []string{},
+ },
+ {
+ desc: "valid UDPRoute targetRefs",
+ mutate: func(sp *egv1a1.SecurityPolicy) {
+ sp.Spec = egv1a1.SecurityPolicySpec{
+ PolicyTargetReferences: egv1a1.PolicyTargetReferences{
+ TargetRefs: []gwapiv1.LocalPolicyTargetReferenceWithSectionName{
+ {
+ LocalPolicyTargetReference: gwapiv1.LocalPolicyTargetReference{
+ Group: gwapiv1.Group("gateway.networking.k8s.io"),
+ Kind: gwapiv1.Kind("UDPRoute"),
+ Name: gwapiv1.ObjectName("udp-backend"),
+ },
+ },
+ },
+ },
+ }
+ },
+ wantErrors: []string{},
+ },
{
desc: "valid mergeType with xRoute targetRef",
mutate: func(sp *egv1a1.SecurityPolicy) {
@@ -128,6 +164,24 @@ func TestSecurityPolicyTarget(t *testing.T) {
},
wantErrors: []string{},
},
+ {
+ desc: "valid mergeType with UDPRoute targetRef",
+ mutate: func(sp *egv1a1.SecurityPolicy) {
+ sp.Spec = egv1a1.SecurityPolicySpec{
+ PolicyTargetReferences: egv1a1.PolicyTargetReferences{
+ TargetRef: &gwapiv1.LocalPolicyTargetReferenceWithSectionName{
+ LocalPolicyTargetReference: gwapiv1.LocalPolicyTargetReference{
+ Group: gwapiv1.Group("gateway.networking.k8s.io"),
+ Kind: gwapiv1.Kind("UDPRoute"),
+ Name: gwapiv1.ObjectName("udp-backend"),
+ },
+ },
+ },
+ MergeType: new(egv1a1.StrategicMerge),
+ }
+ },
+ wantErrors: []string{},
+ },
{
desc: "mergeType rejected on ListenerSet targetRef",
mutate: func(sp *egv1a1.SecurityPolicy) {
@@ -210,7 +264,7 @@ func TestSecurityPolicyTarget(t *testing.T) {
},
wantErrors: []string{
"spec: Invalid value:",
- ": this policy can only have a targetRef.kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute",
+ ": this policy can only have a targetRef.kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute/UDPRoute",
},
},
{
@@ -251,7 +305,7 @@ func TestSecurityPolicyTarget(t *testing.T) {
wantErrors: []string{
"spec: Invalid value:",
": this policy can only have a targetRef.group of gateway.networking.k8s.io",
- ": this policy can only have a targetRef.kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute",
+ ": this policy can only have a targetRef.kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute/UDPRoute",
},
},
{
@@ -274,7 +328,7 @@ func TestSecurityPolicyTarget(t *testing.T) {
wantErrors: []string{
"spec: Invalid value:",
": this policy can only have a targetRefs[*].group of gateway.networking.k8s.io",
- ": this policy can only have a targetRefs[*].kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute",
+ ": this policy can only have a targetRefs[*].kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute/UDPRoute",
},
},
diff --git a/test/e2e/testdata/udproute-authorization-client-ip.yaml b/test/e2e/testdata/udproute-authorization-client-ip.yaml
new file mode 100644
index 00000000000..c5e6d3ebbc8
--- /dev/null
+++ b/test/e2e/testdata/udproute-authorization-client-ip.yaml
@@ -0,0 +1,196 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: udp-authz-coredns
+ namespace: gateway-conformance-infra
+ labels:
+ app: udp-authz
+spec:
+ ports:
+ - name: udp-dns
+ port: 53
+ protocol: UDP
+ targetPort: 53
+ selector:
+ app: udp-authz
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: udp-authz-coredns
+ namespace: gateway-conformance-infra
+data:
+ Corefile: |
+ foo.bar.com:53 {
+ hosts {
+ 10.0.0.1 foo.bar.com
+ ttl 1
+ }
+ log
+ errors
+ }
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: udp-authz-coredns
+ namespace: gateway-conformance-infra
+ labels:
+ app: udp-authz
+spec:
+ selector:
+ matchLabels:
+ app: udp-authz
+ template:
+ metadata:
+ labels:
+ app: udp-authz
+ spec:
+ containers:
+ - args:
+ - -conf
+ - /root/Corefile
+ image: coredns/coredns
+ name: coredns
+ volumeMounts:
+ - mountPath: /root
+ name: conf
+ volumes:
+ - configMap:
+ defaultMode: 420
+ name: udp-authz-coredns
+ name: conf
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+ name: udp-authorization-gateway
+ namespace: gateway-conformance-infra
+spec:
+ gatewayClassName: "{GATEWAY_CLASS_NAME}"
+ # All three listeners share one Gateway on purpose. A config Envoy rejects
+ # would take down the whole proxy, so the allowed listener answering is what
+ # proves the deny listeners are dropping rather than simply missing.
+ listeners:
+ - name: allowed
+ protocol: UDP
+ port: 5310
+ allowedRoutes:
+ kinds:
+ - kind: UDPRoute
+ - name: denied
+ protocol: UDP
+ port: 5311
+ allowedRoutes:
+ kinds:
+ - kind: UDPRoute
+ - name: deny-all
+ protocol: UDP
+ port: 5312
+ allowedRoutes:
+ kinds:
+ - kind: UDPRoute
+---
+# Every route points at the backend above, so an unanswered query can only come
+# from authorization and never from an unresolvable upstream.
+apiVersion: gateway.networking.k8s.io/v1
+kind: UDPRoute
+metadata:
+ name: udp-authz-allowed
+ namespace: gateway-conformance-infra
+spec:
+ parentRefs:
+ - name: udp-authorization-gateway
+ sectionName: allowed
+ rules:
+ - backendRefs:
+ - name: udp-authz-coredns
+ port: 53
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: UDPRoute
+metadata:
+ name: udp-authz-denied
+ namespace: gateway-conformance-infra
+spec:
+ parentRefs:
+ - name: udp-authorization-gateway
+ sectionName: denied
+ rules:
+ - backendRefs:
+ - name: udp-authz-coredns
+ port: 53
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: UDPRoute
+metadata:
+ name: udp-authz-deny-all
+ namespace: gateway-conformance-infra
+spec:
+ parentRefs:
+ - name: udp-authorization-gateway
+ sectionName: deny-all
+ rules:
+ - backendRefs:
+ - name: udp-authz-coredns
+ port: 53
+---
+# Allows every source, whatever address family the client turns out to have
+# after the cluster's NAT. This is the positive control.
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: SecurityPolicy
+metadata:
+ name: udp-authz-allowed-policy
+ namespace: gateway-conformance-infra
+spec:
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: UDPRoute
+ name: udp-authz-allowed
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ name: allow-any-source
+ principal:
+ clientCIDRs:
+ - 0.0.0.0/0
+ - ::/0
+---
+# Allows only a private range the test client can never be in, so the datagram
+# falls through to the Deny default and is dropped.
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: SecurityPolicy
+metadata:
+ name: udp-authz-denied-policy
+ namespace: gateway-conformance-infra
+spec:
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: UDPRoute
+ name: udp-authz-denied
+ authorization:
+ defaultAction: Deny
+ rules:
+ - action: Allow
+ name: allow-unreachable-range
+ principal:
+ clientCIDRs:
+ - 192.168.254.0/24
+---
+# A Deny default with no rules, which compiles to an empty udp_proxy matcher.
+# Targets the Gateway listener rather than the route, so the Gateway-attached
+# path is covered too.
+apiVersion: gateway.envoyproxy.io/v1alpha1
+kind: SecurityPolicy
+metadata:
+ name: udp-authz-deny-all-policy
+ namespace: gateway-conformance-infra
+spec:
+ targetRefs:
+ - group: gateway.networking.k8s.io
+ kind: Gateway
+ name: udp-authorization-gateway
+ sectionName: deny-all
+ authorization:
+ defaultAction: Deny
diff --git a/test/e2e/tests/udproute_authorization_client_ip.go b/test/e2e/tests/udproute_authorization_client_ip.go
new file mode 100644
index 00000000000..a9b2e21871b
--- /dev/null
+++ b/test/e2e/tests/udproute_authorization_client_ip.go
@@ -0,0 +1,157 @@
+// Copyright Envoy Gateway Authors
+// SPDX-License-Identifier: Apache-2.0
+// The full text of the Apache license is available in the LICENSE file at
+// the root of the repo.
+
+//go:build e2e
+
+package tests
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/miekg/dns"
+ "github.com/stretchr/testify/require"
+ corev1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/types"
+ "k8s.io/apimachinery/pkg/util/wait"
+ gwapiv1 "sigs.k8s.io/gateway-api/apis/v1"
+ "sigs.k8s.io/gateway-api/conformance/utils/suite"
+ "sigs.k8s.io/gateway-api/conformance/utils/tlog"
+
+ "github.com/envoyproxy/gateway/internal/gatewayapi"
+ "github.com/envoyproxy/gateway/internal/gatewayapi/resource"
+)
+
+func init() {
+ ConformanceTests = append(ConformanceTests, UDPRouteAuthzWithClientIP)
+}
+
+// udpAuthzDomain is answered by the coredns backend this test ships.
+const udpAuthzDomain = "foo.bar.com."
+
+var UDPRouteAuthzWithClientIP = suite.ConformanceTest{
+ ShortName: "UDPRouteAuthzWithClientIP",
+ Description: "Authorization with client IP Allow/Deny list for UDP routes",
+ Manifests: []string{"testdata/udproute-authorization-client-ip.yaml"},
+ Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
+ ns := "gateway-conformance-infra"
+ gwNN := types.NamespacedName{Name: "udp-authorization-gateway", Namespace: ns}
+ allowedNN := types.NamespacedName{Name: "udp-authz-allowed", Namespace: ns}
+ deniedNN := types.NamespacedName{Name: "udp-authz-denied", Namespace: ns}
+ denyAllNN := types.NamespacedName{Name: "udp-authz-deny-all", Namespace: ns}
+
+ GatewayAndUDPRoutesMustBeAccepted(t, suite.Client, &suite.TimeoutConfig, suite.ControllerName,
+ NewGatewayRef(gwNN), allowedNN, deniedNN, denyAllNN)
+
+ // coredns must be answering before any probe, otherwise the positive
+ // control below would fail for a reason unrelated to authorization.
+ WaitForPods(t, suite.Client, ns, map[string]string{"app": "udp-authz"}, corev1.PodRunning, &PodReady)
+
+ for _, policy := range []struct {
+ name string
+ listener gwapiv1.SectionName
+ }{
+ {"udp-authz-allowed-policy", "allowed"},
+ {"udp-authz-denied-policy", "denied"},
+ {"udp-authz-deny-all-policy", "deny-all"},
+ } {
+ // Route-attached policies report against the Gateway listener too,
+ // so every ancestorRef carries a section name.
+ SecurityPolicyMustBeAccepted(t, suite.Client,
+ types.NamespacedName{Name: policy.name, Namespace: ns},
+ suite.ControllerName, udpAuthzAncestorRef(gwNN, policy.listener))
+ }
+
+ // Each listener has to be addressed by its own port. The address returned
+ // by GatewayAndUDPRoutesMustBeAccepted is always the first listener's,
+ // because it calls the upstream helper without any listener name.
+ allowedAddr := udpAuthzListenerAddr(t, suite, gwNN, "allowed")
+ deniedAddr := udpAuthzListenerAddr(t, suite, gwNN, "denied")
+ denyAllAddr := udpAuthzListenerAddr(t, suite, gwNN, "deny-all")
+
+ // This subtest runs first by design: it is the only evidence that the
+ // proxy is up and the generated config was accepted. Without it a
+ // dropped datagram is indistinguishable from a listener that never
+ // loaded, which would make both deny subtests vacuous.
+ t.Run("allowed client IP gets a response", func(t *testing.T) {
+ udpQueryMustSucceed(t, allowedAddr)
+ })
+
+ t.Run("client IP outside the allowed CIDR is dropped", func(t *testing.T) {
+ udpQueryMustBeDropped(t, deniedAddr)
+ })
+
+ t.Run("deny by default with no rules drops everything", func(t *testing.T) {
+ udpQueryMustBeDropped(t, denyAllAddr)
+ })
+ },
+}
+
+func udpAuthzAncestorRef(gwNN types.NamespacedName, listener gwapiv1.SectionName) gwapiv1.ParentReference {
+ return gwapiv1.ParentReference{
+ Group: gatewayapi.GroupPtr(gwapiv1.GroupName),
+ Kind: gatewayapi.KindPtr(resource.KindGateway),
+ Namespace: gatewayapi.NamespacePtr(gwNN.Namespace),
+ Name: gwapiv1.ObjectName(gwNN.Name),
+ SectionName: &listener,
+ }
+}
+
+// udpAuthzListenerAddr resolves the address of one named listener on a Gateway
+// that has several.
+func udpAuthzListenerAddr(t *testing.T, suite *suite.ConformanceTestSuite, gwNN types.NamespacedName, listener string) string {
+ t.Helper()
+
+ addr, err := WaitForGatewayAddress(t, suite.Client, &suite.TimeoutConfig, gwNN, listener)
+ require.NoErrorf(t, err, "timed out waiting for an address for listener %s", listener)
+ return addr
+}
+
+func udpQueryMustSucceed(t *testing.T, addr string) {
+ t.Helper()
+
+ msg := new(dns.Msg)
+ msg.SetQuestion(udpAuthzDomain, dns.TypeA)
+
+ if err := wait.PollUntilContextTimeout(context.TODO(), time.Second, time.Minute, true,
+ func(_ context.Context) (done bool, err error) {
+ tlog.Logf(t, "performing DNS query %s on %s", udpAuthzDomain, addr)
+ r, err := dns.Exchange(msg, addr)
+ if err != nil {
+ tlog.Logf(t, "failed to perform a UDP query: %v", err)
+ return false, nil
+ }
+ tlog.Logf(t, "got DNS response: %s", r.String())
+ return true, nil
+ }); err != nil {
+ // Fatal rather than an error: the deny assertions mean nothing if the
+ // data plane never came up.
+ t.Fatalf("failed to perform DNS query: %v", err)
+ }
+}
+
+// udpQueryMustBeDropped requires several consecutive queries to go unanswered.
+//
+// A denied datagram draws no reply of any kind, so this deliberately is not a
+// poll: a poll that retried until it succeeded would report success after a
+// full minute of correct denials.
+func udpQueryMustBeDropped(t *testing.T, addr string) {
+ t.Helper()
+
+ msg := new(dns.Msg)
+ msg.SetQuestion(udpAuthzDomain, dns.TypeA)
+ client := &dns.Client{Timeout: 2 * time.Second}
+
+ const attempts = 3
+ for i := 1; i <= attempts; i++ {
+ tlog.Logf(t, "performing DNS query %s on %s, expecting no reply (%d/%d)", udpAuthzDomain, addr, i, attempts)
+ r, _, err := client.Exchange(msg, addr)
+ if err == nil {
+ t.Fatalf("expected the datagram to be dropped, but got a DNS response: %s", r.String())
+ }
+ tlog.Logf(t, "datagram dropped as expected: %v", err)
+ }
+}
diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml
index 02391a49ac6..b7a0654acd7 100644
--- a/test/helm/gateway-crds-helm/all.out.yaml
+++ b/test/helm/gateway-crds-helm/all.out.yaml
@@ -53967,14 +53967,17 @@ spec:
You can use the `ClientIPDetection` or the `ProxyProtocol` field in
the `ClientTrafficPolicy` to configure how the client IP is detected.
- For TCPRoute targets (raw TCP connections), HTTP headers such as
+ For TCPRoute and UDPRoute targets (raw L4 traffic), HTTP headers such as
X-Forwarded-For are not available. The client IP is obtained from the
- TCP connection's peer address. If intermediaries (load balancers, NAT)
- terminate or proxy TCP, the original client IP will only be available
- if the intermediary preserves the source address (for example by
- enabling the PROXY protocol or avoiding SNAT). Ensure your L4 proxy is
+ TCP connection's peer address, or from the source address of the UDP
+ datagram. If intermediaries (load balancers, NAT) terminate or proxy the
+ traffic, the original client IP will only be available if the
+ intermediary preserves the source address (for example by enabling the
+ PROXY protocol or avoiding SNAT). Note that the PROXY protocol is not
+ available on the UDP path, so a UDPRoute target relies entirely on the
+ datagram source address being preserved. Ensure your L4 proxy is
configured to preserve the source IP to enable correct client-IP
- matching for TCPRoute targets.
+ matching for TCPRoute and UDPRoute targets.
items:
description: |-
CIDR defines a CIDR Address range.
@@ -53987,7 +53990,7 @@ spec:
description: |-
ClientIPGeoLocations authorizes the request based on geolocation metadata derived from the client IP.
This field is supported for HTTPRoute and GRPCRoute authorization.
- It is not supported for TCPRoute targets.
+ It is not supported for TCPRoute or UDPRoute targets.
If multiple entries are specified, one of the ClientIPGeoLocation entries must match for the rule to match.
@@ -61666,21 +61669,23 @@ spec:
- message: this policy can only have a targetRef.group of gateway.networking.k8s.io
rule: 'has(self.targetRef) ? self.targetRef.group == ''gateway.networking.k8s.io''
: true'
- - message: this policy can only have a targetRef.kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute
+ - message: this policy can only have a targetRef.kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute/UDPRoute
rule: 'has(self.targetRef) ? self.targetRef.kind in [''Gateway'', ''ListenerSet'',
- ''HTTPRoute'', ''GRPCRoute'', ''TCPRoute''] : true'
+ ''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'', ''UDPRoute''] : true'
- message: this policy can only have a targetRefs[*].group of gateway.networking.k8s.io
rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.group ==
''gateway.networking.k8s.io'') : true '
- - message: this policy can only have a targetRefs[*].kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute
+ - message: this policy can only have a targetRefs[*].kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute/UDPRoute
rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.kind in [''Gateway'',
- ''ListenerSet'', ''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'']) : true '
+ ''ListenerSet'', ''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'', ''UDPRoute''])
+ : true '
- message: mergeType can only be used with xRoute targets
rule: '!has(self.mergeType) || ((!has(self.targetRef) || self.targetRef.kind
- in [''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'']) && (!has(self.targetRefs)
- || self.targetRefs.all(ref, ref.kind in [''HTTPRoute'', ''GRPCRoute'',
- ''TCPRoute''])) && (!has(self.targetSelectors) || self.targetSelectors.all(sel,
- sel.kind in [''HTTPRoute'', ''GRPCRoute'', ''TCPRoute''])))'
+ in [''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'', ''UDPRoute'']) &&
+ (!has(self.targetRefs) || self.targetRefs.all(ref, ref.kind in [''HTTPRoute'',
+ ''GRPCRoute'', ''TCPRoute'', ''UDPRoute''])) && (!has(self.targetSelectors)
+ || self.targetSelectors.all(sel, sel.kind in [''HTTPRoute'', ''GRPCRoute'',
+ ''TCPRoute'', ''UDPRoute''])))'
- message: if authorization.rules.principal.jwt is used, jwt must be defined
rule: '(has(self.authorization) && has(self.authorization.rules) &&
self.authorization.rules.exists(r, has(r.principal) ? has(r.principal.jwt)
diff --git a/test/helm/gateway-crds-helm/e2e.out.yaml b/test/helm/gateway-crds-helm/e2e.out.yaml
index e34be6f998c..677ee2c0244 100644
--- a/test/helm/gateway-crds-helm/e2e.out.yaml
+++ b/test/helm/gateway-crds-helm/e2e.out.yaml
@@ -29905,14 +29905,17 @@ spec:
You can use the `ClientIPDetection` or the `ProxyProtocol` field in
the `ClientTrafficPolicy` to configure how the client IP is detected.
- For TCPRoute targets (raw TCP connections), HTTP headers such as
+ For TCPRoute and UDPRoute targets (raw L4 traffic), HTTP headers such as
X-Forwarded-For are not available. The client IP is obtained from the
- TCP connection's peer address. If intermediaries (load balancers, NAT)
- terminate or proxy TCP, the original client IP will only be available
- if the intermediary preserves the source address (for example by
- enabling the PROXY protocol or avoiding SNAT). Ensure your L4 proxy is
+ TCP connection's peer address, or from the source address of the UDP
+ datagram. If intermediaries (load balancers, NAT) terminate or proxy the
+ traffic, the original client IP will only be available if the
+ intermediary preserves the source address (for example by enabling the
+ PROXY protocol or avoiding SNAT). Note that the PROXY protocol is not
+ available on the UDP path, so a UDPRoute target relies entirely on the
+ datagram source address being preserved. Ensure your L4 proxy is
configured to preserve the source IP to enable correct client-IP
- matching for TCPRoute targets.
+ matching for TCPRoute and UDPRoute targets.
items:
description: |-
CIDR defines a CIDR Address range.
@@ -29925,7 +29928,7 @@ spec:
description: |-
ClientIPGeoLocations authorizes the request based on geolocation metadata derived from the client IP.
This field is supported for HTTPRoute and GRPCRoute authorization.
- It is not supported for TCPRoute targets.
+ It is not supported for TCPRoute or UDPRoute targets.
If multiple entries are specified, one of the ClientIPGeoLocation entries must match for the rule to match.
@@ -37604,21 +37607,23 @@ spec:
- message: this policy can only have a targetRef.group of gateway.networking.k8s.io
rule: 'has(self.targetRef) ? self.targetRef.group == ''gateway.networking.k8s.io''
: true'
- - message: this policy can only have a targetRef.kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute
+ - message: this policy can only have a targetRef.kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute/UDPRoute
rule: 'has(self.targetRef) ? self.targetRef.kind in [''Gateway'', ''ListenerSet'',
- ''HTTPRoute'', ''GRPCRoute'', ''TCPRoute''] : true'
+ ''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'', ''UDPRoute''] : true'
- message: this policy can only have a targetRefs[*].group of gateway.networking.k8s.io
rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.group ==
''gateway.networking.k8s.io'') : true '
- - message: this policy can only have a targetRefs[*].kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute
+ - message: this policy can only have a targetRefs[*].kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute/UDPRoute
rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.kind in [''Gateway'',
- ''ListenerSet'', ''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'']) : true '
+ ''ListenerSet'', ''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'', ''UDPRoute''])
+ : true '
- message: mergeType can only be used with xRoute targets
rule: '!has(self.mergeType) || ((!has(self.targetRef) || self.targetRef.kind
- in [''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'']) && (!has(self.targetRefs)
- || self.targetRefs.all(ref, ref.kind in [''HTTPRoute'', ''GRPCRoute'',
- ''TCPRoute''])) && (!has(self.targetSelectors) || self.targetSelectors.all(sel,
- sel.kind in [''HTTPRoute'', ''GRPCRoute'', ''TCPRoute''])))'
+ in [''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'', ''UDPRoute'']) &&
+ (!has(self.targetRefs) || self.targetRefs.all(ref, ref.kind in [''HTTPRoute'',
+ ''GRPCRoute'', ''TCPRoute'', ''UDPRoute''])) && (!has(self.targetSelectors)
+ || self.targetSelectors.all(sel, sel.kind in [''HTTPRoute'', ''GRPCRoute'',
+ ''TCPRoute'', ''UDPRoute''])))'
- message: if authorization.rules.principal.jwt is used, jwt must be defined
rule: '(has(self.authorization) && has(self.authorization.rules) &&
self.authorization.rules.exists(r, has(r.principal) ? has(r.principal.jwt)
diff --git a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml
index aff9f59a480..e1a6fb0f694 100644
--- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml
+++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml
@@ -29905,14 +29905,17 @@ spec:
You can use the `ClientIPDetection` or the `ProxyProtocol` field in
the `ClientTrafficPolicy` to configure how the client IP is detected.
- For TCPRoute targets (raw TCP connections), HTTP headers such as
+ For TCPRoute and UDPRoute targets (raw L4 traffic), HTTP headers such as
X-Forwarded-For are not available. The client IP is obtained from the
- TCP connection's peer address. If intermediaries (load balancers, NAT)
- terminate or proxy TCP, the original client IP will only be available
- if the intermediary preserves the source address (for example by
- enabling the PROXY protocol or avoiding SNAT). Ensure your L4 proxy is
+ TCP connection's peer address, or from the source address of the UDP
+ datagram. If intermediaries (load balancers, NAT) terminate or proxy the
+ traffic, the original client IP will only be available if the
+ intermediary preserves the source address (for example by enabling the
+ PROXY protocol or avoiding SNAT). Note that the PROXY protocol is not
+ available on the UDP path, so a UDPRoute target relies entirely on the
+ datagram source address being preserved. Ensure your L4 proxy is
configured to preserve the source IP to enable correct client-IP
- matching for TCPRoute targets.
+ matching for TCPRoute and UDPRoute targets.
items:
description: |-
CIDR defines a CIDR Address range.
@@ -29925,7 +29928,7 @@ spec:
description: |-
ClientIPGeoLocations authorizes the request based on geolocation metadata derived from the client IP.
This field is supported for HTTPRoute and GRPCRoute authorization.
- It is not supported for TCPRoute targets.
+ It is not supported for TCPRoute or UDPRoute targets.
If multiple entries are specified, one of the ClientIPGeoLocation entries must match for the rule to match.
@@ -37604,21 +37607,23 @@ spec:
- message: this policy can only have a targetRef.group of gateway.networking.k8s.io
rule: 'has(self.targetRef) ? self.targetRef.group == ''gateway.networking.k8s.io''
: true'
- - message: this policy can only have a targetRef.kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute
+ - message: this policy can only have a targetRef.kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute/UDPRoute
rule: 'has(self.targetRef) ? self.targetRef.kind in [''Gateway'', ''ListenerSet'',
- ''HTTPRoute'', ''GRPCRoute'', ''TCPRoute''] : true'
+ ''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'', ''UDPRoute''] : true'
- message: this policy can only have a targetRefs[*].group of gateway.networking.k8s.io
rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.group ==
''gateway.networking.k8s.io'') : true '
- - message: this policy can only have a targetRefs[*].kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute
+ - message: this policy can only have a targetRefs[*].kind of Gateway/ListenerSet/HTTPRoute/GRPCRoute/TCPRoute/UDPRoute
rule: 'has(self.targetRefs) ? self.targetRefs.all(ref, ref.kind in [''Gateway'',
- ''ListenerSet'', ''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'']) : true '
+ ''ListenerSet'', ''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'', ''UDPRoute''])
+ : true '
- message: mergeType can only be used with xRoute targets
rule: '!has(self.mergeType) || ((!has(self.targetRef) || self.targetRef.kind
- in [''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'']) && (!has(self.targetRefs)
- || self.targetRefs.all(ref, ref.kind in [''HTTPRoute'', ''GRPCRoute'',
- ''TCPRoute''])) && (!has(self.targetSelectors) || self.targetSelectors.all(sel,
- sel.kind in [''HTTPRoute'', ''GRPCRoute'', ''TCPRoute''])))'
+ in [''HTTPRoute'', ''GRPCRoute'', ''TCPRoute'', ''UDPRoute'']) &&
+ (!has(self.targetRefs) || self.targetRefs.all(ref, ref.kind in [''HTTPRoute'',
+ ''GRPCRoute'', ''TCPRoute'', ''UDPRoute''])) && (!has(self.targetSelectors)
+ || self.targetSelectors.all(sel, sel.kind in [''HTTPRoute'', ''GRPCRoute'',
+ ''TCPRoute'', ''UDPRoute''])))'
- message: if authorization.rules.principal.jwt is used, jwt must be defined
rule: '(has(self.authorization) && has(self.authorization.rules) &&
self.authorization.rules.exists(r, has(r.principal) ? has(r.principal.jwt)