Skip to content

Commit 0068d99

Browse files
committed
Implement SSL redirect handling by splitting HTTPRoutes into redirect and backend routes
- Added logic to track HTTPRoutes that require SSL redirect splitting. - Updated `applySSLRedirectPolicy` to mark routes for later processing instead of applying filters immediately. - Introduced `splitHTTPRouteForSSLRedirect` to create separate HTTP redirect and HTTPS backend routes. Signed-off-by: omar <omar.hammami@solo.io>
1 parent 1a9d478 commit 0068d99

4 files changed

Lines changed: 271 additions & 28 deletions

File tree

pkg/i2gw/implementations/kgateway/emitter.go

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ func (e *Emitter) Emit(ir *intermediate.IR) ([]client.Object, error) {
8484
// Track Backends per (namespace, svcName) for service-upstream.
8585
staticBackends := map[types.NamespacedName]*kgateway.Backend{}
8686

87+
// Track HTTPRoutes that need SSL redirect splitting
88+
routesToSplitForSSLRedirect := map[types.NamespacedName]bool{}
89+
8790
for httpRouteKey, httpRouteContext := range ir.HTTPRoutes {
8891
ingx := httpRouteContext.ProviderSpecificIR.IngressNginx
8992
if ingx == nil {
@@ -196,8 +199,11 @@ func (e *Emitter) Emit(ir *intermediate.IR) ([]client.Object, error) {
196199
touched = true
197200
}
198201

199-
// Apply SSL redirect via RequestRedirect filter on HTTPRoute rules.
200-
applySSLRedirectPolicy(pol, httpRouteKey, &httpRouteContext, coverage)
202+
// Check if SSL redirect is enabled (but don't apply it yet - will split route later)
203+
if applySSLRedirectPolicy(pol, httpRouteKey, &httpRouteContext, coverage) {
204+
// Mark this HTTPRoute for SSL redirect splitting
205+
routesToSplitForSSLRedirect[httpRouteKey] = true
206+
}
201207

202208
if !touched {
203209
// No TrafficPolicy fields set for this policy; skip coverage wiring.
@@ -250,6 +256,66 @@ func (e *Emitter) Emit(ir *intermediate.IR) ([]client.Object, error) {
250256
}
251257
}
252258

259+
// Split HTTPRoutes that have SSL redirect enabled
260+
for httpRouteKey := range routesToSplitForSSLRedirect {
261+
httpRouteContext, exists := ir.HTTPRoutes[httpRouteKey]
262+
if !exists {
263+
continue
264+
}
265+
266+
// Get the Gateway for this HTTPRoute
267+
var gatewayCtx *intermediate.GatewayContext
268+
if len(httpRouteContext.Spec.ParentRefs) > 0 {
269+
parentRef := httpRouteContext.Spec.ParentRefs[0]
270+
gatewayNamespace := httpRouteKey.Namespace
271+
if parentRef.Namespace != nil {
272+
gatewayNamespace = string(*parentRef.Namespace)
273+
}
274+
gatewayName := string(parentRef.Name)
275+
if gatewayName != "" {
276+
gatewayKey := types.NamespacedName{
277+
Namespace: gatewayNamespace,
278+
Name: gatewayName,
279+
}
280+
if gw, ok := ir.Gateways[gatewayKey]; ok {
281+
gatewayCtx = &gw
282+
}
283+
}
284+
}
285+
286+
if gatewayCtx == nil {
287+
continue
288+
}
289+
290+
// Split the route
291+
httpRedirectRoute, httpsBackendRoute, success := splitHTTPRouteForSSLRedirect(
292+
httpRouteContext,
293+
httpRouteKey,
294+
gatewayCtx,
295+
)
296+
297+
if success {
298+
// Remove the original route
299+
delete(ir.HTTPRoutes, httpRouteKey)
300+
301+
// Add the HTTP redirect route
302+
httpRedirectKey := types.NamespacedName{
303+
Namespace: httpRedirectRoute.Namespace,
304+
Name: httpRedirectRoute.Name,
305+
}
306+
ir.HTTPRoutes[httpRedirectKey] = *httpRedirectRoute
307+
308+
// Add the HTTPS backend route if it was created
309+
if httpsBackendRoute != nil {
310+
httpsBackendKey := types.NamespacedName{
311+
Namespace: httpsBackendRoute.Namespace,
312+
Name: httpsBackendRoute.Name,
313+
}
314+
ir.HTTPRoutes[httpsBackendKey] = *httpsBackendRoute
315+
}
316+
}
317+
}
318+
253319
// Collect all static Backends computed across HTTPRoutes.
254320
for _, b := range staticBackends {
255321
out = append(out, b)

pkg/i2gw/implementations/kgateway/ssl_redirect.go

Lines changed: 104 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,54 +17,103 @@ limitations under the License.
1717
package kgateway
1818

1919
import (
20+
"fmt"
21+
2022
"github.com/kgateway-dev/ingress2gateway/pkg/i2gw/intermediate"
2123
"k8s.io/apimachinery/pkg/types"
2224
"k8s.io/utils/ptr"
2325
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
2426
)
2527

26-
// applySSLRedirectPolicy applies SSL redirect by adding a RequestRedirect filter
27-
// to HTTPRoute rules when SSLRedirect is enabled in the policy.
28+
// applySSLRedirectPolicy marks rules that need SSL redirect handling.
29+
// The actual route splitting happens later in the emitter.
2830
//
2931
// Semantics:
30-
// - If SSLRedirect is enabled, add a RequestRedirect filter to rule-level Filters
31-
// - The filter redirects HTTP to HTTPS with a 301 status code
32-
// - The filter is applied to all rules covered by the policy
32+
// - If SSLRedirect is enabled, mark the HTTPRoute for later splitting
33+
// - Returns true if SSL redirect is enabled for this policy
3334
func applySSLRedirectPolicy(
3435
pol intermediate.Policy,
3536
httpRouteKey types.NamespacedName,
3637
httpRouteContext *intermediate.HTTPRouteContext,
3738
coverage []intermediate.PolicyIndex,
38-
) {
39+
) bool {
3940
if pol.SSLRedirect == nil || !*pol.SSLRedirect {
40-
return
41+
return false
42+
}
43+
// SSL redirect will be handled by splitting the route later
44+
// Don't modify the route here - preserve backendRefs for the HTTPS route
45+
return true
46+
}
47+
48+
// splitHTTPRouteForSSLRedirect splits an HTTPRoute into two routes when SSL redirect is enabled:
49+
// 1. HTTP redirect route: bound to HTTP listener, has RequestRedirect filter, no backendRefs
50+
// 2. HTTPS backend route: bound to HTTPS listener, has backendRefs, no redirect filter
51+
//
52+
// Returns the HTTP redirect route, HTTPS backend route, and whether splitting was successful.
53+
func splitHTTPRouteForSSLRedirect(
54+
httpRouteContext intermediate.HTTPRouteContext,
55+
httpRouteKey types.NamespacedName,
56+
gatewayCtx *intermediate.GatewayContext,
57+
) (*intermediate.HTTPRouteContext, *intermediate.HTTPRouteContext, bool) {
58+
// Find HTTP and HTTPS listeners by hostname
59+
var httpListenerName, httpsListenerName *gwv1.SectionName
60+
hostname := ""
61+
if len(httpRouteContext.Spec.Hostnames) > 0 {
62+
hostname = string(httpRouteContext.Spec.Hostnames[0])
4163
}
4264

43-
// Get unique rule indices from coverage
44-
ruleSet := make(map[int]struct{})
45-
for _, idx := range coverage {
46-
ruleSet[idx.Rule] = struct{}{}
65+
for _, listener := range gatewayCtx.Spec.Listeners {
66+
if listener.Protocol == gwv1.HTTPProtocolType {
67+
// Check if hostname matches
68+
if hostname == "" || (listener.Hostname != nil && string(*listener.Hostname) == hostname) {
69+
name := listener.Name
70+
httpListenerName = &name
71+
}
72+
} else if listener.Protocol == gwv1.HTTPSProtocolType {
73+
// Check if hostname matches
74+
if hostname == "" || (listener.Hostname != nil && string(*listener.Hostname) == hostname) {
75+
name := listener.Name
76+
httpsListenerName = &name
77+
}
78+
}
4779
}
4880

49-
// Add RequestRedirect filter to each covered rule
50-
for ruleIdx := range ruleSet {
51-
if ruleIdx >= len(httpRouteContext.Spec.Rules) {
52-
continue
81+
// If HTTPS listener doesn't exist, we can't create the HTTPS route
82+
// Still create HTTP redirect route though
83+
if httpsListenerName == nil {
84+
// Only create HTTP redirect route if HTTP listener exists
85+
if httpListenerName == nil {
86+
return nil, nil, false
5387
}
88+
}
89+
90+
// Create HTTP redirect route
91+
httpRedirectRoute := intermediate.HTTPRouteContext{
92+
HTTPRoute: *httpRouteContext.HTTPRoute.DeepCopy(),
93+
ProviderSpecificIR: httpRouteContext.ProviderSpecificIR,
94+
RuleBackendSources: httpRouteContext.RuleBackendSources,
95+
}
96+
httpRedirectRoute.ObjectMeta.Name = fmt.Sprintf("%s-http-redirect", httpRouteKey.Name)
97+
httpRedirectRoute.ObjectMeta.Namespace = httpRouteKey.Namespace
5498

55-
// Check if RequestRedirect filter already exists
99+
// Update parentRefs to bind to HTTP listener
100+
if len(httpRedirectRoute.Spec.ParentRefs) > 0 && httpListenerName != nil {
101+
httpRedirectRoute.Spec.ParentRefs[0].SectionName = httpListenerName
102+
}
103+
104+
// Add RequestRedirect filter and remove backendRefs from all rules
105+
for i := range httpRedirectRoute.Spec.Rules {
106+
// Add RequestRedirect filter
56107
hasRedirect := false
57-
for _, filter := range httpRouteContext.Spec.Rules[ruleIdx].Filters {
108+
for _, filter := range httpRedirectRoute.Spec.Rules[i].Filters {
58109
if filter.Type == gwv1.HTTPRouteFilterRequestRedirect {
59110
hasRedirect = true
60111
break
61112
}
62113
}
63-
64114
if !hasRedirect {
65-
// Add RequestRedirect filter to redirect HTTP to HTTPS
66-
httpRouteContext.Spec.Rules[ruleIdx].Filters = append(
67-
httpRouteContext.Spec.Rules[ruleIdx].Filters,
115+
httpRedirectRoute.Spec.Rules[i].Filters = append(
116+
httpRedirectRoute.Spec.Rules[i].Filters,
68117
gwv1.HTTPRouteFilter{
69118
Type: gwv1.HTTPRouteFilterRequestRedirect,
70119
RequestRedirect: &gwv1.HTTPRequestRedirectFilter{
@@ -73,9 +122,40 @@ func applySSLRedirectPolicy(
73122
},
74123
},
75124
)
76-
// RequestRedirect filters cannot be used with backendRefs per Gateway API spec
77-
// Remove backendRefs when redirecting
78-
httpRouteContext.Spec.Rules[ruleIdx].BackendRefs = nil
79125
}
126+
// Remove backendRefs (RequestRedirect filters cannot coexist with backendRefs)
127+
httpRedirectRoute.Spec.Rules[i].BackendRefs = nil
128+
}
129+
130+
// Create HTTPS backend route (only if HTTPS listener exists)
131+
var httpsBackendRoute *intermediate.HTTPRouteContext
132+
if httpsListenerName != nil {
133+
route := intermediate.HTTPRouteContext{
134+
HTTPRoute: *httpRouteContext.HTTPRoute.DeepCopy(),
135+
ProviderSpecificIR: httpRouteContext.ProviderSpecificIR,
136+
RuleBackendSources: httpRouteContext.RuleBackendSources,
137+
}
138+
route.ObjectMeta.Name = fmt.Sprintf("%s-https", httpRouteKey.Name)
139+
route.ObjectMeta.Namespace = httpRouteKey.Namespace
140+
httpsBackendRoute = &route
141+
142+
// Update parentRefs to bind to HTTPS listener
143+
if len(httpsBackendRoute.Spec.ParentRefs) > 0 {
144+
httpsBackendRoute.Spec.ParentRefs[0].SectionName = httpsListenerName
145+
}
146+
147+
// Remove any RequestRedirect filters from HTTPS route
148+
for i := range httpsBackendRoute.Spec.Rules {
149+
var filtersWithoutRedirect []gwv1.HTTPRouteFilter
150+
for _, filter := range httpsBackendRoute.Spec.Rules[i].Filters {
151+
if filter.Type != gwv1.HTTPRouteFilterRequestRedirect {
152+
filtersWithoutRedirect = append(filtersWithoutRedirect, filter)
153+
}
154+
}
155+
httpsBackendRoute.Spec.Rules[i].Filters = filtersWithoutRedirect
156+
}
157+
// Keep backendRefs for HTTPS route
80158
}
159+
160+
return &httpRedirectRoute, httpsBackendRoute, true
81161
}

pkg/i2gw/implementations/kgateway/testing/testdata/input/ssl_redirect.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ metadata:
88
namespace: default
99
spec:
1010
ingressClassName: nginx
11+
tls:
12+
- hosts:
13+
- redirect.example
14+
secretName: redirect-tls
1115
rules:
1216
- host: redirect.example
1317
http:
@@ -30,6 +34,10 @@ metadata:
3034
namespace: default
3135
spec:
3236
ingressClassName: nginx
37+
tls:
38+
- hosts:
39+
- redirect.example
40+
secretName: redirect-tls
3341
rules:
3442
- host: redirect.example
3543
http:
@@ -59,6 +67,10 @@ metadata:
5967
namespace: default
6068
spec:
6169
ingressClassName: nginx
70+
tls:
71+
- hosts:
72+
- force-redirect.example
73+
secretName: force-redirect-tls
6274
rules:
6375
- host: force-redirect.example
6476
http:

0 commit comments

Comments
 (0)