Skip to content

Commit 8467a4a

Browse files
committed
fix: strip port from Host header before mTLS domain matching
HTTP clients that include explicit ports in URLs (e.g., https://app.example.com:443/) result in Go's http.Request.Host containing the port (app.example.com:443). Previously, GetMtlsDomainConfig() did not strip the port before matching against configured mTLS domains (e.g., *.apps.identity), causing: - Domain matching to fail for requests with explicit ports - No XFCC header added (fell back to default behavior) - Identity extraction failure in CallerIdentity - Pre-auth handler denying requests with 403 and reason "identity-extraction-failed" This particularly affected Java Spring Boot HTTP clients which construct URLs with explicit ports by default. Fix: Use net.SplitHostPort() to strip port before domain matching, ensuring consistent behavior regardless of whether clients include explicit ports. Added comprehensive unit tests covering: - Wildcard domain matching with/without ports - Exact domain matching with/without ports - IsMtlsDomain() function with/without ports - Negative test cases for non-mTLS domains
1 parent a79e779 commit 8467a4a

2 files changed

Lines changed: 110 additions & 0 deletions

File tree

src/code.cloudfoundry.org/gorouter/config/config.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"errors"
88
"fmt"
99
"log/slog"
10+
"net"
1011
"net/url"
1112
"os"
1213
"runtime"
@@ -1015,6 +1016,12 @@ func (c *Config) RoutingApiEnabled() bool {
10151016
// It checks for exact matches first, then wildcard matches (e.g., *.apps.mtls.internal).
10161017
// Returns nil if the host is not an mTLS domain.
10171018
func (c *Config) GetMtlsDomainConfig(host string) *MtlsDomainConfig {
1019+
// Strip port if present (e.g., "app.example.com:443" → "app.example.com")
1020+
// This ensures consistent matching regardless of whether clients include explicit ports
1021+
if h, _, err := net.SplitHostPort(host); err == nil {
1022+
host = h
1023+
}
1024+
10181025
// Check exact match first
10191026
if cfg, ok := c.mtlsDomainMap[host]; ok {
10201027
return cfg

src/code.cloudfoundry.org/gorouter/config/config_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2043,6 +2043,109 @@ drain_timeout: 60s
20432043
})
20442044

20452045
})
2046+
2047+
Describe("GetMtlsDomainConfig", func() {
2048+
var certChain test_util.CertChain
2049+
2050+
BeforeEach(func() {
2051+
certChain = test_util.CreateSignedCertWithRootCA(test_util.CertNames{SANs: test_util.SubjectAltNames{DNS: "test.com"}})
2052+
cfgForSnippet.Domains = []MtlsDomainConfig{
2053+
{
2054+
Domain: "*.apps.identity",
2055+
XFCCFormat: "envoy",
2056+
CACerts: string(certChain.CACertPEM),
2057+
},
2058+
{
2059+
Domain: "exact.example.com",
2060+
XFCCFormat: "raw",
2061+
CACerts: string(certChain.CACertPEM),
2062+
},
2063+
}
2064+
err := config.Initialize(createYMLSnippet(cfgForSnippet))
2065+
Expect(err).ToNot(HaveOccurred())
2066+
err = config.Process()
2067+
Expect(err).ToNot(HaveOccurred())
2068+
})
2069+
2070+
Context("when host includes explicit port", func() {
2071+
It("strips port and matches wildcard domain", func() {
2072+
cfg := config.GetMtlsDomainConfig("xfcc-tester.apps.identity:443")
2073+
Expect(cfg).ToNot(BeNil())
2074+
Expect(cfg.Domain).To(Equal("*.apps.identity"))
2075+
Expect(cfg.XFCCFormat).To(Equal("envoy"))
2076+
})
2077+
2078+
It("strips port and matches exact domain", func() {
2079+
cfg := config.GetMtlsDomainConfig("exact.example.com:8443")
2080+
Expect(cfg).ToNot(BeNil())
2081+
Expect(cfg.Domain).To(Equal("exact.example.com"))
2082+
Expect(cfg.XFCCFormat).To(Equal("raw"))
2083+
})
2084+
})
2085+
2086+
Context("when host does not include port", func() {
2087+
It("matches wildcard domain without port", func() {
2088+
cfg := config.GetMtlsDomainConfig("xfcc-tester.apps.identity")
2089+
Expect(cfg).ToNot(BeNil())
2090+
Expect(cfg.Domain).To(Equal("*.apps.identity"))
2091+
Expect(cfg.XFCCFormat).To(Equal("envoy"))
2092+
})
2093+
2094+
It("matches exact domain without port", func() {
2095+
cfg := config.GetMtlsDomainConfig("exact.example.com")
2096+
Expect(cfg).ToNot(BeNil())
2097+
Expect(cfg.Domain).To(Equal("exact.example.com"))
2098+
Expect(cfg.XFCCFormat).To(Equal("raw"))
2099+
})
2100+
})
2101+
2102+
Context("when host is not an mTLS domain", func() {
2103+
It("returns nil for non-matching host with port", func() {
2104+
cfg := config.GetMtlsDomainConfig("other.example.com:443")
2105+
Expect(cfg).To(BeNil())
2106+
})
2107+
2108+
It("returns nil for non-matching host without port", func() {
2109+
cfg := config.GetMtlsDomainConfig("other.example.com")
2110+
Expect(cfg).To(BeNil())
2111+
})
2112+
})
2113+
})
2114+
2115+
Describe("IsMtlsDomain", func() {
2116+
var certChain test_util.CertChain
2117+
2118+
BeforeEach(func() {
2119+
certChain = test_util.CreateSignedCertWithRootCA(test_util.CertNames{SANs: test_util.SubjectAltNames{DNS: "test.com"}})
2120+
cfgForSnippet.Domains = []MtlsDomainConfig{
2121+
{
2122+
Domain: "*.apps.identity",
2123+
XFCCFormat: "envoy",
2124+
CACerts: string(certChain.CACertPEM),
2125+
},
2126+
}
2127+
err := config.Initialize(createYMLSnippet(cfgForSnippet))
2128+
Expect(err).ToNot(HaveOccurred())
2129+
err = config.Process()
2130+
Expect(err).ToNot(HaveOccurred())
2131+
})
2132+
2133+
It("returns true for mTLS domain with port", func() {
2134+
Expect(config.IsMtlsDomain("xfcc-tester.apps.identity:443")).To(BeTrue())
2135+
})
2136+
2137+
It("returns true for mTLS domain without port", func() {
2138+
Expect(config.IsMtlsDomain("xfcc-tester.apps.identity")).To(BeTrue())
2139+
})
2140+
2141+
It("returns false for non-mTLS domain with port", func() {
2142+
Expect(config.IsMtlsDomain("other.example.com:443")).To(BeFalse())
2143+
})
2144+
2145+
It("returns false for non-mTLS domain without port", func() {
2146+
Expect(config.IsMtlsDomain("other.example.com")).To(BeFalse())
2147+
})
2148+
})
20462149
})
20472150

20482151
func baseConfigFixture() *Config {

0 commit comments

Comments
 (0)