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
This commit is contained in:
Drew Wells 2017-04-06 17:10:02 -05:00 committed by Ludovic Fernandez
parent 7399a83c74
commit 3048509807
4 changed files with 99 additions and 38 deletions

View file

@ -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`).

View file

@ -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"

View file

@ -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")

View file

@ -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"`