Skip to content

Commit a84a2d4

Browse files
committed
fix: bug in overlap detection of cert SANs
Signed-off-by: Rudrakh Panigrahi <rudrakh97@gmail.com>
1 parent 5e41fac commit a84a2d4

File tree

3 files changed

+40
-25
lines changed

3 files changed

+40
-25
lines changed

internal/gatewayapi/listener.go

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ func checkOverlappingHostnames(httpsListeners []*ListenerContext) {
246246
if httpsListeners[i].Port != httpsListeners[j].Port {
247247
continue
248248
}
249-
if isOverlappingHostname(httpsListeners[i].Hostname, httpsListeners[j].Hostname) {
249+
if areOverlappingHostnames(httpsListeners[i].Hostname, httpsListeners[j].Hostname) {
250250
// Overlapping listeners can be more than two, we only report the first two for simplicity.
251251
overlappingListeners[i] = &overlappingListener{
252252
gateway1: httpsListeners[i].gateway,
@@ -398,7 +398,7 @@ type overlappingCertificate struct {
398398
func isOverlappingCertificate(cert1DNSNames, cert2DNSNames []string) *overlappingCertificate {
399399
for _, dns1 := range cert1DNSNames {
400400
for _, dns2 := range cert2DNSNames {
401-
if isOverlappingHostname(ptr.To(gwapiv1.Hostname(dns1)), ptr.To(gwapiv1.Hostname(dns2))) {
401+
if areOverlappingHostnames(ptr.To(gwapiv1.Hostname(dns1)), ptr.To(gwapiv1.Hostname(dns2))) {
402402
return &overlappingCertificate{
403403
san1: dns1,
404404
san2: dns2,
@@ -409,22 +409,31 @@ func isOverlappingCertificate(cert1DNSNames, cert2DNSNames []string) *overlappin
409409
return nil
410410
}
411411

412-
// isOverlappingHostname checks if two hostnames overlap.
413-
func isOverlappingHostname(hostname1, hostname2 *gwapiv1.Hostname) bool {
414-
if hostname1 == nil || hostname2 == nil {
412+
func areOverlappingHostnames(this, other *gwapiv1.Hostname) bool {
413+
if this == nil || other == nil {
415414
return true
416415
}
417-
domain1 := strings.Replace(string(*hostname1), "*.", "", 1)
418-
domain2 := strings.Replace(string(*hostname2), "*.", "", 1)
419-
return isSubdomain(domain1, domain2) || isSubdomain(domain2, domain1)
416+
return hostnameMatchesWithOther(this, other) || hostnameMatchesWithOther(other, this)
420417
}
421418

422-
// isSubdomain checks if subDomain is a sub-domain of domain
423-
func isSubdomain(subDomain, domain string) bool {
424-
if subDomain == domain {
425-
return true
419+
// hostnameMatchesWithOther returns true if this hostname matches other hostname.
420+
// Assumes that hostnames will either be fully qualified or a wildcard hostname prefixed with a single wildcard.
421+
// E.g. "*.*.example.com" is not valid.
422+
func hostnameMatchesWithOther(this, other *gwapiv1.Hostname) bool {
423+
thisString := string(*this)
424+
otherString := string(*other)
425+
if hasWildcardPrefix(other) && !hasWildcardPrefix(this) {
426+
return strings.HasSuffix(thisString, otherString[1:]) &&
427+
!strings.Contains(strings.TrimSuffix(thisString, otherString[1:]), ".") // not a subdomain
428+
}
429+
return thisString == otherString
430+
}
431+
432+
func hasWildcardPrefix(h *gwapiv1.Hostname) bool {
433+
if h == nil {
434+
return false
426435
}
427-
return strings.HasSuffix(subDomain, fmt.Sprintf(".%s", domain))
436+
return len(string(*h)) > 1 && string(*h)[0] == '*'
428437
}
429438

430439
func buildListenerMetadata(listener *ListenerContext, gateway *GatewayContext) *ir.ResourceMetadata {

internal/gatewayapi/listener_test.go

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ func TestProxySamplingRate(t *testing.T) {
100100
}
101101
}
102102

103-
func TestIsOverlappingHostname(t *testing.T) {
103+
func TestAreOverlappingHostnames(t *testing.T) {
104104
tests := []struct {
105105
name string
106106
hostname1 *gwapiv1.Hostname
@@ -120,10 +120,10 @@ func TestIsOverlappingHostname(t *testing.T) {
120120
want: true,
121121
},
122122
{
123-
name: "two wildcards matches subdomain",
123+
name: "two wildcards with subdomain does not match",
124124
hostname1: ptr.To(gwapiv1.Hostname("*.example.com")),
125125
hostname2: ptr.To(gwapiv1.Hostname("*.test.example.com")),
126-
want: true,
126+
want: false,
127127
},
128128
{
129129
name: "nil hostname matches all",
@@ -144,22 +144,22 @@ func TestIsOverlappingHostname(t *testing.T) {
144144
want: true,
145145
},
146146
{
147-
name: "wildcard matches exact",
147+
name: "wildcard matches exactly one level of subdomain",
148148
hostname1: ptr.To(gwapiv1.Hostname("*.example.com")),
149149
hostname2: ptr.To(gwapiv1.Hostname("test.example.com")),
150150
want: true,
151151
},
152152
{
153-
name: "wildcard matches subdomain",
153+
name: "wildcard matches only one level of subdomain",
154154
hostname1: ptr.To(gwapiv1.Hostname("*.example.com")),
155155
hostname2: ptr.To(gwapiv1.Hostname("sub.test.example.com")),
156-
want: true,
156+
want: false,
157157
},
158158
{
159-
name: "wildcard matches empty subdomain",
159+
name: "wildcard does not match empty subdomain",
160160
hostname1: ptr.To(gwapiv1.Hostname("*.example.com")),
161161
hostname2: ptr.To(gwapiv1.Hostname("example.com")),
162-
want: true,
162+
want: false,
163163
},
164164
{
165165
name: "different domains",
@@ -185,15 +185,21 @@ func TestIsOverlappingHostname(t *testing.T) {
185185
hostname2: ptr.To(gwapiv1.Hostname("testing-api.foo.dev")),
186186
want: false,
187187
},
188+
{
189+
name: "sub domain does not match with parent domain",
190+
hostname1: ptr.To(gwapiv1.Hostname("api.foo.dev")),
191+
hostname2: ptr.To(gwapiv1.Hostname("foo.dev")),
192+
want: false,
193+
},
188194
}
189195

190196
for _, tt := range tests {
191197
t.Run(tt.name, func(t *testing.T) {
192-
if got := isOverlappingHostname(tt.hostname1, tt.hostname2); got != tt.want {
198+
if got := areOverlappingHostnames(tt.hostname1, tt.hostname2); got != tt.want {
193199
t.Errorf("isOverlappingHostname(%q, %q) = %v, want %v", ptr.Deref(tt.hostname1, ""), ptr.Deref(tt.hostname2, ""), got, tt.want)
194200
}
195201
// Test should be symmetric
196-
if got := isOverlappingHostname(tt.hostname2, tt.hostname1); got != tt.want {
202+
if got := areOverlappingHostnames(tt.hostname2, tt.hostname1); got != tt.want {
197203
t.Errorf("isOverlappingHostname(%q, %q) = %v, want %v", ptr.Deref(tt.hostname2, ""), ptr.Deref(tt.hostname1, ""), got, tt.want)
198204
}
199205
})
@@ -306,15 +312,14 @@ func TestCheckOverlappingHostnames(t *testing.T) {
306312
Name: "listener-3",
307313
Protocol: gwapiv1.HTTPSProtocolType,
308314
Port: 443,
309-
Hostname: ptr.To(gwapiv1.Hostname("sub.test.example.com")),
315+
Hostname: ptr.To(gwapiv1.Hostname("sub.test.example.com")), // sub domain does not match with parent domain
310316
},
311317
},
312318
},
313319
},
314320
expected: map[int]string{
315321
0: "test.example.com",
316322
1: "*.example.com",
317-
2: "*.example.com",
318323
},
319324
},
320325
{

release-notes/current.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ bug fixes: |
3838
Fixed a nil pointer panic in the XDS translator when building API key authentication filter configurations with `sanitize` enabled and no `forwardClientIDHeader` set.
3939
Truncated Gateway API status condition messages to stay within Kubernetes limits and prevent update failures.
4040
Fixed an issue in EnvoyPatchPolicy where it didn't match the target Gateway/GatewayClass due to an incorrect name reference.
41+
Fixed certificate SAN overlap detection in gateway listeners.
4142
4243
# Enhancements that improve performance.
4344
performance improvements: |

0 commit comments

Comments
 (0)