Add TrustForwardHeader options.

This commit is contained in:
Ludovic Fernandez 2017-10-16 12:46:03 +02:00 committed by Traefiker
parent 9598f646f5
commit aa308b7a3a
10 changed files with 265 additions and 40 deletions

View file

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

View file

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

View file

@ -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},
},
},
}

View file

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

View file

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

51
server/header_rewriter.go Normal file
View file

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

View file

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

View file

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

View file

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

View file

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