diff --git a/cmd/traefik/traefik.go b/cmd/traefik/traefik.go index d9c703e8d..969a03ee9 100644 --- a/cmd/traefik/traefik.go +++ b/cmd/traefik/traefik.go @@ -229,7 +229,10 @@ func run(globalConfiguration *configuration.GlobalConfiguration) { http.DefaultTransport.(*http.Transport).Proxy = http.ProxyFromEnvironment if len(globalConfiguration.EntryPoints) == 0 { - globalConfiguration.EntryPoints = map[string]*configuration.EntryPoint{"http": {Address: ":80"}} + globalConfiguration.EntryPoints = map[string]*configuration.EntryPoint{"http": { + Address: ":80", + ForwardedHeaders: &configuration.ForwardedHeaders{Insecure: true}, + }} globalConfiguration.DefaultEntryPoints = []string{"http"} } @@ -259,6 +262,14 @@ func run(globalConfiguration *configuration.GlobalConfiguration) { globalConfiguration.LogLevel = "DEBUG" } + // ForwardedHeaders must be remove in the next breaking version + for entryPointName := range globalConfiguration.EntryPoints { + entryPoint := globalConfiguration.EntryPoints[entryPointName] + if entryPoint.ForwardedHeaders == nil { + entryPoint.ForwardedHeaders = &configuration.ForwardedHeaders{Insecure: true} + } + } + // logging level, err := logrus.ParseLevel(strings.ToLower(globalConfiguration.LogLevel)) if err != nil { diff --git a/configuration/configuration.go b/configuration/configuration.go index 7e489a008..6d7732dbe 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -10,6 +10,7 @@ import ( "github.com/containous/flaeg" "github.com/containous/traefik/acme" + "github.com/containous/traefik/log" "github.com/containous/traefik/provider/boltdb" "github.com/containous/traefik/provider/consul" "github.com/containous/traefik/provider/docker" @@ -229,11 +230,31 @@ func (ep *EntryPoints) Set(value string) error { compress := toBool(result, "compress") var proxyProtocol *ProxyProtocol - if len(result["proxyprotocol_trustedips"]) > 0 { - trustedIPs := strings.Split(result["proxyprotocol_trustedips"], ",") + ppTrustedIPs := result["proxyprotocol_trustedips"] + if len(result["proxyprotocol_insecure"]) > 0 || len(ppTrustedIPs) > 0 { proxyProtocol = &ProxyProtocol{ - TrustedIPs: trustedIPs, + Insecure: toBool(result, "proxyprotocol_insecure"), } + if len(ppTrustedIPs) > 0 { + proxyProtocol.TrustedIPs = strings.Split(ppTrustedIPs, ",") + } + } + + // TODO must be changed to false by default in the next breaking version. + forwardedHeaders := &ForwardedHeaders{Insecure: true} + if _, ok := result["forwardedheaders_insecure"]; ok { + forwardedHeaders.Insecure = toBool(result, "forwardedheaders_insecure") + } + + fhTrustedIPs := result["forwardedheaders_trustedips"] + if len(fhTrustedIPs) > 0 { + // TODO must be removed in the next breaking version. + forwardedHeaders.Insecure = toBool(result, "forwardedheaders_insecure") + forwardedHeaders.TrustedIPs = strings.Split(fhTrustedIPs, ",") + } + + if proxyProtocol != nil && proxyProtocol.Insecure { + log.Warn("ProxyProtocol.Insecure:true is dangerous. Please use 'ProxyProtocol.TrustedIPs:IPs' and remove 'ProxyProtocol.Insecure:true'") } (*ep)[result["name"]] = &EntryPoint{ @@ -243,6 +264,7 @@ func (ep *EntryPoints) Set(value string) error { Compress: compress, WhitelistSourceRange: whiteListSourceRange, ProxyProtocol: proxyProtocol, + ForwardedHeaders: forwardedHeaders, } return nil @@ -300,8 +322,9 @@ type EntryPoint struct { Redirect *Redirect `export:"true"` Auth *types.Auth `export:"true"` WhitelistSourceRange []string - Compress bool `export:"true"` - ProxyProtocol *ProxyProtocol `export:"true"` + Compress bool `export:"true"` + ProxyProtocol *ProxyProtocol `export:"true"` + ForwardedHeaders *ForwardedHeaders `export:"true"` } // Redirect configures a redirection of an entry point to another, or to an URL @@ -453,5 +476,12 @@ type ForwardingTimeouts struct { // ProxyProtocol contains Proxy-Protocol configuration type ProxyProtocol struct { + Insecure bool + TrustedIPs []string +} + +// ForwardedHeaders Trust client forwarding headers +type ForwardedHeaders struct { + Insecure bool TrustedIPs []string } diff --git a/configuration/configuration_test.go b/configuration/configuration_test.go index 5e7ac0ad7..432685855 100644 --- a/configuration/configuration_test.go +++ b/configuration/configuration_test.go @@ -1,7 +1,6 @@ package configuration import ( - "fmt" "testing" "github.com/stretchr/testify/assert" @@ -16,7 +15,7 @@ func Test_parseEntryPointsConfiguration(t *testing.T) { }{ { name: "all parameters", - value: "Name:foo TLS:goo TLS CA:car Redirect.EntryPoint:RedirectEntryPoint Redirect.Regex:RedirectRegex Redirect.Replacement:RedirectReplacement Compress:true WhiteListSourceRange:WhiteListSourceRange ProxyProtocol.TrustedIPs:192.168.0.1 Address::8000", + value: "Name:foo TLS:goo TLS CA:car Redirect.EntryPoint:RedirectEntryPoint Redirect.Regex:RedirectRegex Redirect.Replacement:RedirectReplacement Compress:true WhiteListSourceRange:WhiteListSourceRange ProxyProtocol.TrustedIPs:192.168.0.1 ProxyProtocol.Insecure:false Address::8000", expectedResult: map[string]string{ "name": "foo", "address": ":8000", @@ -28,6 +27,7 @@ func Test_parseEntryPointsConfiguration(t *testing.T) { "redirect_replacement": "RedirectReplacement", "whitelistsourcerange": "WhiteListSourceRange", "proxyprotocol_trustedips": "192.168.0.1", + "proxyprotocol_insecure": "false", "compress": "true", }, }, @@ -57,10 +57,6 @@ func Test_parseEntryPointsConfiguration(t *testing.T) { conf := parseEntryPointsConfiguration(test.value) - for key, value := range conf { - fmt.Println(key, value) - } - assert.Len(t, conf, len(test.expectedResult)) assert.Equal(t, test.expectedResult, conf) }) @@ -131,7 +127,7 @@ func TestEntryPoints_Set(t *testing.T) { }{ { name: "all parameters camelcase", - expression: "Name:foo Address::8000 TLS:goo,gii TLS CA:car Redirect.EntryPoint:RedirectEntryPoint Redirect.Regex:RedirectRegex Redirect.Replacement:RedirectReplacement Compress:true WhiteListSourceRange:Range ProxyProtocol.TrustedIPs:192.168.0.1", + expression: "Name:foo Address::8000 TLS:goo,gii TLS CA:car Redirect.EntryPoint:RedirectEntryPoint Redirect.Regex:RedirectRegex Redirect.Replacement:RedirectReplacement Compress:true WhiteListSourceRange:Range ProxyProtocol.TrustedIPs:192.168.0.1 ForwardedHeaders.TrustedIPs:10.0.0.3/24,20.0.0.3/24", expectedEntryPointName: "foo", expectedEntryPoint: &EntryPoint{ Address: ":8000", @@ -144,6 +140,9 @@ func TestEntryPoints_Set(t *testing.T) { ProxyProtocol: &ProxyProtocol{ TrustedIPs: []string{"192.168.0.1"}, }, + ForwardedHeaders: &ForwardedHeaders{ + TrustedIPs: []string{"10.0.0.3/24", "20.0.0.3/24"}, + }, WhitelistSourceRange: []string{"Range"}, TLS: &TLS{ ClientCAFiles: []string{"car"}, @@ -158,7 +157,7 @@ func TestEntryPoints_Set(t *testing.T) { }, { name: "all parameters lowercase", - expression: "name:foo address::8000 tls:goo,gii tls ca:car redirect.entryPoint:RedirectEntryPoint redirect.regex:RedirectRegex redirect.replacement:RedirectReplacement compress:true whiteListSourceRange:Range proxyProtocol.TrustedIPs:192.168.0.1", + expression: "name:foo address::8000 tls:goo,gii tls ca:car redirect.entryPoint:RedirectEntryPoint redirect.regex:RedirectRegex redirect.replacement:RedirectReplacement compress:true whiteListSourceRange:Range proxyProtocol.trustedIPs:192.168.0.1 forwardedHeaders.trustedIPs:10.0.0.3/24,20.0.0.3/24", expectedEntryPointName: "foo", expectedEntryPoint: &EntryPoint{ Address: ":8000", @@ -171,6 +170,9 @@ func TestEntryPoints_Set(t *testing.T) { ProxyProtocol: &ProxyProtocol{ TrustedIPs: []string{"192.168.0.1"}, }, + ForwardedHeaders: &ForwardedHeaders{ + TrustedIPs: []string{"10.0.0.3/24", "20.0.0.3/24"}, + }, WhitelistSourceRange: []string{"Range"}, TLS: &TLS{ ClientCAFiles: []string{"car"}, @@ -183,6 +185,76 @@ func TestEntryPoints_Set(t *testing.T) { }, }, }, + { + name: "default", + expression: "Name:foo", + expectedEntryPointName: "foo", + expectedEntryPoint: &EntryPoint{ + WhitelistSourceRange: []string{}, + ForwardedHeaders: &ForwardedHeaders{Insecure: true}, + }, + }, + { + name: "ForwardedHeaders insecure true", + expression: "Name:foo ForwardedHeaders.Insecure:true", + expectedEntryPointName: "foo", + expectedEntryPoint: &EntryPoint{ + WhitelistSourceRange: []string{}, + ForwardedHeaders: &ForwardedHeaders{Insecure: true}, + }, + }, + { + name: "ForwardedHeaders insecure false", + expression: "Name:foo ForwardedHeaders.Insecure:false", + expectedEntryPointName: "foo", + expectedEntryPoint: &EntryPoint{ + WhitelistSourceRange: []string{}, + ForwardedHeaders: &ForwardedHeaders{Insecure: false}, + }, + }, + { + name: "ForwardedHeaders TrustedIPs", + expression: "Name:foo ForwardedHeaders.TrustedIPs:10.0.0.3/24,20.0.0.3/24", + expectedEntryPointName: "foo", + expectedEntryPoint: &EntryPoint{ + WhitelistSourceRange: []string{}, + ForwardedHeaders: &ForwardedHeaders{ + TrustedIPs: []string{"10.0.0.3/24", "20.0.0.3/24"}, + }, + }, + }, + { + name: "ProxyProtocol insecure true", + expression: "Name:foo ProxyProtocol.Insecure:true", + expectedEntryPointName: "foo", + expectedEntryPoint: &EntryPoint{ + WhitelistSourceRange: []string{}, + ForwardedHeaders: &ForwardedHeaders{Insecure: true}, + ProxyProtocol: &ProxyProtocol{Insecure: true}, + }, + }, + { + name: "ProxyProtocol insecure false", + expression: "Name:foo ProxyProtocol.Insecure:false", + expectedEntryPointName: "foo", + expectedEntryPoint: &EntryPoint{ + WhitelistSourceRange: []string{}, + ForwardedHeaders: &ForwardedHeaders{Insecure: true}, + ProxyProtocol: &ProxyProtocol{}, + }, + }, + { + name: "ProxyProtocol TrustedIPs", + expression: "Name:foo ProxyProtocol.TrustedIPs:10.0.0.3/24,20.0.0.3/24", + expectedEntryPointName: "foo", + expectedEntryPoint: &EntryPoint{ + WhitelistSourceRange: []string{}, + ForwardedHeaders: &ForwardedHeaders{Insecure: true}, + ProxyProtocol: &ProxyProtocol{ + TrustedIPs: []string{"10.0.0.3/24", "20.0.0.3/24"}, + }, + }, + }, { name: "compress on", expression: "Name:foo Compress:on", @@ -190,6 +262,7 @@ func TestEntryPoints_Set(t *testing.T) { expectedEntryPoint: &EntryPoint{ Compress: true, WhitelistSourceRange: []string{}, + ForwardedHeaders: &ForwardedHeaders{Insecure: true}, }, }, { @@ -199,6 +272,7 @@ func TestEntryPoints_Set(t *testing.T) { expectedEntryPoint: &EntryPoint{ Compress: true, WhitelistSourceRange: []string{}, + ForwardedHeaders: &ForwardedHeaders{Insecure: true}, }, }, } diff --git a/docs/configuration/entrypoints.md b/docs/configuration/entrypoints.md index c626352e7..7bb55c8f3 100644 --- a/docs/configuration/entrypoints.md +++ b/docs/configuration/entrypoints.md @@ -191,7 +191,7 @@ To enable IP whitelisting at the entrypoint level. ## ProxyProtocol To enable [ProxyProtocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt) support. -Only IPs in `trustedIPs` will lead to remote client address replacement: you should declare your load-balancer IP or CIDR range here (in testing environment, you can trust everyone using `0.0.0.0/0`). +Only IPs in `trustedIPs` will lead to remote client address replacement: you should declare your load-balancer IP or CIDR range here (in testing environment, you can trust everyone using `insecure = true`). !!! danger When queuing Træfik behind another load-balancer, be sure to carefully configure Proxy Protocol on both sides. @@ -200,7 +200,40 @@ Only IPs in `trustedIPs` will lead to remote client address replacement: you sho ```toml [entryPoints] [entryPoints.http] - address = ":80" - [entryPoints.http.proxyProtocol] - trustedIPs = ["127.0.0.1/32", "192.168.1.7"] + address = ":80" + + # Enable ProxyProtocol + [entryPoints.http.proxyProtocol] + # List of trusted IPs + # + # Required + # Default: [] + # + trustedIPs = ["127.0.0.1/32", "192.168.1.7"] + + # Insecure mode FOR TESTING ENVIRONNEMENT ONLY + # + # Optional + # Default: false + # + # insecure = true +``` + +## Forwarded Header + +Only IPs in `trustedIPs` will be authorize to trust the client forwarded headers (`X-Forwarded-*`). + +```toml +[entryPoints] + [entryPoints.http] + address = ":80" + + # Enable Forwarded Headers + [entryPoints.http.forwardedHeaders] + # List of trusted IPs + # + # Required + # Default: [] + # + trustedIPs = ["127.0.0.1/32", "192.168.1.7"] ``` diff --git a/middlewares/ip_whitelister.go b/middlewares/ip_whitelister.go index 490986c29..29eb76063 100644 --- a/middlewares/ip_whitelister.go +++ b/middlewares/ip_whitelister.go @@ -26,7 +26,7 @@ func NewIPWhitelister(whitelistStrings []string) (*IPWhiteLister, error) { whiteLister := IPWhiteLister{} - ip, err := whitelist.NewIP(whitelistStrings) + ip, err := whitelist.NewIP(whitelistStrings, false) if err != nil { return nil, fmt.Errorf("parsing CIDR whitelist %s: %v", whitelistStrings, err) } diff --git a/server/header_rewriter.go b/server/header_rewriter.go new file mode 100644 index 000000000..aebd9fcb7 --- /dev/null +++ b/server/header_rewriter.go @@ -0,0 +1,51 @@ +package server + +import ( + "net" + "net/http" + "os" + + "github.com/containous/traefik/whitelist" + "github.com/vulcand/oxy/forward" +) + +// NewHeaderRewriter Create a header rewriter +func NewHeaderRewriter(trustedIPs []string, insecure bool) (forward.ReqRewriter, error) { + IPs, err := whitelist.NewIP(trustedIPs, insecure) + if err != nil { + return nil, err + } + + h, err := os.Hostname() + if err != nil { + h = "localhost" + } + + return &headerRewriter{ + secureRewriter: &forward.HeaderRewriter{TrustForwardHeader: true, Hostname: h}, + insecureRewriter: &forward.HeaderRewriter{TrustForwardHeader: false, Hostname: h}, + ips: IPs, + insecure: insecure, + }, nil +} + +type headerRewriter struct { + secureRewriter forward.ReqRewriter + insecureRewriter forward.ReqRewriter + insecure bool + ips *whitelist.IP +} + +func (h *headerRewriter) Rewrite(req *http.Request) { + clientIP, _, err := net.SplitHostPort(req.RemoteAddr) + if err != nil { + h.secureRewriter.Rewrite(req) + } + + authorized, _, err := h.ips.Contains(clientIP) + if h.insecure || authorized { + h.secureRewriter.Rewrite(req) + } else { + h.insecureRewriter.Rewrite(req) + } +} diff --git a/server/server.go b/server/server.go index 3a5a76cb9..cfcd91411 100644 --- a/server/server.go +++ b/server/server.go @@ -655,7 +655,7 @@ func (server *Server) prepareServer(entryPointName string, entryPoint *configura } if entryPoint.ProxyProtocol != nil { - IPs, err := whitelist.NewIP(entryPoint.ProxyProtocol.TrustedIPs) + IPs, err := whitelist.NewIP(entryPoint.ProxyProtocol.TrustedIPs, entryPoint.ProxyProtocol.Insecure) if err != nil { return nil, nil, fmt.Errorf("Error creating whitelist: %s", err) } @@ -806,11 +806,19 @@ func (server *Server) loadConfig(configurations types.Configurations, globalConf continue frontend } + rewriter, err := NewHeaderRewriter(entryPoint.ForwardedHeaders.TrustedIPs, entryPoint.ForwardedHeaders.Insecure) + if err != nil { + log.Errorf("Error creating rewriter for frontend %s: %v", frontendName, err) + log.Errorf("Skipping frontend %s...", frontendName) + continue frontend + } + fwd, err := forward.New( forward.Logger(oxyLogger), forward.PassHostHeader(frontend.PassHostHeader), forward.RoundTripper(roundTripper), forward.ErrorHandler(errorHandler), + forward.Rewriter(rewriter), ) if err != nil { diff --git a/server/server_test.go b/server/server_test.go index a5ec9b091..f661a44a3 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -96,7 +96,10 @@ func TestPrepareServerTimeouts(t *testing.T) { t.Parallel() entryPointName := "http" - entryPoint := &configuration.EntryPoint{Address: "localhost:0"} + entryPoint := &configuration.EntryPoint{ + Address: "localhost:0", + ForwardedHeaders: &configuration.ForwardedHeaders{Insecure: true}, + } router := middlewares.NewHandlerSwitcher(mux.NewRouter()) srv := NewServer(test.globalConfig) @@ -210,7 +213,9 @@ func TestServerLoadConfigHealthCheckOptions(t *testing.T) { t.Run(fmt.Sprintf("%s/hc=%t", lbMethod, healthCheck != nil), func(t *testing.T) { globalConfig := configuration.GlobalConfiguration{ EntryPoints: configuration.EntryPoints{ - "http": &configuration.EntryPoint{}, + "http": &configuration.EntryPoint{ + ForwardedHeaders: &configuration.ForwardedHeaders{Insecure: true}, + }, }, HealthCheck: &configuration.HealthCheckConfig{Interval: flaeg.Duration(5 * time.Second)}, } @@ -383,7 +388,7 @@ func TestNewServerWithWhitelistSourceRange(t *testing.T) { func TestServerLoadConfigEmptyBasicAuth(t *testing.T) { globalConfig := configuration.GlobalConfiguration{ EntryPoints: configuration.EntryPoints{ - "http": &configuration.EntryPoint{}, + "http": &configuration.EntryPoint{ForwardedHeaders: &configuration.ForwardedHeaders{Insecure: true}}, }, } @@ -492,7 +497,7 @@ func TestConfigureBackends(t *testing.T) { } } -func TestServerEntrypointWhitelistConfig(t *testing.T) { +func TestServerEntryPointWhitelistConfig(t *testing.T) { tests := []struct { desc string entrypoint *configuration.EntryPoint @@ -501,7 +506,8 @@ func TestServerEntrypointWhitelistConfig(t *testing.T) { { desc: "no whitelist middleware if no config on entrypoint", entrypoint: &configuration.EntryPoint{ - Address: ":0", + Address: ":0", + ForwardedHeaders: &configuration.ForwardedHeaders{Insecure: true}, }, wantMiddleware: false, }, @@ -512,6 +518,7 @@ func TestServerEntrypointWhitelistConfig(t *testing.T) { WhitelistSourceRange: []string{ "127.0.0.1/32", }, + ForwardedHeaders: &configuration.ForwardedHeaders{Insecure: true}, }, wantMiddleware: true, }, @@ -633,7 +640,7 @@ func TestServerResponseEmptyBackend(t *testing.T) { globalConfig := configuration.GlobalConfiguration{ EntryPoints: configuration.EntryPoints{ - "http": &configuration.EntryPoint{}, + "http": &configuration.EntryPoint{ForwardedHeaders: &configuration.ForwardedHeaders{Insecure: true}}, }, } dynamicConfigs := types.Configurations{"config": test.dynamicConfig(testServer.URL)} diff --git a/whitelist/ip.go b/whitelist/ip.go index af3b691e7..322404fab 100644 --- a/whitelist/ip.go +++ b/whitelist/ip.go @@ -11,26 +11,29 @@ import ( type IP struct { whiteListsIPs []*net.IP whiteListsNet []*net.IPNet + insecure bool } // NewIP builds a new IP given a list of CIDR-Strings to whitelist -func NewIP(whitelistStrings []string) (*IP, error) { - if len(whitelistStrings) == 0 { +func NewIP(whitelistStrings []string, insecure bool) (*IP, error) { + if len(whitelistStrings) == 0 && !insecure { return nil, errors.New("no whiteListsNet provided") } ip := IP{} - for _, whitelistString := range whitelistStrings { - ipAddr := net.ParseIP(whitelistString) - if ipAddr != nil { - ip.whiteListsIPs = append(ip.whiteListsIPs, &ipAddr) - } else { - _, whitelist, err := net.ParseCIDR(whitelistString) - if err != nil { - return nil, fmt.Errorf("parsing CIDR whitelist %s: %v", whitelist, err) + if !insecure { + for _, whitelistString := range whitelistStrings { + ipAddr := net.ParseIP(whitelistString) + if ipAddr != nil { + ip.whiteListsIPs = append(ip.whiteListsIPs, &ipAddr) + } else { + _, whitelist, err := net.ParseCIDR(whitelistString) + if err != nil { + return nil, fmt.Errorf("parsing CIDR whitelist %s: %v", whitelist, err) + } + ip.whiteListsNet = append(ip.whiteListsNet, whitelist) } - ip.whiteListsNet = append(ip.whiteListsNet, whitelist) } } @@ -39,6 +42,10 @@ func NewIP(whitelistStrings []string) (*IP, error) { // Contains checks if provided address is in the white list func (ip *IP) Contains(addr string) (bool, net.IP, error) { + if ip.insecure { + return true, nil, nil + } + ipAddr, err := ipFromRemoteAddr(addr) if err != nil { return false, nil, fmt.Errorf("unable to parse address: %s: %s", addr, err) @@ -50,6 +57,10 @@ func (ip *IP) Contains(addr string) (bool, net.IP, error) { // ContainsIP checks if provided address is in the white list func (ip *IP) ContainsIP(addr net.IP) (bool, error) { + if ip.insecure { + return true, nil + } + for _, whiteListIP := range ip.whiteListsIPs { if whiteListIP.Equal(addr) { return true, nil diff --git a/whitelist/ip_test.go b/whitelist/ip_test.go index 46ab7142f..abd65f297 100644 --- a/whitelist/ip_test.go +++ b/whitelist/ip_test.go @@ -75,7 +75,7 @@ func TestNew(t *testing.T) { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() - whitelister, err := NewIP(test.whitelistStrings) + whitelister, err := NewIP(test.whitelistStrings, false) if test.errMessage != "" { require.EqualError(t, err, test.errMessage) } else { @@ -275,7 +275,7 @@ func TestIsAllowed(t *testing.T) { test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() - whiteLister, err := NewIP(test.whitelistStrings) + whiteLister, err := NewIP(test.whitelistStrings, false) require.NoError(t, err) require.NotNil(t, whiteLister) @@ -306,7 +306,7 @@ func TestBrokenIPs(t *testing.T) { "\\&$§&/(", } - whiteLister, err := NewIP([]string{"1.2.3.4/24"}) + whiteLister, err := NewIP([]string{"1.2.3.4/24"}, false) require.NoError(t, err) for _, testIP := range brokenIPs {