Skip to content

Commit b08bcab

Browse files
committed
Prevent duplicate hosts in Ingress rules when using ACME
1 parent 1e52818 commit b08bcab

4 files changed

Lines changed: 173 additions & 136 deletions

File tree

pkg/reconciler/domainmapping/resources/ingress.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,23 @@ func MakeIngress(dm *servingv1beta1.DomainMapping, backendServiceName, hostName,
5757
},
5858
}}
5959

60-
// Add dedicated ACME challenge rules
61-
if len(acmeChallenges) > 0 {
62-
acmeRules := routeresources.MakeACMEChallengeRules(acmeChallenges)
63-
rules = append(rules, acmeRules...)
60+
// Handle ACME challenges
61+
for _, challenge := range acmeChallenges {
62+
if challenge.URL.Host == dm.Name {
63+
// Merge ACME challenge into the main rule (matching host)
64+
acmePath := routeresources.MakeACMEIngressPath(challenge)
65+
// Prepend ACME path before traffic path
66+
rules[0].HTTP.Paths = append([]netv1alpha1.HTTPIngressPath{acmePath}, rules[0].HTTP.Paths...)
67+
} else {
68+
// Create separate rule for non-matching host (e.g., truncated domain)
69+
rules = append(rules, netv1alpha1.IngressRule{
70+
Hosts: []string{challenge.URL.Host},
71+
Visibility: netv1alpha1.IngressVisibilityExternalIP,
72+
HTTP: &netv1alpha1.HTTPIngressRuleValue{
73+
Paths: []netv1alpha1.HTTPIngressPath{routeresources.MakeACMEIngressPath(challenge)},
74+
},
75+
})
76+
}
6477
}
6578

6679
return &netv1alpha1.Ingress{

pkg/reconciler/domainmapping/resources/ingress_test.go

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,16 @@ func TestMakeIngress(t *testing.T) {
198198
Visibility: netv1alpha1.IngressVisibilityExternalIP,
199199
HTTP: &netv1alpha1.HTTPIngressRuleValue{
200200
Paths: []netv1alpha1.HTTPIngressPath{{
201+
Path: "/.well-known/acme-challenge/challenge-token",
202+
Splits: []netv1alpha1.IngressBackendSplit{{
203+
IngressBackend: netv1alpha1.IngressBackend{
204+
ServiceNamespace: "test-ns",
205+
ServiceName: "cm-solver",
206+
ServicePort: intstr.FromInt(8090),
207+
},
208+
Percent: 100,
209+
}},
210+
}, {
201211
RewriteHost: "the-rewrite-host",
202212
Splits: []netv1alpha1.IngressBackendSplit{{
203213
Percent: 100,
@@ -212,22 +222,6 @@ func TestMakeIngress(t *testing.T) {
212222
}},
213223
}},
214224
},
215-
}, {
216-
Hosts: []string{"mapping.com"},
217-
Visibility: netv1alpha1.IngressVisibilityExternalIP,
218-
HTTP: &netv1alpha1.HTTPIngressRuleValue{
219-
Paths: []netv1alpha1.HTTPIngressPath{{
220-
Path: "/.well-known/acme-challenge/challenge-token",
221-
Splits: []netv1alpha1.IngressBackendSplit{{
222-
IngressBackend: netv1alpha1.IngressBackend{
223-
ServiceNamespace: "test-ns",
224-
ServiceName: "cm-solver",
225-
ServicePort: intstr.FromInt(8090),
226-
},
227-
Percent: 100,
228-
}},
229-
}},
230-
},
231225
}},
232226
},
233227
},

pkg/reconciler/route/resources/ingress.go

