Ability to use "X-Forwarded-For" as a source of IP for white list.

This commit is contained in:
Ludovic Fernandez 2018-03-23 17:40:04 +01:00 committed by Traefiker Bot
parent 4802484729
commit d2766b1b4f
50 changed files with 1496 additions and 599 deletions

View file

@ -123,17 +123,19 @@ var _templatesConsul_catalogTmpl = []byte(`[backends]
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{ $whitelistSourceRange := getWhitelistSourceRange $service.Attributes }}
{{if $whitelistSourceRange }}
whitelistSourceRange = [{{range $whitelistSourceRange }}
"{{.}}",
{{end}}]
{{end}}
basicAuth = [{{range getBasicAuth $service.Attributes }} basicAuth = [{{range getBasicAuth $service.Attributes }}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{ $whitelist := getWhiteList $service.Attributes }}
{{if $whitelist }}
[frontends."frontend-{{ $service.ServiceName }}".whiteList]
sourceRange = [{{range $whitelist.SourceRange }}
"{{.}}",
{{end}}]
useXForwardedFor = {{ $whitelist.UseXForwardedFor }}
{{end}}
{{ $redirect := getRedirect $service.Attributes }} {{ $redirect := getRedirect $service.Attributes }}
{{if $redirect }} {{if $redirect }}
[frontends."frontend-{{ $service.ServiceName }}".redirect] [frontends."frontend-{{ $service.ServiceName }}".redirect]
@ -523,17 +525,19 @@ var _templatesDockerTmpl = []byte(`{{$backendServers := .Servers}}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{ $whitelistSourceRange := getWhitelistSourceRange $container.SegmentLabels }}
{{if $whitelistSourceRange }}
whitelistSourceRange = [{{range $whitelistSourceRange }}
"{{.}}",
{{end}}]
{{end}}
basicAuth = [{{range getBasicAuth $container.SegmentLabels }} basicAuth = [{{range getBasicAuth $container.SegmentLabels }}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{ $whitelist := getWhiteList $container.SegmentLabels }}
{{if $whitelist }}
[frontends."frontend-{{ $frontendName }}".whiteList]
sourceRange = [{{range $whitelist.SourceRange }}
"{{.}}",
{{end}}]
useXForwardedFor = {{ $whitelist.UseXForwardedFor }}
{{end}}
{{ $redirect := getRedirect $container.SegmentLabels }} {{ $redirect := getRedirect $container.SegmentLabels }}
{{if $redirect }} {{if $redirect }}
[frontends."frontend-{{ $frontendName }}".redirect] [frontends."frontend-{{ $frontendName }}".redirect]
@ -713,17 +717,18 @@ var _templatesEcsTmpl = []byte(`[backends]
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{ $whitelistSourceRange := getWhitelistSourceRange $instance }}
{{if $whitelistSourceRange }}
whitelistSourceRange = [{{range $whitelistSourceRange }}
"{{.}}",
{{end}}]
{{end}}
basicAuth = [{{range getBasicAuth $instance }} basicAuth = [{{range getBasicAuth $instance }}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{ $whitelist := getWhiteList $instance }}
{{if $whitelist }}
[frontends."frontend-{{ $serviceName }}".whiteList]
sourceRange = [{{range $whitelist.SourceRange }}
"{{.}}",
{{end}}]
useXForwardedFor = {{ $whitelist.UseXForwardedFor }}
{{end}}
{{ $redirect := getRedirect $instance }} {{ $redirect := getRedirect $instance }}
{{if $redirect }} {{if $redirect }}
@ -934,9 +939,13 @@ var _templatesKubernetesTmpl = []byte(`[backends]
"{{.}}", "{{.}}",
{{end}}] {{end}}]
whitelistSourceRange = [{{range $frontend.WhitelistSourceRange }} {{if $frontend.WhiteList }}
[frontends."{{ $frontendName }}".whiteList]
sourceRange = [{{range $frontend.WhiteList.SourceRange }}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
useXForwardedFor = {{ $frontend.WhiteList.UseXForwardedFor }}
{{end}}
{{if $frontend.Redirect }} {{if $frontend.Redirect }}
[frontends."{{ $frontendName }}".redirect] [frontends."{{ $frontendName }}".redirect]
@ -1118,17 +1127,19 @@ var _templatesKvTmpl = []byte(`[backends]
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{ $whitelistSourceRange := getWhitelistSourceRange $frontend }}
{{if $whitelistSourceRange }}
whitelistSourceRange = [{{range $whitelistSourceRange }}
"{{.}}",
{{end}}]
{{end}}
basicAuth = [{{range getBasicAuth $frontend }} basicAuth = [{{range getBasicAuth $frontend }}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{ $whitelist := getWhiteList $frontend }}
{{if $whitelist }}
[frontends."{{ $frontendName }}".whiteList]
sourceRange = [{{range $whitelist.SourceRange }}
"{{.}}",
{{end}}]
useXForwardedFor = {{ $whitelist.UseXForwardedFor }}
{{end}}
{{ $redirect := getRedirect $frontend }} {{ $redirect := getRedirect $frontend }}
{{if $redirect }} {{if $redirect }}
[frontends."{{ $frontendName }}".redirect] [frontends."{{ $frontendName }}".redirect]
@ -1329,17 +1340,19 @@ var _templatesMarathonTmpl = []byte(`{{ $apps := .Applications }}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{ $whitelistSourceRange := getWhitelistSourceRange $app $serviceName }}
{{if $whitelistSourceRange }}
whitelistSourceRange = [{{range $whitelistSourceRange }}
"{{.}}",
{{end}}]
{{end}}
basicAuth = [{{range getBasicAuth $app $serviceName }} basicAuth = [{{range getBasicAuth $app $serviceName }}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{ $whitelist := getWhiteList $app $serviceName }}
{{if $whitelist }}
[frontends."{{ $frontendName }}".whiteList]
sourceRange = [{{range $whitelist.SourceRange }}
"{{.}}",
{{end}}]
useXForwardedFor = {{ $whitelist.UseXForwardedFor }}
{{end}}
{{ $redirect := getRedirect $app $serviceName }} {{ $redirect := getRedirect $app $serviceName }}
{{if $redirect }} {{if $redirect }}
[frontends."{{ $frontendName }}".redirect] [frontends."{{ $frontendName }}".redirect]
@ -1522,17 +1535,19 @@ var _templatesMesosTmpl = []byte(`[backends]
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{ $whitelistSourceRange := getWhitelistSourceRange $app }}
{{if $whitelistSourceRange }}
whitelistSourceRange = [{{range $whitelistSourceRange }}
"{{.}}",
{{end}}]
{{end}}
basicAuth = [{{range getBasicAuth $app }} basicAuth = [{{range getBasicAuth $app }}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{ $whitelist := getWhiteList $app }}
{{if $whitelist }}
[frontends."frontend-{{ $frontendName }}".whiteList]
sourceRange = [{{range $whitelist.SourceRange }}
"{{.}}",
{{end}}]
useXForwardedFor = {{ $whitelist.UseXForwardedFor }}
{{end}}
{{ $redirect := getRedirect $app }} {{ $redirect := getRedirect $app }}
{{if $redirect }} {{if $redirect }}
[frontends."frontend-{{ $frontendName }}".redirect] [frontends."frontend-{{ $frontendName }}".redirect]
@ -1736,17 +1751,19 @@ var _templatesRancherTmpl = []byte(`{{ $backendServers := .Backends }}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{ $whitelistSourceRange := getWhitelistSourceRange $service }}
{{if $whitelistSourceRange }}
whitelistSourceRange = [{{range $whitelistSourceRange }}
"{{.}}",
{{end}}]
{{end}}
basicAuth = [{{range getBasicAuth $service }} basicAuth = [{{range getBasicAuth $service }}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{ $whitelist := getWhiteList $service }}
{{if $whitelist }}
[frontends."frontend-{{ $frontendName }}".whiteList]
sourceRange = [{{range $whitelist.SourceRange }}
"{{.}}",
{{end}}]
useXForwardedFor = {{ $whitelist.UseXForwardedFor }}
{{end}}
{{ $redirect := getRedirect $service }} {{ $redirect := getRedirect $service }}
{{if $redirect }} {{if $redirect }}
[frontends."frontend-{{ $frontendName }}".redirect] [frontends."frontend-{{ $frontendName }}".redirect]

View file

@ -182,12 +182,23 @@ func (gc *GlobalConfiguration) SetEffectiveConfiguration(configFile string) {
} }
} }
// ForwardedHeaders must be remove in the next breaking version
for entryPointName := range gc.EntryPoints { for entryPointName := range gc.EntryPoints {
entryPoint := gc.EntryPoints[entryPointName] entryPoint := gc.EntryPoints[entryPointName]
// ForwardedHeaders must be remove in the next breaking version
if entryPoint.ForwardedHeaders == nil { if entryPoint.ForwardedHeaders == nil {
entryPoint.ForwardedHeaders = &ForwardedHeaders{Insecure: true} entryPoint.ForwardedHeaders = &ForwardedHeaders{Insecure: true}
} }
if len(entryPoint.WhitelistSourceRange) > 0 {
log.Warnf("Deprecated configuration found: %s. Please use %s.", "whiteListSourceRange", "whiteList.sourceRange")
if entryPoint.WhiteList == nil {
entryPoint.WhiteList = &types.WhiteList{
SourceRange: entryPoint.WhitelistSourceRange,
}
entryPoint.WhitelistSourceRange = nil
}
}
} }
// Make sure LifeCycle isn't nil to spare nil checks elsewhere. // Make sure LifeCycle isn't nil to spare nil checks elsewhere.
@ -378,18 +389,6 @@ type ForwardingTimeouts struct {
ResponseHeaderTimeout flaeg.Duration `description:"The amount of time to wait for a server's response headers after fully writing the request (including its body, if any). If zero, no timeout exists" export:"true"` ResponseHeaderTimeout flaeg.Duration `description:"The amount of time to wait for a server's response headers after fully writing the request (including its body, if any). If zero, no timeout exists" export:"true"`
} }
// ProxyProtocol contains Proxy-Protocol configuration
type ProxyProtocol struct {
Insecure bool
TrustedIPs []string
}
// ForwardedHeaders Trust client forwarding headers
type ForwardedHeaders struct {
Insecure bool
TrustedIPs []string
}
// LifeCycle contains configurations relevant to the lifecycle (such as the // LifeCycle contains configurations relevant to the lifecycle (such as the
// shutdown phase) of Traefik. // shutdown phase) of Traefik.
type LifeCycle struct { type LifeCycle struct {

View file

@ -15,12 +15,25 @@ type EntryPoint struct {
TLS *tls.TLS `export:"true"` TLS *tls.TLS `export:"true"`
Redirect *types.Redirect `export:"true"` Redirect *types.Redirect `export:"true"`
Auth *types.Auth `export:"true"` Auth *types.Auth `export:"true"`
WhitelistSourceRange []string WhitelistSourceRange []string // Deprecated
WhiteList *types.WhiteList `export:"true"`
Compress bool `export:"true"` Compress bool `export:"true"`
ProxyProtocol *ProxyProtocol `export:"true"` ProxyProtocol *ProxyProtocol `export:"true"`
ForwardedHeaders *ForwardedHeaders `export:"true"` ForwardedHeaders *ForwardedHeaders `export:"true"`
} }
// ProxyProtocol contains Proxy-Protocol configuration
type ProxyProtocol struct {
Insecure bool `export:"true"`
TrustedIPs []string
}
// ForwardedHeaders Trust client forwarding headers
type ForwardedHeaders struct {
Insecure bool `export:"true"`
TrustedIPs []string
}
// EntryPoints holds entry points configuration of the reverse proxy (ip, port, TLS...) // EntryPoints holds entry points configuration of the reverse proxy (ip, port, TLS...)
type EntryPoints map[string]*EntryPoint type EntryPoints map[string]*EntryPoint
@ -70,6 +83,7 @@ func (ep *EntryPoints) Set(value string) error {
Redirect: makeEntryPointRedirect(result), Redirect: makeEntryPointRedirect(result),
Compress: compress, Compress: compress,
WhitelistSourceRange: whiteListSourceRange, WhitelistSourceRange: whiteListSourceRange,
WhiteList: makeWhiteList(result),
ProxyProtocol: makeEntryPointProxyProtocol(result), ProxyProtocol: makeEntryPointProxyProtocol(result),
ForwardedHeaders: makeEntryPointForwardedHeaders(result), ForwardedHeaders: makeEntryPointForwardedHeaders(result),
} }
@ -77,6 +91,17 @@ func (ep *EntryPoints) Set(value string) error {
return nil return nil
} }
func makeWhiteList(result map[string]string) *types.WhiteList {
var wl *types.WhiteList
if rawRange, ok := result["whitelist_sourcerange"]; ok {
wl = &types.WhiteList{
SourceRange: strings.Split(rawRange, ","),
UseXForwardedFor: toBool(result, "whitelist_usexforwardedfor"),
}
}
return wl
}
func makeEntryPointAuth(result map[string]string) *types.Auth { func makeEntryPointAuth(result map[string]string) *types.Auth {
var basic *types.Basic var basic *types.Basic
if v, ok := result["auth_basic_users"]; ok { if v, ok := result["auth_basic_users"]; ok {

View file

@ -28,7 +28,6 @@ func Test_parseEntryPointsConfiguration(t *testing.T) {
"Redirect.Replacement:http://mydomain/$1 " + "Redirect.Replacement:http://mydomain/$1 " +
"Redirect.Permanent:true " + "Redirect.Permanent:true " +
"Compress:true " + "Compress:true " +
"WhiteListSourceRange:10.42.0.0/16,152.89.1.33/32,afed:be44::/16 " +
"ProxyProtocol.TrustedIPs:192.168.0.1 " + "ProxyProtocol.TrustedIPs:192.168.0.1 " +
"ForwardedHeaders.TrustedIPs:10.0.0.3/24,20.0.0.3/24 " + "ForwardedHeaders.TrustedIPs:10.0.0.3/24,20.0.0.3/24 " +
"Auth.Basic.Users:test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0 " + "Auth.Basic.Users:test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0 " +
@ -40,7 +39,10 @@ func Test_parseEntryPointsConfiguration(t *testing.T) {
"Auth.Forward.TLS.CAOptional:true " + "Auth.Forward.TLS.CAOptional:true " +
"Auth.Forward.TLS.Cert:path/to/foo.cert " + "Auth.Forward.TLS.Cert:path/to/foo.cert " +
"Auth.Forward.TLS.Key:path/to/foo.key " + "Auth.Forward.TLS.Key:path/to/foo.key " +
"Auth.Forward.TLS.InsecureSkipVerify:true ", "Auth.Forward.TLS.InsecureSkipVerify:true " +
"WhiteListSourceRange:10.42.0.0/16,152.89.1.33/32,afed:be44::/16 " +
"whiteList.sourceRange:10.42.0.0/16,152.89.1.33/32,afed:be44::/16 " +
"whiteList.useXForwardedFor:true ",
expectedResult: map[string]string{ expectedResult: map[string]string{
"address": ":8000", "address": ":8000",
"auth_basic_users": "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", "auth_basic_users": "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
@ -66,6 +68,8 @@ func Test_parseEntryPointsConfiguration(t *testing.T) {
"tls": "goo,gii", "tls": "goo,gii",
"tls_acme": "TLS", "tls_acme": "TLS",
"whitelistsourcerange": "10.42.0.0/16,152.89.1.33/32,afed:be44::/16", "whitelistsourcerange": "10.42.0.0/16,152.89.1.33/32,afed:be44::/16",
"whitelist_sourcerange": "10.42.0.0/16,152.89.1.33/32,afed:be44::/16",
"whitelist_usexforwardedfor": "true",
}, },
}, },
{ {
@ -175,7 +179,6 @@ func TestEntryPoints_Set(t *testing.T) {
"Redirect.Replacement:http://mydomain/$1 " + "Redirect.Replacement:http://mydomain/$1 " +
"Redirect.Permanent:true " + "Redirect.Permanent:true " +
"Compress:true " + "Compress:true " +
"WhiteListSourceRange:10.42.0.0/16,152.89.1.33/32,afed:be44::/16 " +
"ProxyProtocol.TrustedIPs:192.168.0.1 " + "ProxyProtocol.TrustedIPs:192.168.0.1 " +
"ForwardedHeaders.TrustedIPs:10.0.0.3/24,20.0.0.3/24 " + "ForwardedHeaders.TrustedIPs:10.0.0.3/24,20.0.0.3/24 " +
"Auth.Basic.Users:test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0 " + "Auth.Basic.Users:test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0 " +
@ -187,7 +190,10 @@ func TestEntryPoints_Set(t *testing.T) {
"Auth.Forward.TLS.CAOptional:true " + "Auth.Forward.TLS.CAOptional:true " +
"Auth.Forward.TLS.Cert:path/to/foo.cert " + "Auth.Forward.TLS.Cert:path/to/foo.cert " +
"Auth.Forward.TLS.Key:path/to/foo.key " + "Auth.Forward.TLS.Key:path/to/foo.key " +
"Auth.Forward.TLS.InsecureSkipVerify:true ", "Auth.Forward.TLS.InsecureSkipVerify:true " +
"WhiteListSourceRange:10.42.0.0/16,152.89.1.33/32,afed:be44::/16 " +
"whiteList.sourceRange:10.42.0.0/16,152.89.1.33/32,afed:be44::/16 " +
"whiteList.useXForwardedFor:true ",
expectedEntryPointName: "foo", expectedEntryPointName: "foo",
expectedEntryPoint: &EntryPoint{ expectedEntryPoint: &EntryPoint{
Address: ":8000", Address: ":8000",
@ -240,6 +246,14 @@ func TestEntryPoints_Set(t *testing.T) {
"152.89.1.33/32", "152.89.1.33/32",
"afed:be44::/16", "afed:be44::/16",
}, },
WhiteList: &types.WhiteList{
SourceRange: []string{
"10.42.0.0/16",
"152.89.1.33/32",
"afed:be44::/16",
},
UseXForwardedFor: true,
},
Compress: true, Compress: true,
ProxyProtocol: &ProxyProtocol{ ProxyProtocol: &ProxyProtocol{
Insecure: false, Insecure: false,

View file

@ -109,7 +109,8 @@ Additional settings can be defined using Consul Catalog tags.
| `<prefix>.frontend.redirect.replacement=http://mydomain/$1` | Redirect to another URL for that frontend.<br>Must be set with `traefik.frontend.redirect.regex`. | | `<prefix>.frontend.redirect.replacement=http://mydomain/$1` | Redirect to another URL for that frontend.<br>Must be set with `traefik.frontend.redirect.regex`. |
| `<prefix>.frontend.redirect.permanent=true` | Return 301 instead of 302. | | `<prefix>.frontend.redirect.permanent=true` | Return 301 instead of 302. |
| `<prefix>.frontend.rule=EXPR` | Override the default frontend rule. Default: `Host:{{.ServiceName}}.{{.Domain}}`. | | `<prefix>.frontend.rule=EXPR` | Override the default frontend rule. Default: `Host:{{.ServiceName}}.{{.Domain}}`. |
| `<prefix>.frontend.whitelistSourceRange=RANGE` | List of IP-Ranges which are allowed to access.<br>An unset or empty list allows all Source-IPs to access. If one of the Net-Specifications are invalid, the whole list is invalid and allows all Source-IPs to access. | | `<prefix>.frontend.whiteList.sourceRange=RANGE` | List of IP-Ranges which are allowed to access.<br>An unset or empty list allows all Source-IPs to access. If one of the Net-Specifications are invalid, the whole list is invalid and allows all Source-IPs to access. |
| `<prefix>.frontend.whiteList.useXForwardedFor=true` | Use `X-Forwarded-For` header as valid source of IP for the white list. |
### Custom Headers ### Custom Headers

View file

@ -194,7 +194,7 @@ services:
Labels can be used on containers to override default behavior. Labels can be used on containers to override default behavior.
| Label | Description | | Label | Description |
|------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| |------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `traefik.docker.network` | Set the docker network to use for connections to this container. [1] | | `traefik.docker.network` | Set the docker network to use for connections to this container. [1] |
| `traefik.enable=false` | Disable this container in Træfik | | `traefik.enable=false` | Disable this container in Træfik |
| `traefik.port=80` | Register this port. Useful when the container exposes multiples ports. | | `traefik.port=80` | Register this port. Useful when the container exposes multiples ports. |
@ -234,7 +234,8 @@ Labels can be used on containers to override default behavior.
| `traefik.frontend.redirect.replacement=http://mydomain/$1` | Redirect to another URL for that frontend.<br>Must be set with `traefik.frontend.redirect.regex`. | | `traefik.frontend.redirect.replacement=http://mydomain/$1` | Redirect to another URL for that frontend.<br>Must be set with `traefik.frontend.redirect.regex`. |
| `traefik.frontend.redirect.permanent=true` | Return 301 instead of 302. | | `traefik.frontend.redirect.permanent=true` | Return 301 instead of 302. |
| `traefik.frontend.rule=EXPR` | Override the default frontend rule. Default: `Host:{containerName}.{domain}` or `Host:{service}.{project_name}.{domain}` if you are using `docker-compose`. | | `traefik.frontend.rule=EXPR` | Override the default frontend rule. Default: `Host:{containerName}.{domain}` or `Host:{service}.{project_name}.{domain}` if you are using `docker-compose`. |
| `traefik.frontend.whitelistSourceRange=RANGE` | List of IP-Ranges which are allowed to access.<br>An unset or empty list allows all Source-IPs to access. If one of the Net-Specifications are invalid, the whole list is invalid and allows all Source-IPs to access. | | `traefik.frontend.whiteList.sourceRange=RANGE` | List of IP-Ranges which are allowed to access.<br>An unset or empty list allows all Source-IPs to access.<br>If one of the Net-Specifications are invalid, the whole list is invalid and allows all Source-IPs to access. |
| `traefik.frontend.whiteList.useXForwardedFor=true` | Use `X-Forwarded-For` header as valid source of IP for the white list. |
[1] `traefik.docker.network`: [1] `traefik.docker.network`:
If a container is linked to several networks, be sure to set the proper network name (you can check with `docker inspect <container_id>`) otherwise it will randomly pick one (depending on how docker is returning them). If a container is linked to several networks, be sure to set the proper network name (you can check with `docker inspect <container_id>`) otherwise it will randomly pick one (depending on how docker is returning them).
@ -303,7 +304,8 @@ Segment labels override the default behavior.
| `traefik.<segment_name>.frontend.redirect.replacement=http://mydomain/$1` | Overrides `traefik.frontend.redirect.replacement`. | | `traefik.<segment_name>.frontend.redirect.replacement=http://mydomain/$1` | Overrides `traefik.frontend.redirect.replacement`. |
| `traefik.<segment_name>.frontend.redirect.permanent=true` | Return 301 instead of 302. | | `traefik.<segment_name>.frontend.redirect.permanent=true` | Return 301 instead of 302. |
| `traefik.<segment_name>.frontend.rule` | Overrides `traefik.frontend.rule`. | | `traefik.<segment_name>.frontend.rule` | Overrides `traefik.frontend.rule`. |
| `traefik.<segment_name>.frontend.whitelistSourceRange=RANGE` | Overrides `traefik.frontend.whitelistSourceRange`. | | `traefik.<segment_name>.frontend.whiteList.sourceRange=RANGE` | Overrides `traefik.frontend.whiteList.sourceRange`. |
| `traefik.<segment_name>.frontend.whiteList.useXForwardedFor=true` | Overrides `traefik.frontend.whiteList.useXForwardedFor`. |
#### Custom Headers #### Custom Headers

View file

@ -163,7 +163,8 @@ Labels can be used on task containers to override default behaviour:
| `traefik.frontend.redirect.replacement=http://mydomain/$1` | Redirect to another URL for that frontend.<br>Must be set with `traefik.frontend.redirect.regex`. | | `traefik.frontend.redirect.replacement=http://mydomain/$1` | Redirect to another URL for that frontend.<br>Must be set with `traefik.frontend.redirect.regex`. |
| `traefik.frontend.redirect.permanent=true` | Return 301 instead of 302. | | `traefik.frontend.redirect.permanent=true` | Return 301 instead of 302. |
| `traefik.frontend.rule=EXPR` | Override the default frontend rule. Default: `Host:{instance_name}.{domain}`. | | `traefik.frontend.rule=EXPR` | Override the default frontend rule. Default: `Host:{instance_name}.{domain}`. |
| `traefik.frontend.whitelistSourceRange=RANGE` | List of IP-Ranges which are allowed to access.<br>An unset or empty list allows all Source-IPs to access. If one of the Net-Specifications are invalid, the whole list is invalid and allows all Source-IPs to access. | | `traefik.frontend.whiteList.sourceRange=RANGE` | List of IP-Ranges which are allowed to access.<br>An unset or empty list allows all Source-IPs to access. If one of the Net-Specifications are invalid, the whole list is invalid and allows all Source-IPs to access. |
| `traefik.frontend.whiteList.useXForwardedFor=true` | Use `X-Forwarded-For` header as valid source of IP for the white list. |
### Custom Headers ### Custom Headers

View file

@ -54,7 +54,10 @@ Træfik can be configured with a file.
"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
] ]
whitelistSourceRange = ["10.42.0.0/16", "152.89.1.33/32", "afed:be44::/16"]
[frontends.frontend1.whiteList]
sourceRange = ["10.42.0.0/16", "152.89.1.33/32", "afed:be44::/16"]
useXForwardedFor = true
[frontends.frontend1.routes] [frontends.frontend1.routes]
[frontends.frontend1.routes.route0] [frontends.frontend1.routes.route0]

View file

@ -200,7 +200,8 @@ The following labels can be defined on Marathon applications. They adjust the be
| `traefik.frontend.redirect.replacement=http://mydomain/$1` | Redirect to another URL for that frontend.<br>Must be set with `traefik.frontend.redirect.regex`. | | `traefik.frontend.redirect.replacement=http://mydomain/$1` | Redirect to another URL for that frontend.<br>Must be set with `traefik.frontend.redirect.regex`. |
| `traefik.frontend.redirect.permanent=true` | Return 301 instead of 302. | | `traefik.frontend.redirect.permanent=true` | Return 301 instead of 302. |
| `traefik.frontend.rule=EXPR` | Override the default frontend rule. Default: `Host:{sub_domain}.{domain}`. | | `traefik.frontend.rule=EXPR` | Override the default frontend rule. Default: `Host:{sub_domain}.{domain}`. |
| `traefik.frontend.whitelistSourceRange=RANGE` | List of IP-Ranges which are allowed to access.<br>An unset or empty list allows all Source-IPs to access. If one of the Net-Specifications are invalid, the whole list is invalid and allows all Source-IPs to access. | | `traefik.frontend.whiteList.sourceRange=RANGE` | List of IP-Ranges which are allowed to access.<br>An unset or empty list allows all Source-IPs to access. If one of the Net-Specifications are invalid, the whole list is invalid and allows all Source-IPs to access. |
| `traefik.frontend.whiteList.useXForwardedFor=true` | Use `X-Forwarded-For` header as valid source of IP for the white list. |
#### Custom Headers #### Custom Headers
@ -265,6 +266,8 @@ You can define as many segments as ports exposed in an application.
| `traefik.<segment_name>.frontend.redirect.permanent=true` | Return 301 instead of 302. | | `traefik.<segment_name>.frontend.redirect.permanent=true` | Return 301 instead of 302. |
| `traefik.<segment_name>.frontend.rule=EXP` | Overrides `traefik.frontend.rule`. Default: `{service_name}.{sub_domain}.{domain}` | | `traefik.<segment_name>.frontend.rule=EXP` | Overrides `traefik.frontend.rule`. Default: `{service_name}.{sub_domain}.{domain}` |
| `traefik.<segment_name>.frontend.whitelistSourceRange=RANGE` | Overrides `traefik.frontend.whitelistSourceRange`. | | `traefik.<segment_name>.frontend.whitelistSourceRange=RANGE` | Overrides `traefik.frontend.whitelistSourceRange`. |
| `traefik.<segment_name>.frontend.whiteList.sourceRange=RANGE` | Overrides `traefik.frontend.whiteList.sourceRange`. |
| `traefik.<segment_name>.frontend.whiteList.useXForwardedFor=true` | Use `X-Forwarded-For` header as valid source of IP for the white list. |
#### Custom Headers #### Custom Headers

View file

@ -135,7 +135,8 @@ The following labels can be defined on Mesos tasks. They adjust the behaviour fo
| `traefik.frontend.redirect.replacement=http://mydomain/$1` | Redirect to another URL for that frontend.<br>Must be set with `traefik.frontend.redirect.regex`. | | `traefik.frontend.redirect.replacement=http://mydomain/$1` | Redirect to another URL for that frontend.<br>Must be set with `traefik.frontend.redirect.regex`. |
| `traefik.frontend.redirect.permanent=true` | Return 301 instead of 302. | | `traefik.frontend.redirect.permanent=true` | Return 301 instead of 302. |
| `traefik.frontend.rule=EXPR` | Override the default frontend rule. Default: `Host:{discovery_name}.{domain}`. | | `traefik.frontend.rule=EXPR` | Override the default frontend rule. Default: `Host:{discovery_name}.{domain}`. |
| `traefik.frontend.whitelistSourceRange=RANGE` | List of IP-Ranges which are allowed to access.<br>An unset or empty list allows all Source-IPs to access. If one of the Net-Specifications are invalid, the whole list is invalid and allows all Source-IPs to access. | | `traefik.frontend.whiteList.sourceRange=RANGE` | List of IP-Ranges which are allowed to access.<br>An unset or empty list allows all Source-IPs to access. If one of the Net-Specifications are invalid, the whole list is invalid and allows all Source-IPs to access. |
| `traefik.frontend.whiteList.useXForwardedFor=true` | Use `X-Forwarded-For` header as valid source of IP for the white list. |
### Custom Headers ### Custom Headers

View file

@ -159,7 +159,8 @@ Labels can be used on task containers to override default behaviour:
| `traefik.frontend.redirect.replacement=http://mydomain/$1` | Redirect to another URL for that frontend.<br>Must be set with `traefik.frontend.redirect.regex`. | | `traefik.frontend.redirect.replacement=http://mydomain/$1` | Redirect to another URL for that frontend.<br>Must be set with `traefik.frontend.redirect.regex`. |
| `traefik.frontend.redirect.permanent=true` | Return 301 instead of 302. | | `traefik.frontend.redirect.permanent=true` | Return 301 instead of 302. |
| `traefik.frontend.rule=EXPR` | Override the default frontend rule. Default: `Host:{service_name}.{stack_name}.{domain}`. | | `traefik.frontend.rule=EXPR` | Override the default frontend rule. Default: `Host:{service_name}.{stack_name}.{domain}`. |
| `traefik.frontend.whitelistSourceRange=RANGE` | List of IP-Ranges which are allowed to access.<br>An unset or empty list allows all Source-IPs to access.<br>If one of the Net-Specifications are invalid, the whole list is invalid and allows all Source-IPs to access. | | `traefik.frontend.whiteList.sourceRange=RANGE` | List of IP-Ranges which are allowed to access.<br>An unset or empty list allows all Source-IPs to access.<br>If one of the Net-Specifications are invalid, the whole list is invalid and allows all Source-IPs to access. |
| `traefik.frontend.whiteList.useXForwardedFor=true` | Use `X-Forwarded-For` header as valid source of IP for the white list. |
### Custom Headers ### Custom Headers

View file

@ -8,9 +8,12 @@
[entryPoints] [entryPoints]
[entryPoints.http] [entryPoints.http]
address = ":80" address = ":80"
whitelistSourceRange = ["10.42.0.0/16", "152.89.1.33/32", "afed:be44::/16"]
compress = true compress = true
[entryPoints.http.whitelist]
sourceRange = ["10.42.0.0/16", "152.89.1.33/32", "afed:be44::/16"]
useXForwardedFor = true
[entryPoints.http.tls] [entryPoints.http.tls]
minVersion = "VersionTLS12" minVersion = "VersionTLS12"
cipherSuites = [ cipherSuites = [
@ -112,7 +115,8 @@ Redirect.Regex:http://localhost/(.*)
Redirect.Replacement:http://mydomain/$1 Redirect.Replacement:http://mydomain/$1
Redirect.Permanent:true Redirect.Permanent:true
Compress:true Compress:true
WhiteListSourceRange:10.42.0.0/16,152.89.1.33/32,afed:be44::/16 WhiteList.SourceRange:10.42.0.0/16,152.89.1.33/32,afed:be44::/16
WhiteList.UseXForwardedFor:true
ProxyProtocol.TrustedIPs:192.168.0.1 ProxyProtocol.TrustedIPs:192.168.0.1
ProxyProtocol.Insecure:tue ProxyProtocol.Insecure:tue
ForwardedHeaders.TrustedIPs:10.0.0.3/24,20.0.0.3/24 ForwardedHeaders.TrustedIPs:10.0.0.3/24,20.0.0.3/24
@ -352,15 +356,18 @@ Responses are compressed when:
* And the `Accept-Encoding` request header contains `gzip` * And the `Accept-Encoding` request header contains `gzip`
* And the response is not already compressed, i.e. the `Content-Encoding` response header is not already set. * And the response is not already compressed, i.e. the `Content-Encoding` response header is not already set.
## Whitelisting ## White Listing
To enable IP whitelisting at the entrypoint level. To enable IP white listing at the entry point level.
```toml ```toml
[entryPoints] [entryPoints]
[entryPoints.http] [entryPoints.http]
address = ":80" address = ":80"
whiteListSourceRange = ["127.0.0.1/32", "192.168.1.7"]
[entryPoints.http]
sourceRange = ["127.0.0.1/32", "192.168.1.7"]
# useXForwardedFor = true
``` ```
## ProxyProtocol ## ProxyProtocol

View file

@ -2,7 +2,6 @@ package middlewares
import ( import (
"fmt" "fmt"
"net"
"net/http" "net/http"
"github.com/containous/traefik/log" "github.com/containous/traefik/log"
@ -18,49 +17,41 @@ type IPWhiteLister struct {
whiteLister *whitelist.IP whiteLister *whitelist.IP
} }
// NewIPWhitelister builds a new IPWhiteLister given a list of CIDR-Strings to whitelist // NewIPWhiteLister builds a new IPWhiteLister given a list of CIDR-Strings to whitelist
func NewIPWhitelister(whitelistStrings []string) (*IPWhiteLister, error) { func NewIPWhiteLister(whiteList []string, useXForwardedFor bool) (*IPWhiteLister, error) {
if len(whiteList) == 0 {
if len(whitelistStrings) == 0 { return nil, errors.New("no white list provided")
return nil, errors.New("no whitelists provided")
} }
whiteLister := IPWhiteLister{} whiteLister := IPWhiteLister{}
ip, err := whitelist.NewIP(whitelistStrings, false) ip, err := whitelist.NewIP(whiteList, false, useXForwardedFor)
if err != nil { if err != nil {
return nil, fmt.Errorf("parsing CIDR whitelist %s: %v", whitelistStrings, err) return nil, fmt.Errorf("parsing CIDR whitelist %s: %v", whiteList, err)
} }
whiteLister.whiteLister = ip whiteLister.whiteLister = ip
whiteLister.handler = negroni.HandlerFunc(whiteLister.handle) whiteLister.handler = negroni.HandlerFunc(whiteLister.handle)
log.Debugf("configured %u IP whitelists: %s", len(whitelistStrings), whitelistStrings) log.Debugf("configured %u IP white list: %s", len(whiteList), whiteList)
return &whiteLister, nil return &whiteLister, nil
} }
func (wl *IPWhiteLister) handle(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { func (wl *IPWhiteLister) handle(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
ipAddress, _, err := net.SplitHostPort(r.RemoteAddr) allowed, ip, err := wl.whiteLister.IsAuthorized(r)
if err != nil { if err != nil {
tracing.SetErrorAndWarnLog(r, "unable to parse remote-address from header: %s - rejecting", r.RemoteAddr) tracing.SetErrorAndDebugLog(r, "request %+v matched none of the white list - rejecting", r)
reject(w)
return
}
allowed, ip, err := wl.whiteLister.Contains(ipAddress)
if err != nil {
tracing.SetErrorAndDebugLog(r, "source-IP %s matched none of the whitelists - rejecting", ipAddress)
reject(w) reject(w)
return return
} }
if allowed { if allowed {
tracing.SetErrorAndDebugLog(r, "source-IP %s matched whitelist %s - passing", ipAddress, wl.whiteLister) tracing.SetErrorAndDebugLog(r, "request %+v matched white list %s - passing", r, wl.whiteLister)
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
return return
} }
tracing.SetErrorAndDebugLog(r, "source-IP %s matched none of the whitelists - rejecting", ip) tracing.SetErrorAndDebugLog(r, "source-IP %s matched none of the white list - rejecting", ip)
reject(w) reject(w)
} }

View file

@ -50,7 +50,7 @@ func (p *Provider) buildConfiguration(catalog []catalogUpdate) *types.Configurat
"getPriority": p.getFuncIntAttribute(label.SuffixFrontendPriority, label.DefaultFrontendPriorityInt), "getPriority": p.getFuncIntAttribute(label.SuffixFrontendPriority, label.DefaultFrontendPriorityInt),
"getPassHostHeader": p.getFuncBoolAttribute(label.SuffixFrontendPassHostHeader, label.DefaultPassHostHeaderBool), "getPassHostHeader": p.getFuncBoolAttribute(label.SuffixFrontendPassHostHeader, label.DefaultPassHostHeaderBool),
"getPassTLSCert": p.getFuncBoolAttribute(label.SuffixFrontendPassTLSCert, label.DefaultPassTLSCert), "getPassTLSCert": p.getFuncBoolAttribute(label.SuffixFrontendPassTLSCert, label.DefaultPassTLSCert),
"getWhitelistSourceRange": p.getFuncSliceAttribute(label.SuffixFrontendWhitelistSourceRange), "getWhiteList": p.getWhiteList,
"getRedirect": p.getRedirect, "getRedirect": p.getRedirect,
"hasErrorPages": p.getFuncHasAttributePrefix(label.BaseFrontendErrorPage), "hasErrorPages": p.getFuncHasAttributePrefix(label.BaseFrontendErrorPage),
"getErrorPages": p.getErrorPages, "getErrorPages": p.getErrorPages,
@ -311,6 +311,19 @@ func (p *Provider) getBuffering(tags []string) *types.Buffering {
} }
} }
func (p *Provider) getWhiteList(tags []string) *types.WhiteList {
ranges := p.getSliceAttribute(label.SuffixFrontendWhiteListSourceRange, tags)
if len(ranges) > 0 {
return &types.WhiteList{
SourceRange: ranges,
UseXForwardedFor: p.getBoolAttribute(label.SuffixFrontendWhiteListUseXForwardedFor, tags, false),
}
}
return nil
}
func (p *Provider) getRedirect(tags []string) *types.Redirect { func (p *Provider) getRedirect(tags []string) *types.Redirect {
permanent := p.getBoolAttribute(label.SuffixFrontendRedirectPermanent, tags, false) permanent := p.getBoolAttribute(label.SuffixFrontendRedirectPermanent, tags, false)

View file

@ -1048,6 +1048,65 @@ func TestProviderGetBuffering(t *testing.T) {
} }
} }
func TestProviderWhiteList(t *testing.T) {
p := &Provider{
Prefix: "traefik",
}
testCases := []struct {
desc string
tags []string
expected *types.WhiteList
}{
{
desc: "should return nil when no white list labels",
expected: nil,
},
{
desc: "should return a struct when only range",
tags: []string{
label.TraefikFrontendWhiteListSourceRange + "=10.10.10.10",
},
expected: &types.WhiteList{
SourceRange: []string{
"10.10.10.10",
},
UseXForwardedFor: false,
},
},
{
desc: "should return a struct when range and UseXForwardedFor",
tags: []string{
label.TraefikFrontendWhiteListSourceRange + "=10.10.10.10",
label.TraefikFrontendWhiteListUseXForwardedFor + "=true",
},
expected: &types.WhiteList{
SourceRange: []string{
"10.10.10.10",
},
UseXForwardedFor: true,
},
},
{
desc: "should return nil when only UseXForwardedFor",
tags: []string{
label.TraefikFrontendWhiteListUseXForwardedFor + "=true",
},
expected: nil,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actual := p.getWhiteList(test.tags)
assert.Equal(t, test.expected, actual)
})
}
}
func TestProviderGetRedirect(t *testing.T) { func TestProviderGetRedirect(t *testing.T) {
p := &Provider{ p := &Provider{
Prefix: "traefik", Prefix: "traefik",

View file

@ -46,12 +46,12 @@ func (p *Provider) buildConfigurationV2(containersInspected []dockerData) *types
"getPassTLSCert": getFuncBoolLabel(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert), "getPassTLSCert": getFuncBoolLabel(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert),
"getEntryPoints": getFuncSliceStringLabel(label.TraefikFrontendEntryPoints), "getEntryPoints": getFuncSliceStringLabel(label.TraefikFrontendEntryPoints),
"getBasicAuth": getFuncSliceStringLabel(label.TraefikFrontendAuthBasic), "getBasicAuth": getFuncSliceStringLabel(label.TraefikFrontendAuthBasic),
"getWhitelistSourceRange": getFuncSliceStringLabel(label.TraefikFrontendWhitelistSourceRange),
"getFrontendRule": p.getFrontendRule, "getFrontendRule": p.getFrontendRule,
"getRedirect": getRedirect, "getRedirect": getRedirect,
"getErrorPages": getErrorPages, "getErrorPages": getErrorPages,
"getRateLimit": getRateLimit, "getRateLimit": getRateLimit,
"getHeaders": getHeaders, "getHeaders": getHeaders,
"getWhiteList": getWhiteList,
} }
// filter containers // filter containers
@ -276,6 +276,31 @@ func getBackendName(container dockerData) string {
return getDefaultBackendName(container) return getDefaultBackendName(container)
} }
func getWhiteList(labels map[string]string) *types.WhiteList {
if label.Has(labels, label.TraefikFrontendWhitelistSourceRange) {
log.Warnf("Deprecated configuration found: %s. Please use %s.", label.TraefikFrontendWhitelistSourceRange, label.TraefikFrontendWhiteListSourceRange)
}
ranges := label.GetSliceStringValue(labels, label.TraefikFrontendWhiteListSourceRange)
if len(ranges) > 0 {
return &types.WhiteList{
SourceRange: ranges,
UseXForwardedFor: label.GetBoolValue(labels, label.TraefikFrontendWhiteListUseXForwardedFor, false),
}
}
// TODO: Deprecated
values := label.GetSliceStringValue(labels, label.TraefikFrontendWhitelistSourceRange)
if len(values) > 0 {
return &types.WhiteList{
SourceRange: values,
UseXForwardedFor: false,
}
}
return nil
}
func getRedirect(labels map[string]string) *types.Redirect { func getRedirect(labels map[string]string) *types.Redirect {
permanent := label.GetBoolValue(labels, label.TraefikFrontendRedirectPermanent, false) permanent := label.GetBoolValue(labels, label.TraefikFrontendRedirectPermanent, false)

View file

@ -123,7 +123,8 @@ func TestDockerBuildConfiguration(t *testing.T) {
label.TraefikFrontendRedirectReplacement: "nope", label.TraefikFrontendRedirectReplacement: "nope",
label.TraefikFrontendRedirectPermanent: "true", label.TraefikFrontendRedirectPermanent: "true",
label.TraefikFrontendRule: "Host:traefik.io", label.TraefikFrontendRule: "Host:traefik.io",
label.TraefikFrontendWhitelistSourceRange: "10.10.10.10", label.TraefikFrontendWhiteListSourceRange: "10.10.10.10",
label.TraefikFrontendWhiteListUseXForwardedFor: "true",
label.TraefikFrontendRequestHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", label.TraefikFrontendRequestHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8",
label.TraefikFrontendResponseHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", label.TraefikFrontendResponseHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8",
@ -187,8 +188,9 @@ func TestDockerBuildConfiguration(t *testing.T) {
"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
}, },
WhitelistSourceRange: []string{ WhiteList: &types.WhiteList{
"10.10.10.10", SourceRange: []string{"10.10.10.10"},
UseXForwardedFor: true,
}, },
Headers: &types.Headers{ Headers: &types.Headers{
CustomRequestHeaders: map[string]string{ CustomRequestHeaders: map[string]string{
@ -692,17 +694,17 @@ func TestDockerGetSliceStringLabel(t *testing.T) {
{ {
desc: "whitelist-label with empty string", desc: "whitelist-label with empty string",
labels: map[string]string{ labels: map[string]string{
label.TraefikFrontendWhitelistSourceRange: "", label.TraefikFrontendWhiteListSourceRange: "",
}, },
labelName: label.TraefikFrontendWhitelistSourceRange, labelName: label.TraefikFrontendWhiteListSourceRange,
expected: nil, expected: nil,
}, },
{ {
desc: "whitelist-label with IPv4 mask", desc: "whitelist-label with IPv4 mask",
labels: map[string]string{ labels: map[string]string{
label.TraefikFrontendWhitelistSourceRange: "1.2.3.4/16", label.TraefikFrontendWhiteListSourceRange: "1.2.3.4/16",
}, },
labelName: label.TraefikFrontendWhitelistSourceRange, labelName: label.TraefikFrontendWhiteListSourceRange,
expected: []string{ expected: []string{
"1.2.3.4/16", "1.2.3.4/16",
}, },
@ -710,9 +712,9 @@ func TestDockerGetSliceStringLabel(t *testing.T) {
{ {
desc: "whitelist-label with IPv6 mask", desc: "whitelist-label with IPv6 mask",
labels: map[string]string{ labels: map[string]string{
label.TraefikFrontendWhitelistSourceRange: "fe80::/16", label.TraefikFrontendWhiteListSourceRange: "fe80::/16",
}, },
labelName: label.TraefikFrontendWhitelistSourceRange, labelName: label.TraefikFrontendWhiteListSourceRange,
expected: []string{ expected: []string{
"fe80::/16", "fe80::/16",
}, },
@ -720,9 +722,9 @@ func TestDockerGetSliceStringLabel(t *testing.T) {
{ {
desc: "whitelist-label with multiple masks", desc: "whitelist-label with multiple masks",
labels: map[string]string{ labels: map[string]string{
label.TraefikFrontendWhitelistSourceRange: "1.1.1.1/24, 1234:abcd::42/32", label.TraefikFrontendWhiteListSourceRange: "1.1.1.1/24, 1234:abcd::42/32",
}, },
labelName: label.TraefikFrontendWhitelistSourceRange, labelName: label.TraefikFrontendWhiteListSourceRange,
expected: []string{ expected: []string{
"1.1.1.1/24", "1.1.1.1/24",
"1234:abcd::42/32", "1234:abcd::42/32",
@ -1025,3 +1027,85 @@ func TestDockerGetPort(t *testing.T) {
}) })
} }
} }
func TestWhiteList(t *testing.T) {
testCases := []struct {
desc string
labels map[string]string
expected *types.WhiteList
}{
{
desc: "should return nil when no white list labels",
labels: map[string]string{},
expected: nil,
},
{
desc: "should return a struct when deprecated label",
labels: map[string]string{
label.TraefikFrontendWhitelistSourceRange: "10.10.10.10",
},
expected: &types.WhiteList{
SourceRange: []string{
"10.10.10.10",
},
UseXForwardedFor: false,
},
},
{
desc: "should return a struct when only range",
labels: map[string]string{
label.TraefikFrontendWhiteListSourceRange: "10.10.10.10",
},
expected: &types.WhiteList{
SourceRange: []string{
"10.10.10.10",
},
UseXForwardedFor: false,
},
},
{
desc: "should return a struct when range and UseXForwardedFor",
labels: map[string]string{
label.TraefikFrontendWhiteListSourceRange: "10.10.10.10",
label.TraefikFrontendWhiteListUseXForwardedFor: "true",
},
expected: &types.WhiteList{
SourceRange: []string{
"10.10.10.10",
},
UseXForwardedFor: true,
},
},
{
desc: "should return a struct when mix deprecated label and new labels",
labels: map[string]string{
label.TraefikFrontendWhitelistSourceRange: "20.20.20.20",
label.TraefikFrontendWhiteListSourceRange: "10.10.10.10",
label.TraefikFrontendWhiteListUseXForwardedFor: "true",
},
expected: &types.WhiteList{
SourceRange: []string{
"10.10.10.10",
},
UseXForwardedFor: true,
},
},
{
desc: "should return nil when only UseXForwardedFor",
labels: map[string]string{
label.TraefikFrontendWhiteListUseXForwardedFor: "true",
},
expected: nil,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actual := getWhiteList(test.labels)
assert.Equal(t, test.expected, actual)
})
}
}

View file

@ -131,7 +131,8 @@ func TestSwarmBuildConfiguration(t *testing.T) {
label.TraefikFrontendRedirectRegex: "nope", label.TraefikFrontendRedirectRegex: "nope",
label.TraefikFrontendRedirectReplacement: "nope", label.TraefikFrontendRedirectReplacement: "nope",
label.TraefikFrontendRule: "Host:traefik.io", label.TraefikFrontendRule: "Host:traefik.io",
label.TraefikFrontendWhitelistSourceRange: "10.10.10.10", label.TraefikFrontendWhiteListSourceRange: "10.10.10.10",
label.TraefikFrontendWhiteListUseXForwardedFor: "true",
label.TraefikFrontendRequestHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", label.TraefikFrontendRequestHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8",
label.TraefikFrontendResponseHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", label.TraefikFrontendResponseHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8",
@ -193,8 +194,9 @@ func TestSwarmBuildConfiguration(t *testing.T) {
"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
}, },
WhitelistSourceRange: []string{ WhiteList: &types.WhiteList{
"10.10.10.10", SourceRange: []string{"10.10.10.10"},
UseXForwardedFor: true,
}, },
Headers: &types.Headers{ Headers: &types.Headers{
CustomRequestHeaders: map[string]string{ CustomRequestHeaders: map[string]string{

View file

@ -85,7 +85,8 @@ func TestSegmentBuildConfiguration(t *testing.T) {
label.Prefix + "sauternes." + label.SuffixFrontendRedirectRegex: "nope", label.Prefix + "sauternes." + label.SuffixFrontendRedirectRegex: "nope",
label.Prefix + "sauternes." + label.SuffixFrontendRedirectReplacement: "nope", label.Prefix + "sauternes." + label.SuffixFrontendRedirectReplacement: "nope",
label.Prefix + "sauternes." + label.SuffixFrontendRedirectPermanent: "true", label.Prefix + "sauternes." + label.SuffixFrontendRedirectPermanent: "true",
label.Prefix + "sauternes." + label.SuffixFrontendWhitelistSourceRange: "10.10.10.10", label.Prefix + "sauternes." + label.SuffixFrontendWhiteListSourceRange: "10.10.10.10",
label.Prefix + "sauternes." + label.SuffixFrontendWhiteListUseXForwardedFor: "true",
label.Prefix + "sauternes." + label.SuffixFrontendRequestHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", label.Prefix + "sauternes." + label.SuffixFrontendRequestHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8",
label.Prefix + "sauternes." + label.SuffixFrontendResponseHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", label.Prefix + "sauternes." + label.SuffixFrontendResponseHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8",
@ -144,8 +145,9 @@ func TestSegmentBuildConfiguration(t *testing.T) {
"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
}, },
WhitelistSourceRange: []string{ WhiteList: &types.WhiteList{
"10.10.10.10", SourceRange: []string{"10.10.10.10"},
UseXForwardedFor: true,
}, },
Headers: &types.Headers{ Headers: &types.Headers{
CustomRequestHeaders: map[string]string{ CustomRequestHeaders: map[string]string{

View file

@ -103,7 +103,7 @@ func (p *Provider) buildConfigurationV1(containersInspected []dockerData) *types
"getServiceWeight": getFuncServiceStringLabelV1(label.SuffixWeight, label.DefaultWeight), "getServiceWeight": getFuncServiceStringLabelV1(label.SuffixWeight, label.DefaultWeight),
// Services - Frontend functions // Services - Frontend functions
"getServiceEntryPoints": getFuncServiceSliceStringLabelV1(label.SuffixFrontendEntryPoints), "getServiceEntryPoints": getFuncServiceSliceStringLabelV1(label.SuffixFrontendEntryPoints),
"getServiceWhitelistSourceRange": getFuncServiceSliceStringLabelV1(label.SuffixFrontendWhitelistSourceRange), "getServiceWhitelistSourceRange": getFuncServiceSliceStringLabelV1(label.SuffixFrontendWhiteListSourceRange),
"getServiceBasicAuth": getFuncServiceSliceStringLabelV1(label.SuffixFrontendAuthBasic), "getServiceBasicAuth": getFuncServiceSliceStringLabelV1(label.SuffixFrontendAuthBasic),
"getServiceFrontendRule": p.getServiceFrontendRuleV1, "getServiceFrontendRule": p.getServiceFrontendRuleV1,
"getServicePassHostHeader": getFuncServiceBoolLabelV1(label.SuffixFrontendPassHostHeader, label.DefaultPassHostHeaderBool), "getServicePassHostHeader": getFuncServiceBoolLabelV1(label.SuffixFrontendPassHostHeader, label.DefaultPassHostHeaderBool),

View file

@ -55,11 +55,11 @@ func (p *Provider) buildConfiguration(services map[string][]ecsInstance) (*types
"getPriority": getFuncIntValue(label.TraefikFrontendPriority, label.DefaultFrontendPriorityInt), "getPriority": getFuncIntValue(label.TraefikFrontendPriority, label.DefaultFrontendPriorityInt),
"getBasicAuth": getFuncSliceString(label.TraefikFrontendAuthBasic), "getBasicAuth": getFuncSliceString(label.TraefikFrontendAuthBasic),
"getEntryPoints": getFuncSliceString(label.TraefikFrontendEntryPoints), "getEntryPoints": getFuncSliceString(label.TraefikFrontendEntryPoints),
"getWhitelistSourceRange": getFuncSliceString(label.TraefikFrontendWhitelistSourceRange),
"getRedirect": getRedirect, "getRedirect": getRedirect,
"getErrorPages": getErrorPages, "getErrorPages": getErrorPages,
"getRateLimit": getRateLimit, "getRateLimit": getRateLimit,
"getHeaders": getHeaders, "getHeaders": getHeaders,
"getWhiteList": getWhiteList,
} }
return p.GetConfiguration("templates/ecs.tmpl", ecsFuncMap, struct { return p.GetConfiguration("templates/ecs.tmpl", ecsFuncMap, struct {
Services map[string][]ecsInstance Services map[string][]ecsInstance
@ -211,6 +211,18 @@ func getServers(instances []ecsInstance) map[string]types.Server {
return servers return servers
} }
func getWhiteList(instance ecsInstance) *types.WhiteList {
ranges := getSliceString(instance, label.TraefikFrontendWhiteListSourceRange)
if len(ranges) > 0 {
return &types.WhiteList{
SourceRange: ranges,
UseXForwardedFor: getBoolValue(instance, label.TraefikFrontendWhiteListUseXForwardedFor, false),
}
}
return nil
}
func getRedirect(instance ecsInstance) *types.Redirect { func getRedirect(instance ecsInstance) *types.Redirect {
permanent := getBoolValue(instance, label.TraefikFrontendRedirectPermanent, false) permanent := getBoolValue(instance, label.TraefikFrontendRedirectPermanent, false)

View file

@ -152,7 +152,8 @@ func TestBuildConfiguration(t *testing.T) {
label.TraefikFrontendRedirectReplacement: aws.String("nope"), label.TraefikFrontendRedirectReplacement: aws.String("nope"),
label.TraefikFrontendRedirectPermanent: aws.String("true"), label.TraefikFrontendRedirectPermanent: aws.String("true"),
label.TraefikFrontendRule: aws.String("Host:traefik.io"), label.TraefikFrontendRule: aws.String("Host:traefik.io"),
label.TraefikFrontendWhitelistSourceRange: aws.String("10.10.10.10"), label.TraefikFrontendWhiteListSourceRange: aws.String("10.10.10.10"),
label.TraefikFrontendWhiteListUseXForwardedFor: aws.String("true"),
label.TraefikFrontendRequestHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), label.TraefikFrontendRequestHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"),
label.TraefikFrontendResponseHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), label.TraefikFrontendResponseHeaders: aws.String("Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"),
@ -257,8 +258,9 @@ func TestBuildConfiguration(t *testing.T) {
"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
}, },
WhitelistSourceRange: []string{ WhiteList: &types.WhiteList{
"10.10.10.10", SourceRange: []string{"10.10.10.10"},
UseXForwardedFor: true,
}, },
Headers: &types.Headers{ Headers: &types.Headers{
CustomRequestHeaders: map[string]string{ CustomRequestHeaders: map[string]string{
@ -1115,6 +1117,74 @@ func TestGetServers(t *testing.T) {
} }
} }
func TestWhiteList(t *testing.T) {
testCases := []struct {
desc string
instance ecsInstance
expected *types.WhiteList
}{
{
desc: "should return nil when no white list labels",
instance: ecsInstance{
containerDefinition: &ecs.ContainerDefinition{
DockerLabels: map[string]*string{},
},
},
expected: nil,
},
{
desc: "should return a struct when only range",
instance: ecsInstance{
containerDefinition: &ecs.ContainerDefinition{
DockerLabels: map[string]*string{
label.TraefikFrontendWhiteListSourceRange: aws.String("10.10.10.10"),
}},
},
expected: &types.WhiteList{
SourceRange: []string{
"10.10.10.10",
},
UseXForwardedFor: false,
},
},
{
desc: "should return a struct when range and UseXForwardedFor",
instance: ecsInstance{
containerDefinition: &ecs.ContainerDefinition{
DockerLabels: map[string]*string{
label.TraefikFrontendWhiteListSourceRange: aws.String("10.10.10.10"),
label.TraefikFrontendWhiteListUseXForwardedFor: aws.String("true"),
}},
},
expected: &types.WhiteList{
SourceRange: []string{
"10.10.10.10",
},
UseXForwardedFor: true,
},
},
{
desc: "should return nil when only UseXForwardedFor",
instance: ecsInstance{
containerDefinition: &ecs.ContainerDefinition{
DockerLabels: map[string]*string{
label.TraefikFrontendWhiteListUseXForwardedFor: aws.String("true"),
}},
},
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actual := getWhiteList(test.instance)
assert.Equal(t, test.expected, actual)
})
}
}
func TestGetRedirect(t *testing.T) { func TestGetRedirect(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string

View file

@ -10,7 +10,8 @@ const (
annotationKubernetesAuthType = "ingress.kubernetes.io/auth-type" annotationKubernetesAuthType = "ingress.kubernetes.io/auth-type"
annotationKubernetesAuthSecret = "ingress.kubernetes.io/auth-secret" annotationKubernetesAuthSecret = "ingress.kubernetes.io/auth-secret"
annotationKubernetesRewriteTarget = "ingress.kubernetes.io/rewrite-target" annotationKubernetesRewriteTarget = "ingress.kubernetes.io/rewrite-target"
annotationKubernetesWhitelistSourceRange = "ingress.kubernetes.io/whitelist-source-range" annotationKubernetesWhiteListSourceRange = "ingress.kubernetes.io/whitelist-source-range"
annotationKubernetesWhiteListUseXForwardedFor = "ingress.kubernetes.io/whitelist-x-forwarded-for"
annotationKubernetesPreserveHost = "ingress.kubernetes.io/preserve-host" annotationKubernetesPreserveHost = "ingress.kubernetes.io/preserve-host"
annotationKubernetesPassTLSCert = "ingress.kubernetes.io/pass-tls-cert" annotationKubernetesPassTLSCert = "ingress.kubernetes.io/pass-tls-cert"
annotationKubernetesFrontendEntryPoints = "ingress.kubernetes.io/frontend-entry-points" annotationKubernetesFrontendEntryPoints = "ingress.kubernetes.io/frontend-entry-points"

View file

@ -202,9 +202,13 @@ func basicAuth(auth ...string) func(*types.Frontend) {
} }
} }
func whitelistSourceRange(ranges ...string) func(*types.Frontend) { func whiteList(useXFF bool, ranges ...string) func(*types.Frontend) {
return func(f *types.Frontend) { return func(f *types.Frontend) {
f.WhitelistSourceRange = ranges if f.WhiteList == nil {
f.WhiteList = &types.WhiteList{}
}
f.WhiteList.UseXForwardedFor = useXFF
f.WhiteList.SourceRange = ranges
} }
} }

View file

@ -200,7 +200,6 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
passTLSCert := getBoolValue(i.Annotations, annotationKubernetesPassTLSCert, p.EnablePassTLSCert) passTLSCert := getBoolValue(i.Annotations, annotationKubernetesPassTLSCert, p.EnablePassTLSCert)
priority := getIntValue(i.Annotations, annotationKubernetesPriority, 0) priority := getIntValue(i.Annotations, annotationKubernetesPriority, 0)
entryPoints := getSliceStringValue(i.Annotations, annotationKubernetesFrontendEntryPoints) entryPoints := getSliceStringValue(i.Annotations, annotationKubernetesFrontendEntryPoints)
whitelistSourceRange := getSliceStringValue(i.Annotations, annotationKubernetesWhitelistSourceRange)
templateObjects.Frontends[baseName] = &types.Frontend{ templateObjects.Frontends[baseName] = &types.Frontend{
Backend: baseName, Backend: baseName,
@ -209,7 +208,7 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
Routes: make(map[string]types.Route), Routes: make(map[string]types.Route),
Priority: priority, Priority: priority,
BasicAuth: basicAuthCreds, BasicAuth: basicAuthCreds,
WhitelistSourceRange: whitelistSourceRange, WhiteList: getWhiteList(i),
Redirect: getFrontendRedirect(i), Redirect: getFrontendRedirect(i),
EntryPoints: entryPoints, EntryPoints: entryPoints,
Headers: getHeader(i), Headers: getHeader(i),
@ -457,7 +456,7 @@ func getTLS(ingress *extensionsv1beta1.Ingress, k8sClient Client) ([]*tls.Config
func endpointPortNumber(servicePort corev1.ServicePort, endpointPorts []corev1.EndpointPort) int { func endpointPortNumber(servicePort corev1.ServicePort, endpointPorts []corev1.EndpointPort) int {
if len(endpointPorts) > 0 { if len(endpointPorts) > 0 {
//name is optional if there is only one port // name is optional if there is only one port
port := endpointPorts[0] port := endpointPorts[0]
for _, endpointPort := range endpointPorts { for _, endpointPort := range endpointPorts {
if servicePort.Name == endpointPort.Name { if servicePort.Name == endpointPort.Name {
@ -510,6 +509,18 @@ func getFrontendRedirect(i *extensionsv1beta1.Ingress) *types.Redirect {
return nil return nil
} }
func getWhiteList(i *extensionsv1beta1.Ingress) *types.WhiteList {
ranges := getSliceStringValue(i.Annotations, annotationKubernetesWhiteListSourceRange)
if len(ranges) <= 0 {
return nil
}
return &types.WhiteList{
SourceRange: ranges,
UseXForwardedFor: getBoolValue(i.Annotations, annotationKubernetesWhiteListUseXForwardedFor, false),
}
}
func getBuffering(service *corev1.Service) *types.Buffering { func getBuffering(service *corev1.Service) *types.Buffering {
var buffering *types.Buffering var buffering *types.Buffering

View file

@ -665,7 +665,8 @@ func TestIngressAnnotations(t *testing.T) {
), ),
buildIngress( buildIngress(
iNamespace("testing"), iNamespace("testing"),
iAnnotation(annotationKubernetesWhitelistSourceRange, "1.1.1.1/24, 1234:abcd::42/32"), iAnnotation(annotationKubernetesWhiteListSourceRange, "1.1.1.1/24, 1234:abcd::42/32"),
iAnnotation(annotationKubernetesWhiteListUseXForwardedFor, "true"),
iRules( iRules(
iRule( iRule(
iHost("test"), iHost("test"),
@ -984,7 +985,7 @@ rateset:
), ),
frontend("test/whitelist-source-range", frontend("test/whitelist-source-range",
passHostHeader(), passHostHeader(),
whitelistSourceRange("1.1.1.1/24", "1234:abcd::42/32"), whiteList(true, "1.1.1.1/24", "1234:abcd::42/32"),
routes( routes(
route("/whitelist-source-range", "PathPrefix:/whitelist-source-range"), route("/whitelist-source-range", "PathPrefix:/whitelist-source-range"),
route("test", "Host:test")), route("test", "Host:test")),

View file

@ -28,7 +28,8 @@ const (
pathFrontendPassHostHeaderDeprecated = "/passHostHeader" // Deprecated pathFrontendPassHostHeaderDeprecated = "/passHostHeader" // Deprecated
pathFrontendPassHostHeader = "/passhostheader" pathFrontendPassHostHeader = "/passhostheader"
pathFrontendPassTLSCert = "/passtlscert" pathFrontendPassTLSCert = "/passtlscert"
pathFrontendWhiteListSourceRange = "/whitelistsourcerange" pathFrontendWhiteListSourceRange = "/whitelist/sourcerange"
pathFrontendWhiteListUseXForwardedFor = "/whitelist/usexforwardedfor"
pathFrontendBasicAuth = "/basicauth" pathFrontendBasicAuth = "/basicauth"
pathFrontendEntryPoints = "/entrypoints" pathFrontendEntryPoints = "/entrypoints"
pathFrontendRedirectEntryPoint = "/redirect/entrypoint" pathFrontendRedirectEntryPoint = "/redirect/entrypoint"

View file

@ -46,13 +46,13 @@ func (p *Provider) buildConfiguration() *types.Configuration {
"getPassHostHeader": p.getPassHostHeader(), "getPassHostHeader": p.getPassHostHeader(),
"getPassTLSCert": p.getFuncBool(pathFrontendPassTLSCert, label.DefaultPassTLSCert), "getPassTLSCert": p.getFuncBool(pathFrontendPassTLSCert, label.DefaultPassTLSCert),
"getEntryPoints": p.getFuncList(pathFrontendEntryPoints), "getEntryPoints": p.getFuncList(pathFrontendEntryPoints),
"getWhitelistSourceRange": p.getFuncList(pathFrontendWhiteListSourceRange),
"getBasicAuth": p.getFuncList(pathFrontendBasicAuth), "getBasicAuth": p.getFuncList(pathFrontendBasicAuth),
"getRoutes": p.getRoutes, "getRoutes": p.getRoutes,
"getRedirect": p.getRedirect, "getRedirect": p.getRedirect,
"getErrorPages": p.getErrorPages, "getErrorPages": p.getErrorPages,
"getRateLimit": p.getRateLimit, "getRateLimit": p.getRateLimit,
"getHeaders": p.getHeaders, "getHeaders": p.getHeaders,
"getWhiteList": p.getWhiteList,
// Backend functions // Backend functions
"getServers": p.getServers, "getServers": p.getServers,
@ -125,6 +125,19 @@ func (p *Provider) getStickinessCookieName(rootPath string) string {
return p.get("", rootPath, pathBackendLoadBalancerStickinessCookieName) return p.get("", rootPath, pathBackendLoadBalancerStickinessCookieName)
} }
func (p *Provider) getWhiteList(rootPath string) *types.WhiteList {
ranges := p.getList(rootPath, pathFrontendWhiteListSourceRange)
if len(ranges) > 0 {
return &types.WhiteList{
SourceRange: ranges,
UseXForwardedFor: p.getBool(false, rootPath, pathFrontendWhiteListUseXForwardedFor),
}
}
return nil
}
func (p *Provider) getRedirect(rootPath string) *types.Redirect { func (p *Provider) getRedirect(rootPath string) *types.Redirect {
permanent := p.getBool(false, rootPath, pathFrontendRedirectPermanent) permanent := p.getBool(false, rootPath, pathFrontendRedirectPermanent)

View file

@ -91,6 +91,7 @@ func TestProviderBuildConfiguration(t *testing.T) {
withPair(pathFrontendPassTLSCert, "true"), withPair(pathFrontendPassTLSCert, "true"),
withPair(pathFrontendEntryPoints, "http,https"), withPair(pathFrontendEntryPoints, "http,https"),
withPair(pathFrontendWhiteListSourceRange, "1.1.1.1/24, 1234:abcd::42/32"), withPair(pathFrontendWhiteListSourceRange, "1.1.1.1/24, 1234:abcd::42/32"),
withPair(pathFrontendWhiteListUseXForwardedFor, "true"),
withPair(pathFrontendBasicAuth, "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/, test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"), withPair(pathFrontendBasicAuth, "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/, test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"),
withPair(pathFrontendRedirectEntryPoint, "https"), withPair(pathFrontendRedirectEntryPoint, "https"),
withPair(pathFrontendRedirectRegex, "nope"), withPair(pathFrontendRedirectRegex, "nope"),
@ -184,7 +185,10 @@ func TestProviderBuildConfiguration(t *testing.T) {
EntryPoints: []string{"http", "https"}, EntryPoints: []string{"http", "https"},
Backend: "backend1", Backend: "backend1",
PassTLSCert: true, PassTLSCert: true,
WhitelistSourceRange: []string{"1.1.1.1/24", "1234:abcd::42/32"}, WhiteList: &types.WhiteList{
SourceRange: []string{"1.1.1.1/24", "1234:abcd::42/32"},
UseXForwardedFor: true,
},
BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"},
Redirect: &types.Redirect{ Redirect: &types.Redirect{
EntryPoint: "https", EntryPoint: "https",
@ -1031,6 +1035,68 @@ func TestProviderHasStickinessLabel(t *testing.T) {
} }
} }
func TestWhiteList(t *testing.T) {
testCases := []struct {
desc string
rootPath string
kvPairs []*store.KVPair
expected *types.WhiteList
}{
{
desc: "should return nil when no white list labels",
rootPath: "traefik/frontends/foo",
expected: nil,
},
{
desc: "should return a struct when only range",
rootPath: "traefik/frontends/foo",
kvPairs: filler("traefik",
frontend("foo",
withPair(pathFrontendWhiteListSourceRange, "10.10.10.10"))),
expected: &types.WhiteList{
SourceRange: []string{
"10.10.10.10",
},
UseXForwardedFor: false,
},
},
{
desc: "should return a struct when range and UseXForwardedFor",
rootPath: "traefik/frontends/foo",
kvPairs: filler("traefik",
frontend("foo",
withPair(pathFrontendWhiteListSourceRange, "10.10.10.10"),
withPair(pathFrontendWhiteListUseXForwardedFor, "true"))),
expected: &types.WhiteList{
SourceRange: []string{
"10.10.10.10",
},
UseXForwardedFor: true,
},
},
{
desc: "should return nil when only UseXForwardedFor",
rootPath: "traefik/frontends/foo",
kvPairs: filler("traefik",
frontend("foo",
withPair(pathFrontendWhiteListUseXForwardedFor, "true"))),
expected: nil,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
p := newProviderMock(test.kvPairs)
actual := p.getWhiteList(test.rootPath)
assert.Equal(t, test.expected, actual)
})
}
}
func TestProviderGetRedirect(t *testing.T) { func TestProviderGetRedirect(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string

View file

@ -65,7 +65,10 @@ const (
SuffixFrontendRedirectReplacement = "frontend.redirect.replacement" SuffixFrontendRedirectReplacement = "frontend.redirect.replacement"
SuffixFrontendRedirectPermanent = "frontend.redirect.permanent" SuffixFrontendRedirectPermanent = "frontend.redirect.permanent"
SuffixFrontendRule = "frontend.rule" SuffixFrontendRule = "frontend.rule"
SuffixFrontendWhitelistSourceRange = "frontend.whitelistSourceRange" SuffixFrontendWhitelistSourceRange = "frontend.whitelistSourceRange" // Deprecated
SuffixFrontendWhiteList = "frontend.whiteList."
SuffixFrontendWhiteListSourceRange = SuffixFrontendWhiteList + "sourceRange"
SuffixFrontendWhiteListUseXForwardedFor = SuffixFrontendWhiteList + "useXForwardedFor"
TraefikDomain = Prefix + SuffixDomain TraefikDomain = Prefix + SuffixDomain
TraefikEnable = Prefix + SuffixEnable TraefikEnable = Prefix + SuffixEnable
TraefikPort = Prefix + SuffixPort TraefikPort = Prefix + SuffixPort
@ -106,7 +109,9 @@ const (
TraefikFrontendRedirectReplacement = Prefix + SuffixFrontendRedirectReplacement TraefikFrontendRedirectReplacement = Prefix + SuffixFrontendRedirectReplacement
TraefikFrontendRedirectPermanent = Prefix + SuffixFrontendRedirectPermanent TraefikFrontendRedirectPermanent = Prefix + SuffixFrontendRedirectPermanent
TraefikFrontendRule = Prefix + SuffixFrontendRule TraefikFrontendRule = Prefix + SuffixFrontendRule
TraefikFrontendWhitelistSourceRange = Prefix + SuffixFrontendWhitelistSourceRange TraefikFrontendWhitelistSourceRange = Prefix + SuffixFrontendWhitelistSourceRange // Deprecated
TraefikFrontendWhiteListSourceRange = Prefix + SuffixFrontendWhiteListSourceRange
TraefikFrontendWhiteListUseXForwardedFor = Prefix + SuffixFrontendWhiteListUseXForwardedFor
TraefikFrontendRequestHeaders = Prefix + SuffixFrontendRequestHeaders TraefikFrontendRequestHeaders = Prefix + SuffixFrontendRequestHeaders
TraefikFrontendResponseHeaders = Prefix + SuffixFrontendResponseHeaders TraefikFrontendResponseHeaders = Prefix + SuffixFrontendResponseHeaders
TraefikFrontendAllowedHosts = Prefix + SuffixFrontendHeadersAllowedHosts TraefikFrontendAllowedHosts = Prefix + SuffixFrontendHeadersAllowedHosts

View file

@ -76,11 +76,14 @@ func (p *Provider) buildConfiguration() *types.Configuration {
"getFrontendRule": p.getFrontendRule, "getFrontendRule": p.getFrontendRule,
"getFrontendName": p.getFrontendName, "getFrontendName": p.getFrontendName,
"getBasicAuth": getFuncSliceStringService(label.SuffixFrontendAuthBasic), "getBasicAuth": getFuncSliceStringService(label.SuffixFrontendAuthBasic),
"getWhitelistSourceRange": getFuncSliceStringService(label.SuffixFrontendWhitelistSourceRange),
"getRedirect": getRedirect, "getRedirect": getRedirect,
"getErrorPages": getErrorPages, "getErrorPages": getErrorPages,
"getRateLimit": getRateLimit, "getRateLimit": getRateLimit,
"getHeaders": getHeaders, "getHeaders": getHeaders,
"getWhiteList": getWhiteList,
// TODO Deprecated [breaking]
"getWhitelistSourceRange": getFuncSliceStringService(label.SuffixFrontendWhitelistSourceRange),
} }
v := url.Values{} v := url.Values{}
@ -486,6 +489,20 @@ func (p *Provider) getServers(application marathon.Application, serviceName stri
return servers return servers
} }
func getWhiteList(application marathon.Application, serviceName string) *types.WhiteList {
labels := getLabels(application, serviceName)
ranges := label.GetSliceStringValue(labels, getLabelName(serviceName, label.SuffixFrontendWhiteListSourceRange))
if len(ranges) > 0 {
return &types.WhiteList{
SourceRange: ranges,
UseXForwardedFor: label.GetBoolValue(labels, getLabelName(serviceName, label.SuffixFrontendWhiteListUseXForwardedFor), false),
}
}
return nil
}
func getRedirect(application marathon.Application, serviceName string) *types.Redirect { func getRedirect(application marathon.Application, serviceName string) *types.Redirect {
labels := getLabels(application, serviceName) labels := getLabels(application, serviceName)

View file

@ -206,7 +206,8 @@ func TestBuildConfigurationNonAPIErrors(t *testing.T) {
withLabel(label.TraefikFrontendRedirectReplacement, "nope"), withLabel(label.TraefikFrontendRedirectReplacement, "nope"),
withLabel(label.TraefikFrontendRedirectPermanent, "true"), withLabel(label.TraefikFrontendRedirectPermanent, "true"),
withLabel(label.TraefikFrontendRule, "Host:traefik.io"), withLabel(label.TraefikFrontendRule, "Host:traefik.io"),
withLabel(label.TraefikFrontendWhitelistSourceRange, "10.10.10.10"), withLabel(label.TraefikFrontendWhiteListSourceRange, "10.10.10.10"),
withLabel(label.TraefikFrontendWhiteListUseXForwardedFor, "true"),
withLabel(label.TraefikFrontendRequestHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), withLabel(label.TraefikFrontendRequestHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"),
withLabel(label.TraefikFrontendResponseHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"), withLabel(label.TraefikFrontendResponseHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8"),
@ -268,8 +269,9 @@ func TestBuildConfigurationNonAPIErrors(t *testing.T) {
"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
}, },
WhitelistSourceRange: []string{ WhiteList: &types.WhiteList{
"10.10.10.10", SourceRange: []string{"10.10.10.10"},
UseXForwardedFor: true,
}, },
Headers: &types.Headers{ Headers: &types.Headers{
CustomRequestHeaders: map[string]string{ CustomRequestHeaders: map[string]string{
@ -498,7 +500,7 @@ func TestBuildConfigurationServicesNonAPIErrors(t *testing.T) {
application: application( application: application(
appPorts(80, 81), appPorts(80, 81),
//withLabel(label.TraefikBackend, "foobar"), // withLabel(label.TraefikBackend, "foobar"),
withLabel(label.TraefikBackendCircuitBreakerExpression, "NetworkErrorRatio() > 0.5"), withLabel(label.TraefikBackendCircuitBreakerExpression, "NetworkErrorRatio() > 0.5"),
withLabel(label.TraefikBackendHealthCheckPath, "/health"), withLabel(label.TraefikBackendHealthCheckPath, "/health"),
@ -530,7 +532,8 @@ func TestBuildConfigurationServicesNonAPIErrors(t *testing.T) {
withServiceLabel(label.TraefikFrontendRedirectReplacement, "nope", "containous"), withServiceLabel(label.TraefikFrontendRedirectReplacement, "nope", "containous"),
withServiceLabel(label.TraefikFrontendRedirectPermanent, "true", "containous"), withServiceLabel(label.TraefikFrontendRedirectPermanent, "true", "containous"),
withServiceLabel(label.TraefikFrontendRule, "Host:traefik.io", "containous"), withServiceLabel(label.TraefikFrontendRule, "Host:traefik.io", "containous"),
withServiceLabel(label.TraefikFrontendWhitelistSourceRange, "10.10.10.10", "containous"), withServiceLabel(label.TraefikFrontendWhiteListSourceRange, "10.10.10.10", "containous"),
withServiceLabel(label.TraefikFrontendWhiteListUseXForwardedFor, "true", "containous"),
withServiceLabel(label.TraefikFrontendRequestHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", "containous"), withServiceLabel(label.TraefikFrontendRequestHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", "containous"),
withServiceLabel(label.TraefikFrontendResponseHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", "containous"), withServiceLabel(label.TraefikFrontendResponseHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", "containous"),
@ -591,8 +594,9 @@ func TestBuildConfigurationServicesNonAPIErrors(t *testing.T) {
"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
}, },
WhitelistSourceRange: []string{ WhiteList: &types.WhiteList{
"10.10.10.10", SourceRange: []string{"10.10.10.10"},
UseXForwardedFor: true,
}, },
Headers: &types.Headers{ Headers: &types.Headers{
CustomRequestHeaders: map[string]string{ CustomRequestHeaders: map[string]string{
@ -1627,6 +1631,107 @@ func TestGetServers(t *testing.T) {
} }
} }
func TestWhiteList(t *testing.T) {
testCases := []struct {
desc string
application marathon.Application
serviceName string
expected *types.WhiteList
}{
{
desc: "should return nil when no white list labels",
application: application(
appPorts(80),
),
expected: nil,
},
{
desc: "should return a struct when only range",
application: application(
appPorts(80),
withLabel(label.TraefikFrontendWhiteListSourceRange, "10.10.10.10"),
),
expected: &types.WhiteList{
SourceRange: []string{
"10.10.10.10",
},
UseXForwardedFor: false,
},
},
{
desc: "should return a struct when range and UseXForwardedFor",
application: application(
appPorts(80),
withLabel(label.TraefikFrontendWhiteListSourceRange, "10.10.10.10"),
withLabel(label.TraefikFrontendWhiteListUseXForwardedFor, "true"),
),
expected: &types.WhiteList{
SourceRange: []string{
"10.10.10.10",
},
UseXForwardedFor: true,
},
},
{
desc: "should return nil when only UseXForwardedFor",
application: application(
appPorts(80),
withLabel(label.TraefikFrontendWhiteListUseXForwardedFor, "true"),
),
expected: nil,
},
// Service
{
desc: "should return a struct when only range on service",
application: application(
appPorts(80),
withLabel(label.Prefix+"containous."+label.SuffixFrontendWhiteListSourceRange, "10.10.10.10"),
),
serviceName: "containous",
expected: &types.WhiteList{
SourceRange: []string{
"10.10.10.10",
},
UseXForwardedFor: false,
},
},
{
desc: "should return a struct when range and UseXForwardedFor on service",
application: application(
appPorts(80),
withLabel(label.Prefix+"containous."+label.SuffixFrontendWhiteListSourceRange, "10.10.10.10"),
withLabel(label.Prefix+"containous."+label.SuffixFrontendWhiteListUseXForwardedFor, "true"),
),
serviceName: "containous",
expected: &types.WhiteList{
SourceRange: []string{
"10.10.10.10",
},
UseXForwardedFor: true,
},
},
{
desc: "should return nil when only UseXForwardedFor on service",
application: application(
appPorts(80),
withLabel(label.Prefix+"containous."+label.SuffixFrontendWhiteListUseXForwardedFor, "true"),
),
serviceName: "containous",
expected: nil,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actual := getWhiteList(test.application, test.serviceName)
assert.Equal(t, test.expected, actual)
})
}
}
func TestGetRedirect(t *testing.T) { func TestGetRedirect(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string

View file

@ -44,7 +44,6 @@ func (p *Provider) buildConfiguration(tasks []state.Task) *types.Configuration {
"getFrontEndName": getFrontendName, "getFrontEndName": getFrontendName,
"getEntryPoints": getFuncSliceStringValue(label.TraefikFrontendEntryPoints), "getEntryPoints": getFuncSliceStringValue(label.TraefikFrontendEntryPoints),
"getBasicAuth": getFuncSliceStringValue(label.TraefikFrontendAuthBasic), "getBasicAuth": getFuncSliceStringValue(label.TraefikFrontendAuthBasic),
"getWhitelistSourceRange": getFuncSliceStringValue(label.TraefikFrontendWhitelistSourceRange),
"getPriority": getFuncStringValue(label.TraefikFrontendPriority, label.DefaultFrontendPriority), "getPriority": getFuncStringValue(label.TraefikFrontendPriority, label.DefaultFrontendPriority),
"getPassHostHeader": getFuncBoolValue(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeaderBool), "getPassHostHeader": getFuncBoolValue(label.TraefikFrontendPassHostHeader, label.DefaultPassHostHeaderBool),
"getPassTLSCert": getFuncBoolValue(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert), "getPassTLSCert": getFuncBoolValue(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert),
@ -53,6 +52,7 @@ func (p *Provider) buildConfiguration(tasks []state.Task) *types.Configuration {
"getErrorPages": getErrorPages, "getErrorPages": getErrorPages,
"getRateLimit": getRateLimit, "getRateLimit": getRateLimit,
"getHeaders": getHeaders, "getHeaders": getHeaders,
"getWhiteList": getWhiteList,
// TODO Deprecated [breaking] // TODO Deprecated [breaking]
"getFrontendBackend": getBackendName, "getFrontendBackend": getBackendName,
@ -337,6 +337,18 @@ func (p *Provider) getServers(tasks []state.Task) map[string]types.Server {
return servers return servers
} }
func getWhiteList(task state.Task) *types.WhiteList {
ranges := getSliceStringValue(task, label.TraefikFrontendWhiteListSourceRange)
if len(ranges) > 0 {
return &types.WhiteList{
SourceRange: ranges,
UseXForwardedFor: getBoolValue(task, label.TraefikFrontendWhiteListUseXForwardedFor, false),
}
}
return nil
}
func getRedirect(task state.Task) *types.Redirect { func getRedirect(task state.Task) *types.Redirect {
permanent := getBoolValue(task, label.TraefikFrontendRedirectPermanent, false) permanent := getBoolValue(task, label.TraefikFrontendRedirectPermanent, false)

View file

@ -148,7 +148,8 @@ func TestBuildConfiguration(t *testing.T) {
withLabel(label.TraefikFrontendRedirectReplacement, "nope"), withLabel(label.TraefikFrontendRedirectReplacement, "nope"),
withLabel(label.TraefikFrontendRedirectPermanent, "true"), withLabel(label.TraefikFrontendRedirectPermanent, "true"),
withLabel(label.TraefikFrontendRule, "Host:traefik.io"), withLabel(label.TraefikFrontendRule, "Host:traefik.io"),
withLabel(label.TraefikFrontendWhitelistSourceRange, "10.10.10.10"), withLabel(label.TraefikFrontendWhiteListSourceRange, "10.10.10.10"),
withLabel(label.TraefikFrontendWhiteListUseXForwardedFor, "true"),
withLabel(label.TraefikFrontendRequestHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type:application/json; charset=utf-8"), withLabel(label.TraefikFrontendRequestHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type:application/json; charset=utf-8"),
withLabel(label.TraefikFrontendResponseHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type:application/json; charset=utf-8"), withLabel(label.TraefikFrontendResponseHeaders, "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type:application/json; charset=utf-8"),
@ -212,8 +213,9 @@ func TestBuildConfiguration(t *testing.T) {
"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
}, },
WhitelistSourceRange: []string{ WhiteList: &types.WhiteList{
"10.10.10.10", SourceRange: []string{"10.10.10.10"},
UseXForwardedFor: true,
}, },
Headers: &types.Headers{ Headers: &types.Headers{
CustomRequestHeaders: map[string]string{ CustomRequestHeaders: map[string]string{
@ -953,6 +955,75 @@ func TestGetServers(t *testing.T) {
} }
} }
func TestWhiteList(t *testing.T) {
testCases := []struct {
desc string
task state.Task
expected *types.WhiteList
}{
{
desc: "should return nil when no white list labels",
task: aTask("ID1",
withIP("10.10.10.10"),
withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))),
withDefaultStatus(),
),
expected: nil,
},
{
desc: "should return a struct when only range",
task: aTask("ID1",
withLabel(label.TraefikFrontendWhiteListSourceRange, "10.10.10.10"),
withIP("10.10.10.10"),
withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))),
withDefaultStatus(),
),
expected: &types.WhiteList{
SourceRange: []string{
"10.10.10.10",
},
UseXForwardedFor: false,
},
},
{
desc: "should return a struct when range and UseXForwardedFor",
task: aTask("ID1",
withLabel(label.TraefikFrontendWhiteListSourceRange, "10.10.10.10"),
withLabel(label.TraefikFrontendWhiteListUseXForwardedFor, "true"),
withIP("10.10.10.10"),
withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))),
withDefaultStatus(),
),
expected: &types.WhiteList{
SourceRange: []string{
"10.10.10.10",
},
UseXForwardedFor: true,
},
},
{
desc: "should return nil when only UseXForwardedFor",
task: aTask("ID1",
withLabel(label.TraefikFrontendWhiteListUseXForwardedFor, "true"),
withIP("10.10.10.10"),
withInfo("name1", withPorts(withPort("TCP", 80, "WEB"))),
withDefaultStatus(),
),
expected: nil,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actual := getWhiteList(test.task)
assert.Equal(t, test.expected, actual)
})
}
}
func TestGetRedirect(t *testing.T) { func TestGetRedirect(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string

View file

@ -63,12 +63,14 @@ func (p *Provider) buildConfiguration(services []rancherData) *types.Configurati
"getPassTLSCert": getFuncBool(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert), "getPassTLSCert": getFuncBool(label.TraefikFrontendPassTLSCert, label.DefaultPassTLSCert),
"getEntryPoints": getFuncSliceString(label.TraefikFrontendEntryPoints), "getEntryPoints": getFuncSliceString(label.TraefikFrontendEntryPoints),
"getBasicAuth": getFuncSliceString(label.TraefikFrontendAuthBasic), "getBasicAuth": getFuncSliceString(label.TraefikFrontendAuthBasic),
"getWhitelistSourceRange": getFuncSliceString(label.TraefikFrontendWhitelistSourceRange),
"getErrorPages": getErrorPages, "getErrorPages": getErrorPages,
"getRateLimit": getRateLimit, "getRateLimit": getRateLimit,
"getRedirect": getRedirect, "getRedirect": getRedirect,
"getHeaders": getHeaders, "getHeaders": getHeaders,
"getWhiteList": getWhiteList,
// TODO Deprecated [breaking]
"getWhitelistSourceRange": getFuncSliceString(label.TraefikFrontendWhitelistSourceRange),
} }
// filter services // filter services
@ -271,6 +273,19 @@ func getServers(service rancherData) map[string]types.Server {
return servers return servers
} }
func getWhiteList(service rancherData) *types.WhiteList {
ranges := label.GetSliceStringValue(service.Labels, label.TraefikFrontendWhiteListSourceRange)
if len(ranges) > 0 {
return &types.WhiteList{
SourceRange: ranges,
UseXForwardedFor: label.GetBoolValue(service.Labels, label.TraefikFrontendWhiteListUseXForwardedFor, false),
}
}
return nil
}
func getRedirect(service rancherData) *types.Redirect { func getRedirect(service rancherData) *types.Redirect {
permanent := label.GetBoolValue(service.Labels, label.TraefikFrontendRedirectPermanent, false) permanent := label.GetBoolValue(service.Labels, label.TraefikFrontendRedirectPermanent, false)

View file

@ -66,7 +66,8 @@ func TestProviderBuildConfiguration(t *testing.T) {
label.TraefikFrontendRedirectReplacement: "nope", label.TraefikFrontendRedirectReplacement: "nope",
label.TraefikFrontendRedirectPermanent: "true", label.TraefikFrontendRedirectPermanent: "true",
label.TraefikFrontendRule: "Host:traefik.io", label.TraefikFrontendRule: "Host:traefik.io",
label.TraefikFrontendWhitelistSourceRange: "10.10.10.10", label.TraefikFrontendWhiteListSourceRange: "10.10.10.10",
label.TraefikFrontendWhiteListUseXForwardedFor: "true",
label.TraefikFrontendRequestHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", label.TraefikFrontendRequestHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8",
label.TraefikFrontendResponseHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8", label.TraefikFrontendResponseHeaders: "Access-Control-Allow-Methods:POST,GET,OPTIONS || Content-type: application/json; charset=utf-8",
@ -128,9 +129,12 @@ func TestProviderBuildConfiguration(t *testing.T) {
"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/",
"test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0",
}, },
WhitelistSourceRange: []string{ WhiteList: &types.WhiteList{
SourceRange: []string{
"10.10.10.10", "10.10.10.10",
}, },
UseXForwardedFor: true,
},
Headers: &types.Headers{ Headers: &types.Headers{
CustomRequestHeaders: map[string]string{ CustomRequestHeaders: map[string]string{
"Access-Control-Allow-Methods": "POST,GET,OPTIONS", "Access-Control-Allow-Methods": "POST,GET,OPTIONS",
@ -1001,6 +1005,78 @@ func TestGetServers(t *testing.T) {
} }
} }
func TestWhiteList(t *testing.T) {
testCases := []struct {
desc string
service rancherData
expected *types.WhiteList
}{
{
desc: "should return nil when no white list labels",
service: rancherData{
Labels: map[string]string{},
Health: "healthy",
State: "active",
},
expected: nil,
},
{
desc: "should return a struct when only range",
service: rancherData{
Labels: map[string]string{
label.TraefikFrontendWhiteListSourceRange: "10.10.10.10",
},
Health: "healthy",
State: "active",
},
expected: &types.WhiteList{
SourceRange: []string{
"10.10.10.10",
},
UseXForwardedFor: false,
},
},
{
desc: "should return a struct when range and UseXForwardedFor",
service: rancherData{
Labels: map[string]string{
label.TraefikFrontendWhiteListSourceRange: "10.10.10.10",
label.TraefikFrontendWhiteListUseXForwardedFor: "true",
},
Health: "healthy",
State: "active",
},
expected: &types.WhiteList{
SourceRange: []string{
"10.10.10.10",
},
UseXForwardedFor: true,
},
},
{
desc: "should return nil when only UseXForwardedFor",
service: rancherData{
Labels: map[string]string{
label.TraefikFrontendWhiteListUseXForwardedFor: "true",
},
Health: "healthy",
State: "active",
},
expected: nil,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actual := getWhiteList(test.service)
assert.Equal(t, test.expected, actual)
})
}
}
func TestGetRedirect(t *testing.T) { func TestGetRedirect(t *testing.T) {
testCases := []struct { testCases := []struct {
desc string desc string

View file

@ -1,7 +1,6 @@
package server package server
import ( import (
"net"
"net/http" "net/http"
"os" "os"
@ -12,7 +11,7 @@ import (
// NewHeaderRewriter Create a header rewriter // NewHeaderRewriter Create a header rewriter
func NewHeaderRewriter(trustedIPs []string, insecure bool) (forward.ReqRewriter, error) { func NewHeaderRewriter(trustedIPs []string, insecure bool) (forward.ReqRewriter, error) {
IPs, err := whitelist.NewIP(trustedIPs, insecure) IPs, err := whitelist.NewIP(trustedIPs, insecure, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -38,14 +37,7 @@ type headerRewriter struct {
} }
func (h *headerRewriter) Rewrite(req *http.Request) { func (h *headerRewriter) Rewrite(req *http.Request) {
clientIP, _, err := net.SplitHostPort(req.RemoteAddr) authorized, _, err := h.ips.IsAuthorized(req)
if err != nil {
log.Error(err)
h.secureRewriter.Rewrite(req)
return
}
authorized, _, err := h.ips.Contains(clientIP)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
h.secureRewriter.Rewrite(req) h.secureRewriter.Rewrite(req)

View file

@ -326,8 +326,8 @@ func (s *Server) setupServerEntryPoint(newServerEntryPointName string, newServer
} }
serverMiddlewares = append(serverMiddlewares, s.globalConfiguration.API.StatsRecorder) serverMiddlewares = append(serverMiddlewares, s.globalConfiguration.API.StatsRecorder)
} }
} }
if s.globalConfiguration.EntryPoints[newServerEntryPointName].Auth != nil { if s.globalConfiguration.EntryPoints[newServerEntryPointName].Auth != nil {
authMiddleware, err := mauth.NewAuthenticator(s.globalConfiguration.EntryPoints[newServerEntryPointName].Auth, s.tracingMiddleware) authMiddleware, err := mauth.NewAuthenticator(s.globalConfiguration.EntryPoints[newServerEntryPointName].Auth, s.tracingMiddleware)
if err != nil { if err != nil {
@ -336,17 +336,22 @@ func (s *Server) setupServerEntryPoint(newServerEntryPointName string, newServer
serverMiddlewares = append(serverMiddlewares, s.wrapNegroniHandlerWithAccessLog(authMiddleware, fmt.Sprintf("Auth for entrypoint %s", newServerEntryPointName))) serverMiddlewares = append(serverMiddlewares, s.wrapNegroniHandlerWithAccessLog(authMiddleware, fmt.Sprintf("Auth for entrypoint %s", newServerEntryPointName)))
serverInternalMiddlewares = append(serverInternalMiddlewares, authMiddleware) serverInternalMiddlewares = append(serverInternalMiddlewares, authMiddleware)
} }
if s.globalConfiguration.EntryPoints[newServerEntryPointName].Compress { if s.globalConfiguration.EntryPoints[newServerEntryPointName].Compress {
serverMiddlewares = append(serverMiddlewares, &middlewares.Compress{}) serverMiddlewares = append(serverMiddlewares, &middlewares.Compress{})
} }
if len(s.globalConfiguration.EntryPoints[newServerEntryPointName].WhitelistSourceRange) > 0 {
ipWhitelistMiddleware, err := middlewares.NewIPWhitelister(s.globalConfiguration.EntryPoints[newServerEntryPointName].WhitelistSourceRange) ipWhitelistMiddleware, err := buildIPWhiteLister(
s.globalConfiguration.EntryPoints[newServerEntryPointName].WhiteList,
s.globalConfiguration.EntryPoints[newServerEntryPointName].WhitelistSourceRange)
if err != nil { if err != nil {
log.Fatal("Error starting server: ", err) log.Fatal("Error starting server: ", err)
} }
if ipWhitelistMiddleware != nil {
serverMiddlewares = append(serverMiddlewares, s.wrapNegroniHandlerWithAccessLog(ipWhitelistMiddleware, fmt.Sprintf("ipwhitelister for entrypoint %s", newServerEntryPointName))) serverMiddlewares = append(serverMiddlewares, s.wrapNegroniHandlerWithAccessLog(ipWhitelistMiddleware, fmt.Sprintf("ipwhitelister for entrypoint %s", newServerEntryPointName)))
serverInternalMiddlewares = append(serverInternalMiddlewares, ipWhitelistMiddleware) serverInternalMiddlewares = append(serverInternalMiddlewares, ipWhitelistMiddleware)
} }
newSrv, listener, err := s.prepareServer(newServerEntryPointName, s.globalConfiguration.EntryPoints[newServerEntryPointName], newServerEntryPoint.httpRouter, serverMiddlewares, serverInternalMiddlewares) newSrv, listener, err := s.prepareServer(newServerEntryPointName, s.globalConfiguration.EntryPoints[newServerEntryPointName], newServerEntryPoint.httpRouter, serverMiddlewares, serverInternalMiddlewares)
if err != nil { if err != nil {
log.Fatal("Error preparing server: ", err) log.Fatal("Error preparing server: ", err)
@ -794,7 +799,7 @@ func (s *Server) prepareServer(entryPointName string, entryPoint *configuration.
} }
if entryPoint.ProxyProtocol != nil { if entryPoint.ProxyProtocol != nil {
IPs, err := whitelist.NewIP(entryPoint.ProxyProtocol.TrustedIPs, entryPoint.ProxyProtocol.Insecure) IPs, err := whitelist.NewIP(entryPoint.ProxyProtocol.TrustedIPs, entryPoint.ProxyProtocol.Insecure, false)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("error creating whitelist: %s", err) return nil, nil, fmt.Errorf("error creating whitelist: %s", err)
} }
@ -1137,13 +1142,16 @@ func (s *Server) loadConfig(configurations types.Configurations, globalConfigura
n.Use(middlewares.NewBackendMetricsMiddleware(s.metricsRegistry, frontend.Backend)) n.Use(middlewares.NewBackendMetricsMiddleware(s.metricsRegistry, frontend.Backend))
} }
ipWhitelistMiddleware, err := configureIPWhitelistMiddleware(frontend.WhitelistSourceRange) ipWhitelistMiddleware, err := buildIPWhiteLister(frontend.WhiteList, frontend.WhitelistSourceRange)
if err != nil { if err != nil {
log.Errorf("Error creating IP Whitelister: %s", err) log.Errorf("Error creating IP Whitelister: %s", err)
} else if ipWhitelistMiddleware != nil { } else if ipWhitelistMiddleware != nil {
ipWhitelistMiddleware = s.wrapNegroniHandlerWithAccessLog(ipWhitelistMiddleware, fmt.Sprintf("ipwhitelister for %s", frontendName)) n.Use(
n.Use(s.tracingMiddleware.NewNegroniHandlerWrapper("IP whitelist", ipWhitelistMiddleware, false)) s.tracingMiddleware.NewNegroniHandlerWrapper(
log.Infof("Configured IP Whitelists: %s", frontend.WhitelistSourceRange) "IP whitelist",
s.wrapNegroniHandlerWithAccessLog(ipWhitelistMiddleware, fmt.Sprintf("ipwhitelister for %s", frontendName)),
false))
log.Debugf("Configured IP Whitelists: %s", frontend.WhitelistSourceRange)
} }
if frontend.Redirect != nil && entryPointName != frontend.Redirect.EntryPoint { if frontend.Redirect != nil && entryPointName != frontend.Redirect.EntryPoint {
@ -1256,18 +1264,13 @@ func (s *Server) configureLBServers(lb healthcheck.LoadBalancer, config *types.C
return nil return nil
} }
func configureIPWhitelistMiddleware(whitelistSourceRanges []string) (negroni.Handler, error) { func buildIPWhiteLister(whiteList *types.WhiteList, wlRange []string) (*middlewares.IPWhiteLister, error) {
if len(whitelistSourceRanges) > 0 { if whiteList != nil &&
ipSourceRanges := whitelistSourceRanges len(whiteList.SourceRange) > 0 {
ipWhitelistMiddleware, err := middlewares.NewIPWhitelister(ipSourceRanges) return middlewares.NewIPWhiteLister(whiteList.SourceRange, whiteList.UseXForwardedFor)
} else if len(wlRange) > 0 {
if err != nil { return middlewares.NewIPWhiteLister(wlRange, false)
return nil, err
} }
return ipWhitelistMiddleware, nil
}
return nil, nil return nil, nil
} }

View file

@ -571,48 +571,75 @@ func TestServerParseHealthCheckOptions(t *testing.T) {
} }
} }
func TestNewServerWithWhitelistSourceRange(t *testing.T) { func TestBuildIPWhiteLister(t *testing.T) {
cases := []struct { testCases := []struct {
desc string desc string
whitelistStrings []string whitelistSourceRange []string
whiteList *types.WhiteList
middlewareConfigured bool middlewareConfigured bool
errMessage string errMessage string
}{ }{
{ {
desc: "no whitelists configued", desc: "no whitelists configured",
whitelistStrings: nil, whitelistSourceRange: nil,
middlewareConfigured: false, middlewareConfigured: false,
errMessage: "", errMessage: "",
}, { },
desc: "whitelists configued", {
whitelistStrings: []string{ desc: "whitelists configured (deprecated)",
whitelistSourceRange: []string{
"1.2.3.4/24", "1.2.3.4/24",
"fe80::/16", "fe80::/16",
}, },
middlewareConfigured: true, middlewareConfigured: true,
errMessage: "", errMessage: "",
}, { },
desc: "invalid whitelists configued", {
whitelistStrings: []string{ desc: "invalid whitelists configured (deprecated)",
whitelistSourceRange: []string{
"foo", "foo",
}, },
middlewareConfigured: false, middlewareConfigured: false,
errMessage: "parsing CIDR whitelist [foo]: parsing CIDR whitelist <nil>: invalid CIDR address: foo", errMessage: "parsing CIDR whitelist [foo]: parsing CIDR white list <nil>: invalid CIDR address: foo",
},
{
desc: "whitelists configured",
whiteList: &types.WhiteList{
SourceRange: []string{
"1.2.3.4/24",
"fe80::/16",
},
UseXForwardedFor: false,
},
middlewareConfigured: true,
errMessage: "",
},
{
desc: "invalid whitelists configured (deprecated)",
whiteList: &types.WhiteList{
SourceRange: []string{
"foo",
},
UseXForwardedFor: false,
},
middlewareConfigured: false,
errMessage: "parsing CIDR whitelist [foo]: parsing CIDR white list <nil>: invalid CIDR address: foo",
}, },
} }
for _, tc := range cases { for _, test := range testCases {
tc := tc test := test
t.Run(tc.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
t.Parallel() t.Parallel()
middleware, err := configureIPWhitelistMiddleware(tc.whitelistStrings)
if tc.errMessage != "" { middleware, err := buildIPWhiteLister(test.whiteList, test.whitelistSourceRange)
require.EqualError(t, err, tc.errMessage)
if test.errMessage != "" {
require.EqualError(t, err, test.errMessage)
} else { } else {
assert.NoError(t, err) assert.NoError(t, err)
if tc.middlewareConfigured { if test.middlewareConfigured {
require.NotNil(t, middleware, "not expected middleware to be configured") require.NotNil(t, middleware, "not expected middleware to be configured")
} else { } else {
require.Nil(t, middleware, "expected middleware to be configured") require.Nil(t, middleware, "expected middleware to be configured")

View file

@ -66,17 +66,19 @@
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{ $whitelistSourceRange := getWhitelistSourceRange $service.Attributes }}
{{if $whitelistSourceRange }}
whitelistSourceRange = [{{range $whitelistSourceRange }}
"{{.}}",
{{end}}]
{{end}}
basicAuth = [{{range getBasicAuth $service.Attributes }} basicAuth = [{{range getBasicAuth $service.Attributes }}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{ $whitelist := getWhiteList $service.Attributes }}
{{if $whitelist }}
[frontends."frontend-{{ $service.ServiceName }}".whiteList]
sourceRange = [{{range $whitelist.SourceRange }}
"{{.}}",
{{end}}]
useXForwardedFor = {{ $whitelist.UseXForwardedFor }}
{{end}}
{{ $redirect := getRedirect $service.Attributes }} {{ $redirect := getRedirect $service.Attributes }}
{{if $redirect }} {{if $redirect }}
[frontends."frontend-{{ $service.ServiceName }}".redirect] [frontends."frontend-{{ $service.ServiceName }}".redirect]

View file

@ -67,17 +67,19 @@
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{ $whitelistSourceRange := getWhitelistSourceRange $container.SegmentLabels }}
{{if $whitelistSourceRange }}
whitelistSourceRange = [{{range $whitelistSourceRange }}
"{{.}}",
{{end}}]
{{end}}
basicAuth = [{{range getBasicAuth $container.SegmentLabels }} basicAuth = [{{range getBasicAuth $container.SegmentLabels }}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{ $whitelist := getWhiteList $container.SegmentLabels }}
{{if $whitelist }}
[frontends."frontend-{{ $frontendName }}".whiteList]
sourceRange = [{{range $whitelist.SourceRange }}
"{{.}}",
{{end}}]
useXForwardedFor = {{ $whitelist.UseXForwardedFor }}
{{end}}
{{ $redirect := getRedirect $container.SegmentLabels }} {{ $redirect := getRedirect $container.SegmentLabels }}
{{if $redirect }} {{if $redirect }}
[frontends."frontend-{{ $frontendName }}".redirect] [frontends."frontend-{{ $frontendName }}".redirect]

View file

@ -66,17 +66,18 @@
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{ $whitelistSourceRange := getWhitelistSourceRange $instance }}
{{if $whitelistSourceRange }}
whitelistSourceRange = [{{range $whitelistSourceRange }}
"{{.}}",
{{end}}]
{{end}}
basicAuth = [{{range getBasicAuth $instance }} basicAuth = [{{range getBasicAuth $instance }}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{ $whitelist := getWhiteList $instance }}
{{if $whitelist }}
[frontends."frontend-{{ $serviceName }}".whiteList]
sourceRange = [{{range $whitelist.SourceRange }}
"{{.}}",
{{end}}]
useXForwardedFor = {{ $whitelist.UseXForwardedFor }}
{{end}}
{{ $redirect := getRedirect $instance }} {{ $redirect := getRedirect $instance }}
{{if $redirect }} {{if $redirect }}

View file

@ -56,9 +56,13 @@
"{{.}}", "{{.}}",
{{end}}] {{end}}]
whitelistSourceRange = [{{range $frontend.WhitelistSourceRange }} {{if $frontend.WhiteList }}
[frontends."{{ $frontendName }}".whiteList]
sourceRange = [{{range $frontend.WhiteList.SourceRange }}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
useXForwardedFor = {{ $frontend.WhiteList.UseXForwardedFor }}
{{end}}
{{if $frontend.Redirect }} {{if $frontend.Redirect }}
[frontends."{{ $frontendName }}".redirect] [frontends."{{ $frontendName }}".redirect]

View file

@ -66,17 +66,19 @@
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{ $whitelistSourceRange := getWhitelistSourceRange $frontend }}
{{if $whitelistSourceRange }}
whitelistSourceRange = [{{range $whitelistSourceRange }}
"{{.}}",
{{end}}]
{{end}}
basicAuth = [{{range getBasicAuth $frontend }} basicAuth = [{{range getBasicAuth $frontend }}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{ $whitelist := getWhiteList $frontend }}
{{if $whitelist }}
[frontends."{{ $frontendName }}".whiteList]
sourceRange = [{{range $whitelist.SourceRange }}
"{{.}}",
{{end}}]
useXForwardedFor = {{ $whitelist.UseXForwardedFor }}
{{end}}
{{ $redirect := getRedirect $frontend }} {{ $redirect := getRedirect $frontend }}
{{if $redirect }} {{if $redirect }}
[frontends."{{ $frontendName }}".redirect] [frontends."{{ $frontendName }}".redirect]

View file

@ -73,17 +73,19 @@
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{ $whitelistSourceRange := getWhitelistSourceRange $app $serviceName }}
{{if $whitelistSourceRange }}
whitelistSourceRange = [{{range $whitelistSourceRange }}
"{{.}}",
{{end}}]
{{end}}
basicAuth = [{{range getBasicAuth $app $serviceName }} basicAuth = [{{range getBasicAuth $app $serviceName }}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{ $whitelist := getWhiteList $app $serviceName }}
{{if $whitelist }}
[frontends."{{ $frontendName }}".whiteList]
sourceRange = [{{range $whitelist.SourceRange }}
"{{.}}",
{{end}}]
useXForwardedFor = {{ $whitelist.UseXForwardedFor }}
{{end}}
{{ $redirect := getRedirect $app $serviceName }} {{ $redirect := getRedirect $app $serviceName }}
{{if $redirect }} {{if $redirect }}
[frontends."{{ $frontendName }}".redirect] [frontends."{{ $frontendName }}".redirect]

View file

@ -69,17 +69,19 @@
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{ $whitelistSourceRange := getWhitelistSourceRange $app }}
{{if $whitelistSourceRange }}
whitelistSourceRange = [{{range $whitelistSourceRange }}
"{{.}}",
{{end}}]
{{end}}
basicAuth = [{{range getBasicAuth $app }} basicAuth = [{{range getBasicAuth $app }}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{ $whitelist := getWhiteList $app }}
{{if $whitelist }}
[frontends."frontend-{{ $frontendName }}".whiteList]
sourceRange = [{{range $whitelist.SourceRange }}
"{{.}}",
{{end}}]
useXForwardedFor = {{ $whitelist.UseXForwardedFor }}
{{end}}
{{ $redirect := getRedirect $app }} {{ $redirect := getRedirect $app }}
{{if $redirect }} {{if $redirect }}
[frontends."frontend-{{ $frontendName }}".redirect] [frontends."frontend-{{ $frontendName }}".redirect]

View file

@ -67,17 +67,19 @@
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{ $whitelistSourceRange := getWhitelistSourceRange $service }}
{{if $whitelistSourceRange }}
whitelistSourceRange = [{{range $whitelistSourceRange }}
"{{.}}",
{{end}}]
{{end}}
basicAuth = [{{range getBasicAuth $service }} basicAuth = [{{range getBasicAuth $service }}
"{{.}}", "{{.}}",
{{end}}] {{end}}]
{{ $whitelist := getWhiteList $service }}
{{if $whitelist }}
[frontends."frontend-{{ $frontendName }}".whiteList]
sourceRange = [{{range $whitelist.SourceRange }}
"{{.}}",
{{end}}]
useXForwardedFor = {{ $whitelist.UseXForwardedFor }}
{{end}}
{{ $redirect := getRedirect $service }} {{ $redirect := getRedirect $service }}
{{if $redirect }} {{if $redirect }}
[frontends."frontend-{{ $frontendName }}".redirect] [frontends."frontend-{{ $frontendName }}".redirect]

View file

@ -61,6 +61,12 @@ type Buffering struct {
RetryExpression string `json:"retryExpression,omitempty"` RetryExpression string `json:"retryExpression,omitempty"`
} }
// WhiteList contains white list configuration.
type WhiteList struct {
SourceRange []string `json:"sourceRange,omitempty"`
UseXForwardedFor bool `json:"useXForwardedFor,omitempty" export:"true"`
}
// HealthCheck holds HealthCheck configuration // HealthCheck holds HealthCheck configuration
type HealthCheck struct { type HealthCheck struct {
Path string `json:"path,omitempty"` Path string `json:"path,omitempty"`
@ -89,7 +95,7 @@ type ServerRoute struct {
ReplacePathRegex string ReplacePathRegex string
} }
//ErrorPage holds custom error page configuration // ErrorPage holds custom error page configuration
type ErrorPage struct { type ErrorPage struct {
Status []string `json:"status,omitempty"` Status []string `json:"status,omitempty"`
Backend string `json:"backend,omitempty"` Backend string `json:"backend,omitempty"`
@ -172,7 +178,8 @@ type Frontend struct {
PassTLSCert bool `json:"passTLSCert,omitempty"` PassTLSCert bool `json:"passTLSCert,omitempty"`
Priority int `json:"priority"` Priority int `json:"priority"`
BasicAuth []string `json:"basicAuth"` BasicAuth []string `json:"basicAuth"`
WhitelistSourceRange []string `json:"whitelistSourceRange,omitempty"` WhitelistSourceRange []string `json:"whitelistSourceRange,omitempty"` // Deprecated
WhiteList *WhiteList `json:"whiteList,omitempty"`
Headers *Headers `json:"headers,omitempty"` Headers *Headers `json:"headers,omitempty"`
Errors map[string]*ErrorPage `json:"errors,omitempty"` Errors map[string]*ErrorPage `json:"errors,omitempty"`
RateLimit *RateLimit `json:"ratelimit,omitempty"` RateLimit *RateLimit `json:"ratelimit,omitempty"`
@ -547,7 +554,7 @@ func NewHTTPCodeRanges(strBlocks []string) (HTTPCodeRanges, error) {
var blocks HTTPCodeRanges var blocks HTTPCodeRanges
for _, block := range strBlocks { for _, block := range strBlocks {
codes := strings.Split(block, "-") codes := strings.Split(block, "-")
//if only a single HTTP code was configured, assume the best and create the correct configuration on the user's behalf // if only a single HTTP code was configured, assume the best and create the correct configuration on the user's behalf
if len(codes) == 1 { if len(codes) == 1 {
codes = append(codes, codes[0]) codes = append(codes, codes[0])
} }

View file

@ -3,36 +3,45 @@ package whitelist
import ( import (
"fmt" "fmt"
"net" "net"
"net/http"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
const (
// XForwardedFor Header name
XForwardedFor = "X-Forwarded-For"
)
// IP allows to check that addresses are in a white list // IP allows to check that addresses are in a white list
type IP struct { type IP struct {
whiteListsIPs []*net.IP whiteListsIPs []*net.IP
whiteListsNet []*net.IPNet whiteListsNet []*net.IPNet
insecure bool insecure bool
useXForwardedFor bool
} }
// NewIP builds a new IP given a list of CIDR-Strings to whitelist // NewIP builds a new IP given a list of CIDR-Strings to white list
func NewIP(whitelistStrings []string, insecure bool) (*IP, error) { func NewIP(whiteList []string, insecure bool, useXForwardedFor bool) (*IP, error) {
if len(whitelistStrings) == 0 && !insecure { if len(whiteList) == 0 && !insecure {
return nil, errors.New("no white list provided") return nil, errors.New("no white list provided")
} }
ip := IP{insecure: insecure} ip := IP{
insecure: insecure,
useXForwardedFor: useXForwardedFor,
}
if !insecure { if !insecure {
for _, whitelistString := range whitelistStrings { for _, ipMask := range whiteList {
ipAddr := net.ParseIP(whitelistString) if ipAddr := net.ParseIP(ipMask); ipAddr != nil {
if ipAddr != nil {
ip.whiteListsIPs = append(ip.whiteListsIPs, &ipAddr) ip.whiteListsIPs = append(ip.whiteListsIPs, &ipAddr)
} else { } else {
_, whitelist, err := net.ParseCIDR(whitelistString) _, ipAddr, err := net.ParseCIDR(ipMask)
if err != nil { if err != nil {
return nil, fmt.Errorf("parsing CIDR whitelist %s: %v", whitelist, err) return nil, fmt.Errorf("parsing CIDR white list %s: %v", ipAddr, err)
} }
ip.whiteListsNet = append(ip.whiteListsNet, whitelist) ip.whiteListsNet = append(ip.whiteListsNet, ipAddr)
} }
} }
} }
@ -40,13 +49,38 @@ func NewIP(whitelistStrings []string, insecure bool) (*IP, error) {
return &ip, nil return &ip, nil
} }
// Contains checks if provided address is in the white list // IsAuthorized checks if provided request is authorized by the white list
func (ip *IP) Contains(addr string) (bool, net.IP, error) { func (ip *IP) IsAuthorized(req *http.Request) (bool, net.IP, error) {
if ip.insecure { if ip.insecure {
return true, nil, nil return true, nil, nil
} }
ipAddr, err := ipFromRemoteAddr(addr) if ip.useXForwardedFor {
xFFs := req.Header[XForwardedFor]
if len(xFFs) > 1 {
for _, xFF := range xFFs {
ok, i, err := ip.contains(parseHost(xFF))
if err != nil {
return false, nil, err
}
if ok {
return ok, i, nil
}
}
}
}
host, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
return false, nil, err
}
return ip.contains(host)
}
// contains checks if provided address is in the white list
func (ip *IP) contains(addr string) (bool, net.IP, error) {
ipAddr, err := parseIP(addr)
if err != nil { if err != nil {
return false, nil, fmt.Errorf("unable to parse address: %s: %s", addr, err) return false, nil, fmt.Errorf("unable to parse address: %s: %s", addr, err)
} }
@ -76,7 +110,7 @@ func (ip *IP) ContainsIP(addr net.IP) (bool, error) {
return false, nil return false, nil
} }
func ipFromRemoteAddr(addr string) (net.IP, error) { func parseIP(addr string) (net.IP, error) {
userIP := net.ParseIP(addr) userIP := net.ParseIP(addr)
if userIP == nil { if userIP == nil {
return nil, fmt.Errorf("can't parse IP from address %s", addr) return nil, fmt.Errorf("can't parse IP from address %s", addr)
@ -84,3 +118,11 @@ func ipFromRemoteAddr(addr string) (net.IP, error) {
return userIP, nil return userIP, nil
} }
func parseHost(addr string) string {
host, _, err := net.SplitHostPort(addr)
if err != nil {
return addr
}
return host
}

View file

@ -2,55 +2,151 @@ package whitelist
import ( import (
"net" "net"
"net/http"
"net/http/httptest"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestIsAuthorized(t *testing.T) {
testCases := []struct {
desc string
whiteList []string
allowXForwardedFor bool
remoteAddr string
xForwardedForValues []string
expected bool
}{
{
desc: "allow UseXForwardedFor, remoteAddr not in range, UseXForwardedFor in range",
whiteList: []string{"1.2.3.4/24"},
allowXForwardedFor: true,
remoteAddr: "10.2.3.1:123",
xForwardedForValues: []string{"1.2.3.1", "10.2.3.1"},
expected: true,
},
{
desc: "allow UseXForwardedFor, remoteAddr in range, UseXForwardedFor in range",
whiteList: []string{"1.2.3.4/24"},
allowXForwardedFor: true,
remoteAddr: "1.2.3.1:123",
xForwardedForValues: []string{"1.2.3.1", "10.2.3.1"},
expected: true,
},
{
desc: "allow UseXForwardedFor, remoteAddr in range, UseXForwardedFor not in range",
whiteList: []string{"1.2.3.4/24"},
allowXForwardedFor: true,
remoteAddr: "1.2.3.1:123",
xForwardedForValues: []string{"10.2.3.1", "10.2.3.1"},
expected: true,
},
{
desc: "allow UseXForwardedFor, remoteAddr not in range, UseXForwardedFor not in range",
whiteList: []string{"1.2.3.4/24"},
allowXForwardedFor: true,
remoteAddr: "10.2.3.1:123",
xForwardedForValues: []string{"10.2.3.1", "10.2.3.1"},
expected: false,
},
{
desc: "don't allow UseXForwardedFor, remoteAddr not in range, UseXForwardedFor in range",
whiteList: []string{"1.2.3.4/24"},
allowXForwardedFor: false,
remoteAddr: "10.2.3.1:123",
xForwardedForValues: []string{"1.2.3.1", "10.2.3.1"},
expected: false,
},
{
desc: "don't allow UseXForwardedFor, remoteAddr in range, UseXForwardedFor in range",
whiteList: []string{"1.2.3.4/24"},
allowXForwardedFor: false,
remoteAddr: "1.2.3.1:123",
xForwardedForValues: []string{"1.2.3.1", "10.2.3.1"},
expected: true,
},
{
desc: "don't allow UseXForwardedFor, remoteAddr in range, UseXForwardedFor not in range",
whiteList: []string{"1.2.3.4/24"},
allowXForwardedFor: false,
remoteAddr: "1.2.3.1:123",
xForwardedForValues: []string{"10.2.3.1", "10.2.3.1"},
expected: true,
},
{
desc: "don't allow UseXForwardedFor, remoteAddr not in range, UseXForwardedFor not in range",
whiteList: []string{"1.2.3.4/24"},
allowXForwardedFor: false,
remoteAddr: "10.2.3.1:123",
xForwardedForValues: []string{"10.2.3.1", "10.2.3.1"},
expected: false,
},
}
for _, test := range testCases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
req := NewRequest(test.remoteAddr, test.xForwardedForValues)
whiteLister, err := NewIP(test.whiteList, false, test.allowXForwardedFor)
require.NoError(t, err)
authorized, ips, err := whiteLister.IsAuthorized(req)
require.NoError(t, err)
assert.NotNil(t, ips)
assert.Equal(t, test.expected, authorized)
})
}
}
func TestNew(t *testing.T) { func TestNew(t *testing.T) {
cases := []struct { cases := []struct {
desc string desc string
whitelistStrings []string whiteList []string
expectedWhitelists []*net.IPNet expectedWhitelists []*net.IPNet
errMessage string errMessage string
}{ }{
{ {
desc: "nil whitelist", desc: "nil whitelist",
whitelistStrings: nil, whiteList: nil,
expectedWhitelists: nil, expectedWhitelists: nil,
errMessage: "no white list provided", errMessage: "no white list provided",
}, { }, {
desc: "empty whitelist", desc: "empty whitelist",
whitelistStrings: []string{}, whiteList: []string{},
expectedWhitelists: nil, expectedWhitelists: nil,
errMessage: "no white list provided", errMessage: "no white list provided",
}, { }, {
desc: "whitelist containing empty string", desc: "whitelist containing empty string",
whitelistStrings: []string{ whiteList: []string{
"1.2.3.4/24", "1.2.3.4/24",
"", "",
"fe80::/16", "fe80::/16",
}, },
expectedWhitelists: nil, expectedWhitelists: nil,
errMessage: "parsing CIDR whitelist <nil>: invalid CIDR address: ", errMessage: "parsing CIDR white list <nil>: invalid CIDR address: ",
}, { }, {
desc: "whitelist containing only an empty string", desc: "whitelist containing only an empty string",
whitelistStrings: []string{ whiteList: []string{
"", "",
}, },
expectedWhitelists: nil, expectedWhitelists: nil,
errMessage: "parsing CIDR whitelist <nil>: invalid CIDR address: ", errMessage: "parsing CIDR white list <nil>: invalid CIDR address: ",
}, { }, {
desc: "whitelist containing an invalid string", desc: "whitelist containing an invalid string",
whitelistStrings: []string{ whiteList: []string{
"foo", "foo",
}, },
expectedWhitelists: nil, expectedWhitelists: nil,
errMessage: "parsing CIDR whitelist <nil>: invalid CIDR address: foo", errMessage: "parsing CIDR white list <nil>: invalid CIDR address: foo",
}, { }, {
desc: "IPv4 & IPv6 whitelist", desc: "IPv4 & IPv6 whitelist",
whitelistStrings: []string{ whiteList: []string{
"1.2.3.4/24", "1.2.3.4/24",
"fe80::/16", "fe80::/16",
}, },
@ -61,7 +157,7 @@ func TestNew(t *testing.T) {
errMessage: "", errMessage: "",
}, { }, {
desc: "IPv4 only", desc: "IPv4 only",
whitelistStrings: []string{ whiteList: []string{
"127.0.0.1/8", "127.0.0.1/8",
}, },
expectedWhitelists: []*net.IPNet{ expectedWhitelists: []*net.IPNet{
@ -75,12 +171,12 @@ func TestNew(t *testing.T) {
test := test test := test
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
t.Parallel() t.Parallel()
whitelister, err := NewIP(test.whitelistStrings, false) whiteLister, err := NewIP(test.whiteList, false, false)
if test.errMessage != "" { if test.errMessage != "" {
require.EqualError(t, err, test.errMessage) require.EqualError(t, err, test.errMessage)
} else { } else {
require.NoError(t, err) require.NoError(t, err)
for index, actual := range whitelister.whiteListsNet { for index, actual := range whiteLister.whiteListsNet {
expected := test.expectedWhitelists[index] expected := test.expectedWhitelists[index]
assert.Equal(t, expected.IP, actual.IP) assert.Equal(t, expected.IP, actual.IP)
assert.Equal(t, expected.Mask.String(), actual.Mask.String()) assert.Equal(t, expected.Mask.String(), actual.Mask.String())
@ -99,9 +195,7 @@ func TestContainsIsAllowed(t *testing.T) {
}{ }{
{ {
desc: "IPv4", desc: "IPv4",
whitelistStrings: []string{ whitelistStrings: []string{"1.2.3.4/24"},
"1.2.3.4/24",
},
passIPs: []string{ passIPs: []string{
"1.2.3.1", "1.2.3.1",
"1.2.3.32", "1.2.3.32",
@ -117,12 +211,8 @@ func TestContainsIsAllowed(t *testing.T) {
}, },
{ {
desc: "IPv4 single IP", desc: "IPv4 single IP",
whitelistStrings: []string{ whitelistStrings: []string{"8.8.8.8"},
"8.8.8.8", passIPs: []string{"8.8.8.8"},
},
passIPs: []string{
"8.8.8.8",
},
rejectIPs: []string{ rejectIPs: []string{
"8.8.8.7", "8.8.8.7",
"8.8.8.9", "8.8.8.9",
@ -134,12 +224,8 @@ func TestContainsIsAllowed(t *testing.T) {
}, },
{ {
desc: "IPv4 Net single IP", desc: "IPv4 Net single IP",
whitelistStrings: []string{ whitelistStrings: []string{"8.8.8.8/32"},
"8.8.8.8/32", passIPs: []string{"8.8.8.8"},
},
passIPs: []string{
"8.8.8.8",
},
rejectIPs: []string{ rejectIPs: []string{
"8.8.8.7", "8.8.8.7",
"8.8.8.9", "8.8.8.9",
@ -151,10 +237,7 @@ func TestContainsIsAllowed(t *testing.T) {
}, },
{ {
desc: "multiple IPv4", desc: "multiple IPv4",
whitelistStrings: []string{ whitelistStrings: []string{"1.2.3.4/24", "8.8.8.8/8"},
"1.2.3.4/24",
"8.8.8.8/8",
},
passIPs: []string{ passIPs: []string{
"1.2.3.1", "1.2.3.1",
"1.2.3.32", "1.2.3.32",
@ -175,9 +258,7 @@ func TestContainsIsAllowed(t *testing.T) {
}, },
{ {
desc: "IPv6", desc: "IPv6",
whitelistStrings: []string{ whitelistStrings: []string{"2a03:4000:6:d080::/64"},
"2a03:4000:6:d080::/64",
},
passIPs: []string{ passIPs: []string{
"2a03:4000:6:d080::", "2a03:4000:6:d080::",
"2a03:4000:6:d080::1", "2a03:4000:6:d080::1",
@ -193,12 +274,8 @@ func TestContainsIsAllowed(t *testing.T) {
}, },
{ {
desc: "IPv6 single IP", desc: "IPv6 single IP",
whitelistStrings: []string{ whitelistStrings: []string{"2a03:4000:6:d080::42/128"},
"2a03:4000:6:d080::42/128", passIPs: []string{"2a03:4000:6:d080::42"},
},
passIPs: []string{
"2a03:4000:6:d080::42",
},
rejectIPs: []string{ rejectIPs: []string{
"2a03:4000:6:d080::1", "2a03:4000:6:d080::1",
"2a03:4000:6:d080:dead:beef:ffff:ffff", "2a03:4000:6:d080:dead:beef:ffff:ffff",
@ -207,10 +284,7 @@ func TestContainsIsAllowed(t *testing.T) {
}, },
{ {
desc: "multiple IPv6", desc: "multiple IPv6",
whitelistStrings: []string{ whitelistStrings: []string{"2a03:4000:6:d080::/64", "fe80::/16"},
"2a03:4000:6:d080::/64",
"fe80::/16",
},
passIPs: []string{ passIPs: []string{
"2a03:4000:6:d080::", "2a03:4000:6:d080::",
"2a03:4000:6:d080::1", "2a03:4000:6:d080::1",
@ -228,12 +302,7 @@ func TestContainsIsAllowed(t *testing.T) {
}, },
{ {
desc: "multiple IPv6 & IPv4", desc: "multiple IPv6 & IPv4",
whitelistStrings: []string{ whitelistStrings: []string{"2a03:4000:6:d080::/64", "fe80::/16", "1.2.3.4/24", "8.8.8.8/8"},
"2a03:4000:6:d080::/64",
"fe80::/16",
"1.2.3.4/24",
"8.8.8.8/8",
},
passIPs: []string{ passIPs: []string{
"2a03:4000:6:d080::", "2a03:4000:6:d080::",
"2a03:4000:6:d080::1", "2a03:4000:6:d080::1",
@ -264,9 +333,7 @@ func TestContainsIsAllowed(t *testing.T) {
}, },
{ {
desc: "broken IP-addresses", desc: "broken IP-addresses",
whitelistStrings: []string{ whitelistStrings: []string{"127.0.0.1/32"},
"127.0.0.1/32",
},
passIPs: nil, passIPs: nil,
}, },
} }
@ -276,23 +343,23 @@ func TestContainsIsAllowed(t *testing.T) {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
t.Parallel() t.Parallel()
whiteLister, err := NewIP(test.whitelistStrings, false) whiteLister, err := NewIP(test.whitelistStrings, false, false)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, whiteLister) require.NotNil(t, whiteLister)
for _, testIP := range test.passIPs { for _, testIP := range test.passIPs {
allowed, ip, err := whiteLister.Contains(testIP) allowed, ip, err := whiteLister.contains(testIP)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, ip, err) require.NotNil(t, ip, err)
assert.True(t, allowed, testIP+" should have passed "+test.desc) assert.Truef(t, allowed, "%s should have passed.", testIP)
} }
for _, testIP := range test.rejectIPs { for _, testIP := range test.rejectIPs {
allowed, ip, err := whiteLister.Contains(testIP) allowed, ip, err := whiteLister.contains(testIP)
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, ip, err) require.NotNil(t, ip, err)
assert.False(t, allowed, testIP+" should not have passed "+test.desc) assert.Falsef(t, allowed, "%s should not have passed.", testIP)
} }
}) })
} }
@ -300,7 +367,7 @@ func TestContainsIsAllowed(t *testing.T) {
func TestContainsInsecure(t *testing.T) { func TestContainsInsecure(t *testing.T) {
mustNewIP := func(whitelistStrings []string, insecure bool) *IP { mustNewIP := func(whitelistStrings []string, insecure bool) *IP {
ip, err := NewIP(whitelistStrings, insecure) ip, err := NewIP(whitelistStrings, insecure, false)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -338,7 +405,7 @@ func TestContainsInsecure(t *testing.T) {
t.Run(test.desc, func(t *testing.T) { t.Run(test.desc, func(t *testing.T) {
t.Parallel() t.Parallel()
ok, _, err := test.whiteLister.Contains(test.ip) ok, _, err := test.whiteLister.contains(test.ip)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, test.expected, ok) assert.Equal(t, test.expected, ok)
@ -355,13 +422,25 @@ func TestContainsBrokenIPs(t *testing.T) {
"\\&$§&/(", "\\&$§&/(",
} }
whiteLister, err := NewIP([]string{"1.2.3.4/24"}, false) whiteLister, err := NewIP([]string{"1.2.3.4/24"}, false, false)
require.NoError(t, err) require.NoError(t, err)
for _, testIP := range brokenIPs { for _, testIP := range brokenIPs {
_, ip, err := whiteLister.Contains(testIP) _, ip, err := whiteLister.contains(testIP)
assert.Error(t, err) assert.Error(t, err)
require.Nil(t, ip, err) require.Nil(t, ip, err)
} }
}
func NewRequest(remoteAddr string, xForwardedFor []string) *http.Request {
req := httptest.NewRequest(http.MethodGet, "http://example.com/foo", nil)
if len(remoteAddr) > 0 {
req.RemoteAddr = remoteAddr
}
if len(xForwardedFor) > 0 {
for _, xff := range xForwardedFor {
req.Header.Add(XForwardedFor, xff)
}
}
return req
} }