From b5be8a6fa4d8f80fcb372a451e6d7c341e3b0f51 Mon Sep 17 00:00:00 2001 From: Steven Kreitzer Date: Thu, 18 Jan 2024 09:19:11 -0600 Subject: [PATCH] feat: auto tls sni Signed-off-by: Steven Kreitzer --- config/configuration.go | 2 ++ ingress/config.go | 19 +++++++++++++++++++ ingress/origin_proxy.go | 21 +++++++++++++++++++++ ingress/origin_service.go | 8 +++++--- ingress/rule_test.go | 8 ++++---- 5 files changed, 51 insertions(+), 7 deletions(-) diff --git a/config/configuration.go b/config/configuration.go index 0a8228b7..05122d76 100644 --- a/config/configuration.go +++ b/config/configuration.go @@ -205,6 +205,8 @@ type OriginRequestConfig struct { HTTPHostHeader *string `yaml:"httpHostHeader" json:"httpHostHeader,omitempty"` // Hostname on the origin server certificate. OriginServerName *string `yaml:"originServerName" json:"originServerName,omitempty"` + // Auto configure the Hostname on the origin server certificate. + MatchSNIToHost *bool `yaml:"matchSNItoHost" json:"matchSNItoHost,omitempty"` // Path to the CA for the certificate of your origin. // This option should be used only if your certificate is not signed by Cloudflare. CAPool *string `yaml:"caPool" json:"caPool,omitempty"` diff --git a/ingress/config.go b/ingress/config.go index 2c0c97c8..670a9db1 100644 --- a/ingress/config.go +++ b/ingress/config.go @@ -32,6 +32,7 @@ const ( ProxyKeepAliveTimeoutFlag = "proxy-keepalive-timeout" HTTPHostHeaderFlag = "http-host-header" OriginServerNameFlag = "origin-server-name" + MatchSNIToHostFlag = "match-sni-to-host" NoTLSVerifyFlag = "no-tls-verify" NoChunkedEncodingFlag = "no-chunked-encoding" ProxyAddressFlag = "proxy-address" @@ -118,6 +119,7 @@ func originRequestFromSingleRule(c *cli.Context) OriginRequestConfig { var keepAliveTimeout = defaultKeepAliveTimeout var httpHostHeader string var originServerName string + var matchSNItoHost bool var caPool string var noTLSVerify bool var disableChunkedEncoding bool @@ -150,6 +152,9 @@ func originRequestFromSingleRule(c *cli.Context) OriginRequestConfig { if flag := OriginServerNameFlag; c.IsSet(flag) { originServerName = c.String(flag) } + if flag := MatchSNIToHostFlag; c.IsSet(flag) { + matchSNItoHost = c.Bool(flag) + } if flag := tlsconfig.OriginCAPoolFlag; c.IsSet(flag) { caPool = c.String(flag) } @@ -185,6 +190,7 @@ func originRequestFromSingleRule(c *cli.Context) OriginRequestConfig { KeepAliveTimeout: keepAliveTimeout, HTTPHostHeader: httpHostHeader, OriginServerName: originServerName, + MatchSNIToHost: matchSNItoHost, CAPool: caPool, NoTLSVerify: noTLSVerify, DisableChunkedEncoding: disableChunkedEncoding, @@ -229,6 +235,9 @@ func originRequestFromConfig(c config.OriginRequestConfig) OriginRequestConfig { if c.OriginServerName != nil { out.OriginServerName = *c.OriginServerName } + if c.MatchSNIToHost != nil { + out.MatchSNIToHost = *c.MatchSNIToHost + } if c.CAPool != nil { out.CAPool = *c.CAPool } @@ -287,6 +296,8 @@ type OriginRequestConfig struct { HTTPHostHeader string `yaml:"httpHostHeader" json:"httpHostHeader"` // Hostname on the origin server certificate. OriginServerName string `yaml:"originServerName" json:"originServerName"` + // Auto configure the Hostname on the origin server certificate. + MatchSNIToHost bool `yaml:"matchSNItoHost" json:"matchSNItoHost"` // Path to the CA for the certificate of your origin. // This option should be used only if your certificate is not signed by Cloudflare. CAPool string `yaml:"caPool" json:"caPool"` @@ -362,6 +373,12 @@ func (defaults *OriginRequestConfig) setOriginServerName(overrides config.Origin } } +func (defaults *OriginRequestConfig) setMatchSNIToHost(overrides config.OriginRequestConfig) { + if val := overrides.MatchSNIToHost; val != nil { + defaults.MatchSNIToHost = *val + } +} + func (defaults *OriginRequestConfig) setCAPool(overrides config.OriginRequestConfig) { if val := overrides.CAPool; val != nil { defaults.CAPool = *val @@ -447,6 +464,7 @@ func setConfig(defaults OriginRequestConfig, overrides config.OriginRequestConfi cfg.setTCPKeepAlive(overrides) cfg.setHTTPHostHeader(overrides) cfg.setOriginServerName(overrides) + cfg.setMatchSNIToHost(overrides) cfg.setCAPool(overrides) cfg.setNoTLSVerify(overrides) cfg.setDisableChunkedEncoding(overrides) @@ -501,6 +519,7 @@ func ConvertToRawOriginConfig(c OriginRequestConfig) config.OriginRequestConfig KeepAliveTimeout: keepAliveTimeout, HTTPHostHeader: emptyStringToNil(c.HTTPHostHeader), OriginServerName: emptyStringToNil(c.OriginServerName), + MatchSNIToHost: defaultBoolToNil(c.MatchSNIToHost), CAPool: emptyStringToNil(c.CAPool), NoTLSVerify: defaultBoolToNil(c.NoTLSVerify), DisableChunkedEncoding: defaultBoolToNil(c.DisableChunkedEncoding), diff --git a/ingress/origin_proxy.go b/ingress/origin_proxy.go index 186ddff1..1caf28f0 100644 --- a/ingress/origin_proxy.go +++ b/ingress/origin_proxy.go @@ -2,7 +2,9 @@ package ingress import ( "context" + "crypto/tls" "fmt" + "net" "net/http" "github.com/rs/zerolog" @@ -48,9 +50,28 @@ func (o *httpService) RoundTrip(req *http.Request) (*http.Response, error) { req.Header.Set("X-Forwarded-Host", req.Host) req.Host = o.hostHeader } + + if o.matchSNIToHost { + o.SetOriginServerName(req) + } + return o.transport.RoundTrip(req) } +func (o *httpService) SetOriginServerName(req *http.Request) { + o.transport.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + conn, err := o.transport.DialContext(ctx, network, addr) + if err != nil { + return nil, err + } + return tls.Client(conn, &tls.Config{ + RootCAs: o.transport.TLSClientConfig.RootCAs, + InsecureSkipVerify: o.transport.TLSClientConfig.InsecureSkipVerify, + ServerName: req.Host, + }), nil + } +} + func (o *statusCode) RoundTrip(_ *http.Request) (*http.Response, error) { if o.defaultResp { o.log.Warn().Msgf(ErrNoIngressRulesCLI.Error()) diff --git a/ingress/origin_service.go b/ingress/origin_service.go index ec7ccb48..e13204c5 100644 --- a/ingress/origin_service.go +++ b/ingress/origin_service.go @@ -68,9 +68,10 @@ func (o unixSocketPath) MarshalJSON() ([]byte, error) { } type httpService struct { - url *url.URL - hostHeader string - transport *http.Transport + url *url.URL + hostHeader string + transport *http.Transport + matchSNIToHost bool } func (o *httpService) start(log *zerolog.Logger, _ <-chan struct{}, cfg OriginRequestConfig) error { @@ -80,6 +81,7 @@ func (o *httpService) start(log *zerolog.Logger, _ <-chan struct{}, cfg OriginRe } o.hostHeader = cfg.HTTPHostHeader o.transport = transport + o.matchSNIToHost = cfg.MatchSNIToHost return nil } diff --git a/ingress/rule_test.go b/ingress/rule_test.go index 1a46f155..a3d12e01 100644 --- a/ingress/rule_test.go +++ b/ingress/rule_test.go @@ -204,25 +204,25 @@ func TestMarshalJSON(t *testing.T) { { name: "Nil", path: nil, - expected: `{"hostname":"example.com","path":null,"service":"https://localhost:8000","Handlers":null,"originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"teamName":"","audTag":null}}}`, + expected: `{"hostname":"example.com","path":null,"service":"https://localhost:8000","Handlers":null,"originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","matchSNItoHost":false,"caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"teamName":"","audTag":null}}}`, want: true, }, { name: "Nil regex", path: &Regexp{Regexp: nil}, - expected: `{"hostname":"example.com","path":null,"service":"https://localhost:8000","Handlers":null,"originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"teamName":"","audTag":null}}}`, + expected: `{"hostname":"example.com","path":null,"service":"https://localhost:8000","Handlers":null,"originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","matchSNItoHost":false,"caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"teamName":"","audTag":null}}}`, want: true, }, { name: "Empty", path: &Regexp{Regexp: regexp.MustCompile("")}, - expected: `{"hostname":"example.com","path":"","service":"https://localhost:8000","Handlers":null,"originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"teamName":"","audTag":null}}}`, + expected: `{"hostname":"example.com","path":"","service":"https://localhost:8000","Handlers":null,"originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","matchSNItoHost":false,"caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"teamName":"","audTag":null}}}`, want: true, }, { name: "Basic", path: &Regexp{Regexp: regexp.MustCompile("/echo")}, - expected: `{"hostname":"example.com","path":"/echo","service":"https://localhost:8000","Handlers":null,"originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"teamName":"","audTag":null}}}`, + expected: `{"hostname":"example.com","path":"/echo","service":"https://localhost:8000","Handlers":null,"originRequest":{"connectTimeout":30,"tlsTimeout":10,"tcpKeepAlive":30,"noHappyEyeballs":false,"keepAliveTimeout":90,"keepAliveConnections":100,"httpHostHeader":"","originServerName":"","matchSNItoHost":false,"caPool":"","noTLSVerify":false,"disableChunkedEncoding":false,"bastionMode":false,"proxyAddress":"127.0.0.1","proxyPort":0,"proxyType":"","ipRules":null,"http2Origin":false,"access":{"teamName":"","audTag":null}}}`, want: true, }, }