Lines changed: 86 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,14 @@ func makeIngressSpec(
135135
featuresConfig := config.FromContextOrDefaults(ctx).Features
136136
networkConfig := config.FromContextOrDefaults(ctx).Network
137137

138+
// Build ACME lookup map: host -> []HTTPIngressPath
139+
acmePathsByHost := make(map[string][]netv1alpha1.HTTPIngressPath)
140+
for _, challenge := range acmeChallenges {
141+
host := challenge.URL.Host
142+
acmePath := MakeACMEIngressPath(challenge)
143+
acmePathsByHost[host] = append(acmePathsByHost[host], acmePath)
144+
}
145+
138146
for _, name := range names {
139147
visibilities := []netv1alpha1.IngressVisibility{netv1alpha1.IngressVisibilityClusterLocal}
140148
// If this is a public target (or not being marked as cluster-local), we also make public rule.
@@ -146,49 +154,58 @@ func makeIngressSpec(
146154
if err != nil {
147155
return netv1alpha1.IngressSpec{}, err
148156
}
149-
rule := makeIngressRule(domains, r.Namespace,
157+
domainRules := makeIngressRules(domains, r.Namespace,
150158
visibility, tc.Targets[name], ro.RolloutsByTag(name), networkConfig.SystemInternalTLSEnabled())
151-
if featuresConfig.TagHeaderBasedRouting == apicfg.Enabled {
152-
if rule.HTTP.Paths[0].AppendHeaders == nil {
153-
rule.HTTP.Paths[0].AppendHeaders = make(map[string]string, 1)
159+
160+
// Apply tag header routing and ACME merging to each rule
161+
for i := range domainRules {
162+
rule := &domainRules[i]
163+
164+
if featuresConfig.TagHeaderBasedRouting == apicfg.Enabled {
165+
if rule.HTTP.Paths[0].AppendHeaders == nil {
166+
rule.HTTP.Paths[0].AppendHeaders = make(map[string]string, 1)
167+
}
168+
169+
if name == traffic.DefaultTarget {
170+
// To provide information if a request is routed via the "default route" or not,
171+
// the header "Knative-Serving-Default-Route: true" is appended here.
172+
// If the header has "true" and there is a "Knative-Serving-Tag" header,
173+
// then the request is having the undefined tag header,
174+
// which will be observed in queue-proxy.
175+
rule.HTTP.Paths[0].AppendHeaders[netheader.DefaultRouteKey] = "true"
176+
177+
// Add ingress paths for a request with the tag header.
178+
// If a request has one of the `names` (tag name), specified as the
179+
// Knative-Serving-Tag header, except for the DefaultTarget,
180+
// the request will be routed to that target.
181+
// corresponding to the tag name.
182+
// Since names are sorted `DefaultTarget == ""` is the first one,
183+
// so just pass the subslice.
184+
rule.HTTP.Paths = append(
185+
makeTagBasedRoutingIngressPaths(r.Namespace, tc, ro, networkConfig.SystemInternalTLSEnabled(), names[1:]), rule.HTTP.Paths...)
186+
} else {
187+
// If a request is routed by a tag-attached hostname instead of the tag header,
188+
// the request may not have the tag header "Knative-Serving-Tag",
189+
// even though the ingress path used in the case is also originated
190+
// from the same Knative route with the ingress path for the tag based routing.
191+
//
192+
// To prevent such inconsistency,
193+
// the tag header is appended with the tag corresponding to the tag-attached hostname
194+
rule.HTTP.Paths[0].AppendHeaders[netheader.RouteTagKey] = name
195+
}
154196
}
155197

156-
if name == traffic.DefaultTarget {
157-
// To provide information if a request is routed via the "default route" or not,
158-
// the header "Knative-Serving-Default-Route: true" is appended here.
159-
// If the header has "true" and there is a "Knative-Serving-Tag" header,
160-
// then the request is having the undefined tag header,
161-
// which will be observed in queue-proxy.
162-
rule.HTTP.Paths[0].AppendHeaders[netheader.DefaultRouteKey] = "true"
163-
164-
// Add ingress paths for a request with the tag header.
165-
// If a request has one of the `names` (tag name), specified as the
166-
// Knative-Serving-Tag header, except for the DefaultTarget,
167-
// the request will be routed to that target.
168-
// corresponding to the tag name.
169-
// Since names are sorted `DefaultTarget == ""` is the first one,
170-
// so just pass the subslice.
171-
rule.HTTP.Paths = append(
172-
makeTagBasedRoutingIngressPaths(r.Namespace, tc, ro, networkConfig.SystemInternalTLSEnabled(), names[1:]), rule.HTTP.Paths...)
173-
} else {
174-
// If a request is routed by a tag-attached hostname instead of the tag header,
175-
// the request may not have the tag header "Knative-Serving-Tag",
176-
// even though the ingress path used in the case is also originated
177-
// from the same Knative route with the ingress path for the tag based routing.
178-
//
179-
// To prevent such inconsistency,
180-
// the tag header is appended with the tag corresponding to the tag-attached hostname
181-
rule.HTTP.Paths[0].AppendHeaders[netheader.RouteTagKey] = name
198+
// Merge ACME paths for ExternalIP single-host rules
199+
// Note: ACME challenges without matching traffic rules are silently ignored
200+
if visibility == netv1alpha1.IngressVisibilityExternalIP && len(rule.Hosts) == 1 {
201+
if acmePaths, exists := acmePathsByHost[rule.Hosts[0]]; exists {
202+
rule.HTTP.Paths = append(acmePaths, rule.HTTP.Paths...)
203+
}
182204
}
183205
}
184-
rules = append(rules, rule)
185-
}
186-
}
187206

188-
// Create dedicated rules for ACME challenges
189-
if len(acmeChallenges) > 0 {
190-
acmeRules := MakeACMEChallengeRules(acmeChallenges)
191-
rules = append(rules, acmeRules...)
207+
rules = append(rules, domainRules...)
208+
}
192209
}
193210

194211
httpOption, err := servingnetworking.GetHTTPOption(ctx, config.FromContext(ctx).Network, r.GetAnnotations())
@@ -203,46 +220,49 @@ func makeIngressSpec(
203220
}, nil
204221
}
205222

206-
// MakeACMEChallengeRules creates one dedicated rule per ACME challenge domain.
207-
func MakeACMEChallengeRules(acmeChallenges []netv1alpha1.HTTP01Challenge) []netv1alpha1.IngressRule {
208-
rules := make([]netv1alpha1.IngressRule, 0, len(acmeChallenges))
209-
210-
for _, challenge := range acmeChallenges {
211-
rules = append(rules, netv1alpha1.IngressRule{
212-
Hosts: []string{challenge.URL.Host},
213-
Visibility: netv1alpha1.IngressVisibilityExternalIP,
214-
HTTP: &netv1alpha1.HTTPIngressRuleValue{
215-
Paths: []netv1alpha1.HTTPIngressPath{{
216-
Splits: []netv1alpha1.IngressBackendSplit{{
217-
IngressBackend: netv1alpha1.IngressBackend{
218-
ServiceNamespace: challenge.ServiceNamespace,
219-
ServiceName: challenge.ServiceName,
220-
ServicePort: challenge.ServicePort,
221-
},
222-
Percent: 100,
223-
}},
224-
Path: challenge.URL.Path,
225-
}},
223+
// MakeACMEIngressPath converts an ACME challenge into an HTTPIngressPath.
224+
func MakeACMEIngressPath(challenge netv1alpha1.HTTP01Challenge) netv1alpha1.HTTPIngressPath {
225+
return netv1alpha1.HTTPIngressPath{
226+
Path: challenge.URL.Path,
227+
Splits: []netv1alpha1.IngressBackendSplit{{
228+
IngressBackend: netv1alpha1.IngressBackend{
229+
ServiceNamespace: challenge.ServiceNamespace,
230+
ServiceName: challenge.ServiceName,
231+
ServicePort: challenge.ServicePort,
226232
},
227-
})
233+
Percent: 100,
234+
}},
228235
}
229-
230-
return rules
231236
}
232237

233-
func makeIngressRule(domains sets.Set[string], ns string,
238+
func makeIngressRules(domains sets.Set[string], ns string,
234239
visibility netv1alpha1.IngressVisibility,
235240
targets traffic.RevisionTargets,
236241
roCfgs []*traffic.ConfigurationRollout,
237242
encryption bool,
238-
) netv1alpha1.IngressRule {
243+
) []netv1alpha1.IngressRule {
244+
basePath := makeBaseIngressPath(ns, targets, roCfgs, encryption)
245+
246+
// ClusterLocal: keep multi-host (no ACME challenges needed)
247+
if visibility == netv1alpha1.IngressVisibilityClusterLocal {
248+
return []netv1alpha1.IngressRule{makeIngressRuleForHosts(sets.List(domains), visibility, basePath)}
249+
}
250+
251+
// ExternalIP: create one rule per domain (enables per-host ACME challenge merging)
252+
domainList := sets.List(domains)
253+
rules := make([]netv1alpha1.IngressRule, 0, len(domainList))
254+
for _, domain := range domainList {
255+
rules = append(rules, makeIngressRuleForHosts([]string{domain}, visibility, basePath))
256+
}
257+
return rules
258+
}
259+
260+
func makeIngressRuleForHosts(hosts []string, visibility netv1alpha1.IngressVisibility, basePath *netv1alpha1.HTTPIngressPath) netv1alpha1.IngressRule {
239261
return netv1alpha1.IngressRule{
240-
Hosts: sets.List(domains),
262+
Hosts: hosts,
241263
Visibility: visibility,
242264
HTTP: &netv1alpha1.HTTPIngressRuleValue{
243-
Paths: []netv1alpha1.HTTPIngressPath{
244-
*makeBaseIngressPath(ns, targets, roCfgs, encryption),
245-
},
265+
Paths: []netv1alpha1.HTTPIngressPath{*basePath},
246266
},
247267
}
248268
}

0 commit comments

Comments
 (0)