From 3048509807f655d01588d838b16ba1684fc9df73 Mon Sep 17 00:00:00 2001 From: Drew Wells Date: Thu, 6 Apr 2017 17:10:02 -0500 Subject: [PATCH] enable TLS client forwarding Copys the incoming TLS client certificate to the outgoing request. The backend can then use this certificate for client authentication ie. k8s client cert authentication --- docs/basics.md | 63 ++++++++++++++++---------------- docs/user-guide/examples.md | 1 + server/server.go | 72 +++++++++++++++++++++++++++++++++---- types/types.go | 1 + 4 files changed, 99 insertions(+), 38 deletions(-) diff --git a/docs/basics.md b/docs/basics.md index 47a606df4..c457cfafd 100644 --- a/docs/basics.md +++ b/docs/basics.md @@ -94,7 +94,7 @@ Separate multiple rule values by `,` (comma) in order to enable ANY semantics (i Separate multiple rule values by `;` (semicolon) in order to enable ALL semantics (i.e., forward a request if all rules match). -You can optionally enable `passHostHeader` to forward client `Host` header to the backend. +You can optionally enable `passHostHeader` to forward client `Host` header to the backend. You can also optionally enable `passTLSCert` to forward TLS Client certificates to the backend. Following is the list of existing matcher rules along with examples: @@ -140,6 +140,7 @@ Here is an example of frontends definition: [frontends.frontend2] backend = "backend1" passHostHeader = true + passTLSCert = true priority = 10 entrypoints = ["https"] # overrides defaultEntryPoints [frontends.frontend2.routes.test_1] @@ -161,34 +162,34 @@ As seen in the previous example, you can combine multiple rules. In TOML file, you can use multiple routes: ```toml -[frontends.frontend3] -backend = "backend2" - [frontends.frontend3.routes.test_1] - rule = "Host:test3.localhost" - [frontends.frontend3.routes.test_2] - rule = "Path:/test" + [frontends.frontend3] + backend = "backend2" + [frontends.frontend3.routes.test_1] + rule = "Host:test3.localhost" + [frontends.frontend3.routes.test_2] + rule = "Path:/test" ``` Here `frontend3` will forward the traffic to the `backend2` if the rules `Host:test3.localhost` **AND** `Path:/test` are matched. You can also use the notation using a `;` separator, same result: ```toml -[frontends.frontend3] -backend = "backend2" - [frontends.frontend3.routes.test_1] - rule = "Host:test3.localhost;Path:/test" + [frontends.frontend3] + backend = "backend2" + [frontends.frontend3.routes.test_1] + rule = "Host:test3.localhost;Path:/test" ``` Finally, you can create a rule to bind multiple domains or Path to a frontend, using the `,` separator: ```toml -[frontends.frontend2] - [frontends.frontend2.routes.test_1] - rule = "Host:test1.localhost,test2.localhost" -[frontends.frontend3] -backend = "backend2" - [frontends.frontend3.routes.test_1] - rule = "Path:/test1,/test2" + [frontends.frontend2] + [frontends.frontend2.routes.test_1] + rule = "Host:test1.localhost,test2.localhost" + [frontends.frontend3] + backend = "backend2" + [frontends.frontend3.routes.test_1] + rule = "Path:/test1,/test2" ``` ### Rules Order @@ -218,19 +219,19 @@ By default, routes will be sorted (in descending order) using rules length (to a You can customize priority by frontend: ```toml -[frontends] - [frontends.frontend1] - backend = "backend1" - priority = 10 - passHostHeader = true - [frontends.frontend1.routes.test_1] - rule = "PathPrefix:/to" - [frontends.frontend2] - priority = 5 - backend = "backend2" - passHostHeader = true - [frontends.frontend2.routes.test_1] - rule = "PathPrefix:/toto" + [frontends] + [frontends.frontend1] + backend = "backend1" + priority = 10 + passHostHeader = true + [frontends.frontend1.routes.test_1] + rule = "PathPrefix:/to" + [frontends.frontend2] + priority = 5 + backend = "backend2" + passHostHeader = true + [frontends.frontend2.routes.test_1] + rule = "PathPrefix:/toto" ``` Here, `frontend1` will be matched before `frontend2` (`10 > 5`). diff --git a/docs/user-guide/examples.md b/docs/user-guide/examples.md index 9aa9af16e..f58828fe6 100644 --- a/docs/user-guide/examples.md +++ b/docs/user-guide/examples.md @@ -89,6 +89,7 @@ entryPoint = "https" [frontends.frontend2] backend = "backend1" passHostHeader = true + passTLSCert = true entrypoints = ["https"] # overrides defaultEntryPoints [frontends.frontend2.routes.test_1] rule = "Host:{subdomain:[a-z]+}.localhost" diff --git a/server/server.go b/server/server.go index 9bac653a2..83cb656a5 100644 --- a/server/server.go +++ b/server/server.go @@ -418,6 +418,33 @@ func (server *Server) listenSignals() { server.Stop() } +func createClientTLSConfig(tlsOption *TLS) (*tls.Config, error) { + if tlsOption == nil { + return nil, errors.New("no TLS provided") + } + + config, err := tlsOption.Certificates.CreateTLSConfig() + if err != nil { + return nil, err + } + + if len(tlsOption.ClientCAFiles) > 0 { + pool := x509.NewCertPool() + for _, caFile := range tlsOption.ClientCAFiles { + data, err := ioutil.ReadFile(caFile) + if err != nil { + return nil, err + } + if !pool.AppendCertsFromPEM(data) { + return nil, errors.New("invalid certificate(s) in " + caFile) + } + } + config.RootCAs = pool + } + config.BuildNameToCertificate() + return config, nil +} + // creates a TLS config that allows terminating HTTPS for multiple domains using SNI func (server *Server) createTLSConfig(entryPointName string, tlsOption *TLS, router *middlewares.HandlerSwitcher) (*tls.Config, error) { if tlsOption == nil { @@ -500,6 +527,7 @@ func (server *Server) createTLSConfig(entryPointName string, tlsOption *TLS, rou } } } + return config, nil } @@ -549,6 +577,17 @@ func (server *Server) buildEntryPoints(globalConfiguration GlobalConfiguration) return serverEntryPoints } +// clientTLSRoundTripper is used for forwarding client authentication to +// backend server +func clientTLSRoundTripper(config *tls.Config) http.RoundTripper { + if config == nil { + return http.DefaultTransport + } + return &http.Transport{ + TLSClientConfig: config, + } +} + // LoadConfig returns a new gorilla.mux Route from the specified global configuration and the dynamic // provider configurations. func (server *Server) loadConfig(configurations configs, globalConfiguration GlobalConfiguration) (map[string]*serverEntryPoint, error) { @@ -565,12 +604,6 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo log.Debugf("Creating frontend %s", frontendName) - fwd, err := forward.New(forward.Logger(oxyLogger), forward.PassHostHeader(frontend.PassHostHeader)) - if err != nil { - log.Errorf("Error creating forwarder for frontend %s: %v", frontendName, err) - log.Errorf("Skipping frontend %s...", frontendName) - continue frontend - } if len(frontend.EntryPoints) == 0 { log.Errorf("No entrypoint defined for frontend %s, defaultEntryPoints:%s", frontendName, globalConfiguration.DefaultEntryPoints) log.Errorf("Skipping frontend %s...", frontendName) @@ -618,6 +651,31 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo } if backends[entryPointName+frontend.Backend] == nil { log.Debugf("Creating backend %s", frontend.Backend) + + var ( + tlsConfig *tls.Config + err error + lb http.Handler + ) + + if frontend.PassTLSCert { + tlsConfig, err = createClientTLSConfig(entryPoint.TLS) + if err != nil { + log.Errorf("Failed to create TLS config for frontend %s: %v", frontendName, err) + continue frontend + } + } + + // passing nil will use the roundtripper http.DefaultTransport + rt := clientTLSRoundTripper(tlsConfig) + + fwd, err := forward.New(forward.Logger(oxyLogger), forward.PassHostHeader(frontend.PassHostHeader), forward.RoundTripper(rt)) + if err != nil { + log.Errorf("Error creating forwarder for frontend %s: %v", frontendName, err) + log.Errorf("Skipping frontend %s...", frontendName) + continue frontend + } + var rr *roundrobin.RoundRobin var saveFrontend http.Handler if server.accessLoggerMiddleware != nil { @@ -627,6 +685,7 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo } else { rr, _ = roundrobin.New(fwd) } + if configuration.Backends[frontend.Backend] == nil { log.Errorf("Undefined backend '%s' for frontend %s", frontend.Backend, frontendName) log.Errorf("Skipping frontend %s...", frontendName) @@ -648,7 +707,6 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo sticky = roundrobin.NewStickySession(cookiename) } - var lb http.Handler switch lbMethod { case types.Drr: log.Debugf("Creating load-balancer drr") diff --git a/types/types.go b/types/types.go index 3bffff7f4..2fed8e25e 100644 --- a/types/types.go +++ b/types/types.go @@ -60,6 +60,7 @@ type Frontend struct { Backend string `json:"backend,omitempty"` Routes map[string]Route `json:"routes,omitempty"` PassHostHeader bool `json:"passHostHeader,omitempty"` + PassTLSCert bool `json:"passTLSCert,omitempty"` Priority int `json:"priority"` BasicAuth []string `json:"basicAuth"` WhitelistSourceRange []string `json:"whitelistSourceRange,omitempty"`