IP Whitelists for Frontend (with Docker- & Kubernetes-Provider Support)

This commit is contained in:
MaZderMind 2017-04-30 11:22:07 +02:00 committed by Ludovic Fernandez
parent 55f610422a
commit 5f0b215e90
16 changed files with 731 additions and 14 deletions

View file

@ -479,6 +479,13 @@ defaultEntryPoints = ["http", "https"]
backend = "backend1"
passHostHeader = true
priority = 10
# restrict access to this frontend to the specified list of IPv4/IPv6 CIDR Nets
# 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.
whitelistSourceRange = ["10.42.0.0/16", "152.89.1.33/32", "afed:be44::/16"]
entrypoints = ["https"] # overrides defaultEntryPoints
[frontends.frontend2.routes.test_1]
rule = "Host:{subdomain:[a-z]+}.localhost"
@ -867,7 +874,7 @@ Labels can be used on containers to override default behaviour:
- `traefik.frontend.priority=10`: override default frontend priority
- `traefik.frontend.entryPoints=http,https`: assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`.
- `traefik.frontend.auth.basic=test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0`: Sets a Basic Auth for that frontend with the users test:test and test2:test2
- `traefik.docker.network`: Set the docker network to use for connections to this container. 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). For instance when deploying docker `stack` from compose files, the compose defined networks will be prefixed with the `stack` name.
- `traefik.frontend.whitelistSourceRange: "1.2.3.0/24, fe80::/16"`: List of IP-Ranges which are allowed to access. 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.docker.network`: Set the docker network to use for connections to this container. 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). For instance when deploying docker `stack` from compose files, the compose defined networks will be prefixed with the `stack` name.
If several ports need to be exposed from a container, the services labels can be used
- `traefik.<service-name>.port=443`: create a service binding with frontend/backend using this port. Overrides `traefik.port`.
@ -1187,6 +1194,13 @@ Additionally, an annotation can be used on Kubernetes services to set the [circu
- `traefik.backend.circuitbreaker: <expression>`: set the circuit breaker expression for the backend (Default: nil).
As known from nginx when used as Kubernetes Ingress Controller, a List of IP-Ranges which are allowed to access can be configured by using an ingress annotation:
- `ingress.kubernetes.io/whitelist-source-range: "1.2.3.0/24, fe80::/16"`
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.
### Authentication
Is possible to add additional authentication annotations in the Ingress rule.

2
glide.lock generated
View file

@ -1,4 +1,4 @@
hash: 1aa32496b865dda72d76c7cba3458f1c2c467acf0b99aab4609323f109aa64f6
hash: e59e8244152a823cd3633fb09cdd583c4e5be78d7b50fb7047ba6b6a9ed5e8ec
updated: 2017-05-02T11:46:23.91434995-04:00
imports:
- name: cloud.google.com/go

View file

@ -0,0 +1,308 @@
package middlewares
import (
"fmt"
"net"
"net/http"
"net/http/httptest"
"testing"
"github.com/codegangsta/negroni"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewIPWhitelister(t *testing.T) {
cases := []struct {
desc string
whitelistStrings []string
expectedWhitelists []*net.IPNet
errMessage string
}{
{
desc: "nil whitelist",
whitelistStrings: nil,
expectedWhitelists: nil,
errMessage: "no whitelists provided",
}, {
desc: "empty whitelist",
whitelistStrings: []string{},
expectedWhitelists: nil,
errMessage: "no whitelists provided",
}, {
desc: "whitelist containing empty string",
whitelistStrings: []string{
"1.2.3.4/24",
"",
"fe80::/16",
},
expectedWhitelists: nil,
errMessage: "parsing CIDR whitelist <nil>: invalid CIDR address: ",
}, {
desc: "whitelist containing only an empty string",
whitelistStrings: []string{
"",
},
expectedWhitelists: nil,
errMessage: "parsing CIDR whitelist <nil>: invalid CIDR address: ",
}, {
desc: "whitelist containing an invalid string",
whitelistStrings: []string{
"foo",
},
expectedWhitelists: nil,
errMessage: "parsing CIDR whitelist <nil>: invalid CIDR address: foo",
}, {
desc: "IPv4 & IPv6 whitelist",
whitelistStrings: []string{
"1.2.3.4/24",
"fe80::/16",
},
expectedWhitelists: []*net.IPNet{
{IP: net.IPv4(1, 2, 3, 0).To4(), Mask: net.IPv4Mask(255, 255, 255, 0)},
{IP: net.ParseIP("fe80::"), Mask: net.IPMask(net.ParseIP("ffff::"))},
},
errMessage: "",
}, {
desc: "IPv4 only",
whitelistStrings: []string{
"127.0.0.1/8",
},
expectedWhitelists: []*net.IPNet{
{IP: net.IPv4(127, 0, 0, 0).To4(), Mask: net.IPv4Mask(255, 0, 0, 0)},
},
errMessage: "",
},
}
for _, test := range cases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
whitelister, err := NewIPWhitelister(test.whitelistStrings)
if test.errMessage != "" {
require.EqualError(t, err, test.errMessage)
} else {
require.NoError(t, err)
for index, actual := range whitelister.whitelists {
expected := test.expectedWhitelists[index]
assert.Equal(t, expected.IP, actual.IP)
assert.Equal(t, expected.Mask.String(), actual.Mask.String())
}
}
})
}
}
func TestIPWhitelisterHandle(t *testing.T) {
cases := []struct {
desc string
whitelistStrings []string
passIPs []string
rejectIPs []string
}{
{
desc: "IPv4",
whitelistStrings: []string{
"1.2.3.4/24",
},
passIPs: []string{
"1.2.3.1",
"1.2.3.32",
"1.2.3.156",
"1.2.3.255",
},
rejectIPs: []string{
"1.2.16.1",
"1.2.32.1",
"127.0.0.1",
"8.8.8.8",
},
},
{
desc: "IPv4 single IP",
whitelistStrings: []string{
"8.8.8.8/32",
},
passIPs: []string{
"8.8.8.8",
},
rejectIPs: []string{
"8.8.8.7",
"8.8.8.9",
"8.8.8.0",
"8.8.8.255",
"4.4.4.4",
"127.0.0.1",
},
},
{
desc: "multiple IPv4",
whitelistStrings: []string{
"1.2.3.4/24",
"8.8.8.8/8",
},
passIPs: []string{
"1.2.3.1",
"1.2.3.32",
"1.2.3.156",
"1.2.3.255",
"8.8.4.4",
"8.0.0.1",
"8.32.42.128",
"8.255.255.255",
},
rejectIPs: []string{
"1.2.16.1",
"1.2.32.1",
"127.0.0.1",
"4.4.4.4",
"4.8.8.8",
},
},
{
desc: "IPv6",
whitelistStrings: []string{
"2a03:4000:6:d080::/64",
},
passIPs: []string{
"[2a03:4000:6:d080::]",
"[2a03:4000:6:d080::1]",
"[2a03:4000:6:d080:dead:beef:ffff:ffff]",
"[2a03:4000:6:d080::42]",
},
rejectIPs: []string{
"[2a03:4000:7:d080::]",
"[2a03:4000:7:d080::1]",
"[fe80::]",
"[4242::1]",
},
},
{
desc: "IPv6 single IP",
whitelistStrings: []string{
"2a03:4000:6:d080::42/128",
},
passIPs: []string{
"[2a03:4000:6:d080::42]",
},
rejectIPs: []string{
"[2a03:4000:6:d080::1]",
"[2a03:4000:6:d080:dead:beef:ffff:ffff]",
"[2a03:4000:6:d080::43]",
},
},
{
desc: "multiple IPv6",
whitelistStrings: []string{
"2a03:4000:6:d080::/64",
"fe80::/16",
},
passIPs: []string{
"[2a03:4000:6:d080::]",
"[2a03:4000:6:d080::1]",
"[2a03:4000:6:d080:dead:beef:ffff:ffff]",
"[2a03:4000:6:d080::42]",
"[fe80::1]",
"[fe80:aa00:00bb:4232:ff00:eeee:00ff:1111]",
"[fe80::fe80]",
},
rejectIPs: []string{
"[2a03:4000:7:d080::]",
"[2a03:4000:7:d080::1]",
"[4242::1]",
},
},
{
desc: "multiple IPv6 & IPv4",
whitelistStrings: []string{
"2a03:4000:6:d080::/64",
"fe80::/16",
"1.2.3.4/24",
"8.8.8.8/8",
},
passIPs: []string{
"[2a03:4000:6:d080::]",
"[2a03:4000:6:d080::1]",
"[2a03:4000:6:d080:dead:beef:ffff:ffff]",
"[2a03:4000:6:d080::42]",
"[fe80::1]",
"[fe80:aa00:00bb:4232:ff00:eeee:00ff:1111]",
"[fe80::fe80]",
"1.2.3.1",
"1.2.3.32",
"1.2.3.156",
"1.2.3.255",
"8.8.4.4",
"8.0.0.1",
"8.32.42.128",
"8.255.255.255",
},
rejectIPs: []string{
"[2a03:4000:7:d080::]",
"[2a03:4000:7:d080::1]",
"[4242::1]",
"1.2.16.1",
"1.2.32.1",
"127.0.0.1",
"4.4.4.4",
"4.8.8.8",
},
},
{
desc: "broken IP-addresses",
whitelistStrings: []string{
"127.0.0.1/32",
},
passIPs: nil,
rejectIPs: []string{
"foo",
"10.0.0.350",
"fe:::80",
"",
"\\&$§&/(",
},
},
}
for _, test := range cases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
whitelister, err := NewIPWhitelister(test.whitelistStrings)
require.NoError(t, err)
require.NotNil(t, whitelister)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "traefik")
})
n := negroni.New(whitelister)
n.UseHandler(handler)
for _, testIP := range test.passIPs {
req, err := http.NewRequest("GET", "/", nil)
require.NoError(t, err)
req.RemoteAddr = testIP + ":2342"
recorder := httptest.NewRecorder()
n.ServeHTTP(recorder, req)
assert.Equal(t, http.StatusOK, recorder.Code, testIP+" should have passed "+test.desc)
assert.Contains(t, recorder.Body.String(), "traefik")
}
for _, testIP := range test.rejectIPs {
req, err := http.NewRequest("GET", "/", nil)
require.NoError(t, err)
req.RemoteAddr = testIP + ":2342"
recorder := httptest.NewRecorder()
n.ServeHTTP(recorder, req)
assert.Equal(t, http.StatusForbidden, recorder.Code, testIP+" should not have passed "+test.desc)
assert.NotContains(t, recorder.Body.String(), "traefik")
}
})
}
}

View file

@ -0,0 +1,83 @@
package middlewares
import (
"fmt"
"net"
"net/http"
"github.com/codegangsta/negroni"
"github.com/containous/traefik/log"
)
// IPWhitelister is a middleware that provides Checks of the Requesting IP against a set of Whitelists
type IPWhitelister struct {
handler negroni.Handler
whitelists []*net.IPNet
}
// NewIPWhitelister builds a new IPWhitelister given a list of CIDR-Strings to whitelist
func NewIPWhitelister(whitelistStrings []string) (*IPWhitelister, error) {
whitelister := IPWhitelister{}
if len(whitelistStrings) == 0 {
return nil, fmt.Errorf("no whitelists provided")
}
for _, whitelistString := range whitelistStrings {
_, whitelist, err := net.ParseCIDR(whitelistString)
if err != nil {
return nil, fmt.Errorf("parsing CIDR whitelist %s: %v", whitelist, err)
}
whitelister.whitelists = append(whitelister.whitelists, whitelist)
}
whitelister.handler = negroni.HandlerFunc(whitelister.handle)
log.Debugf("configured %u IP whitelists: %s", len(whitelister.whitelists), whitelister.whitelists)
return &whitelister, nil
}
func (whitelister *IPWhitelister) handle(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
remoteIP, err := ipFromRemoteAddr(r.RemoteAddr)
if err != nil {
log.Warnf("unable to parse remote-address from header: %s - rejecting", r.RemoteAddr)
reject(w)
return
}
for _, whitelist := range whitelister.whitelists {
if whitelist.Contains(*remoteIP) {
log.Debugf("source-IP %s matched whitelist %s - passing", remoteIP, whitelist)
next.ServeHTTP(w, r)
return
}
}
log.Debugf("source-IP %s matched none of the whitelists - rejecting", remoteIP)
reject(w)
return
}
func reject(w http.ResponseWriter) {
statusCode := http.StatusForbidden
w.WriteHeader(statusCode)
w.Write([]byte(http.StatusText(statusCode)))
}
func ipFromRemoteAddr(addr string) (*net.IP, error) {
ip, _, err := net.SplitHostPort(addr)
if err != nil {
return nil, fmt.Errorf("can't extract IP/Port from address %s: %s", addr, err)
}
userIP := net.ParseIP(ip)
if userIP == nil {
return nil, fmt.Errorf("can't parse IP from address %s", ip)
}
return &userIP, nil
}
func (whitelister *IPWhitelister) ServeHTTP(rw http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
whitelister.handler.ServeHTTP(rw, r, next)
}

View file

@ -272,6 +272,7 @@ func (p *Provider) loadDockerConfig(containersInspected []dockerData) *types.Con
"getServicePassHostHeader": p.getServicePassHostHeader,
"getServicePriority": p.getServicePriority,
"getServiceBackend": p.getServiceBackend,
"getWhitelistSourceRange": p.getWhitelistSourceRange,
}
// filter containers
filteredContainers := fun.Filter(func(container dockerData) bool {
@ -663,6 +664,15 @@ func (p *Provider) getPassHostHeader(container dockerData) string {
return "true"
}
func (p *Provider) getWhitelistSourceRange(container dockerData) []string {
var whitelistSourceRange []string
if whitelistSourceRangeLabel, err := getLabel(container, "traefik.frontend.whitelistSourceRange"); err == nil {
whitelistSourceRange = provider.SplitAndTrimString(whitelistSourceRangeLabel)
}
return whitelistSourceRange
}
func (p *Provider) getPriority(container dockerData) string {
if priority, err := getLabel(container, "traefik.frontend.priority"); err == nil {
return priority

View file

@ -400,6 +400,68 @@ func TestDockerGetPassHostHeader(t *testing.T) {
}
}
func TestDockerGetWhitelistSourceRange(t *testing.T) {
containers := []struct {
desc string
container docker.ContainerJSON
expected []string
}{
{
desc: "no whitelist-label",
container: containerJSON(),
expected: nil,
},
{
desc: "whitelist-label with empty string",
container: containerJSON(labels(map[string]string{
"traefik.frontend.whitelistSourceRange": "",
})),
expected: nil,
},
{
desc: "whitelist-label with IPv4 mask",
container: containerJSON(labels(map[string]string{
"traefik.frontend.whitelistSourceRange": "1.2.3.4/16",
})),
expected: []string{
"1.2.3.4/16",
},
},
{
desc: "whitelist-label with IPv6 mask",
container: containerJSON(labels(map[string]string{
"traefik.frontend.whitelistSourceRange": "fe80::/16",
})),
expected: []string{
"fe80::/16",
},
},
{
desc: "whitelist-label with multiple masks",
container: containerJSON(labels(map[string]string{
"traefik.frontend.whitelistSourceRange": "1.1.1.1/24, 1234:abcd::42/32",
})),
expected: []string{
"1.1.1.1/24",
"1234:abcd::42/32",
},
},
}
for _, e := range containers {
e := e
t.Run(e.desc, func(t *testing.T) {
t.Parallel()
dockerData := parseContainer(e.container)
provider := &Provider{}
actual := provider.getWhitelistSourceRange(dockerData)
if !reflect.DeepEqual(actual, e.expected) {
t.Errorf("expected %q, got %q", e.expected, actual)
}
})
}
}
func TestDockerGetLabel(t *testing.T) {
containers := []struct {
container docker.ContainerJSON

View file

@ -31,6 +31,8 @@ const (
ruleTypePathStrip = "PathStrip"
ruleTypePath = "Path"
ruleTypePathPrefix = "PathPrefix"
annotationKubernetesWhitelistSourceRange = "ingress.kubernetes.io/whitelist-source-range"
)
const traefikDefaultRealm = "traefik"
@ -171,6 +173,9 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
return nil, errors.New("no realm customization supported")
}
witelistSourceRangeAnnotation := i.Annotations[annotationKubernetesWhitelistSourceRange]
whitelistSourceRange := provider.SplitAndTrimString(witelistSourceRangeAnnotation)
if _, exists := templateObjects.Frontends[r.Host+pa.Path]; !exists {
basicAuthCreds, err := handleBasicAuthConfig(i, k8sClient)
if err != nil {
@ -182,6 +187,7 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
Routes: make(map[string]types.Route),
Priority: len(pa.Path),
BasicAuth: basicAuthCreds,
WhitelistSourceRange: whitelistSourceRange,
}
}
if len(r.Host) > 0 {

View file

@ -1523,6 +1523,35 @@ func TestIngressAnnotations(t *testing.T) {
},
},
},
{
ObjectMeta: v1.ObjectMeta{
Namespace: "testing",
Annotations: map[string]string{
"kubernetes.io/ingress.class": "traefik",
"ingress.kubernetes.io/whitelist-source-range": "1.1.1.1/24, 1234:abcd::42/32",
},
},
Spec: v1beta1.IngressSpec{
Rules: []v1beta1.IngressRule{
{
Host: "test",
IngressRuleValue: v1beta1.IngressRuleValue{
HTTP: &v1beta1.HTTPIngressRuleValue{
Paths: []v1beta1.HTTPIngressPath{
{
Path: "/whitelist-source-range",
Backend: v1beta1.IngressBackend{
ServiceName: "service1",
ServicePort: intstr.FromInt(80),
},
},
},
},
},
},
},
},
},
}
services := []*v1.Service{
{
@ -1613,6 +1642,19 @@ func TestIngressAnnotations(t *testing.T) {
Method: "wrr",
},
},
"test/whitelist-source-range": {
Servers: map[string]types.Server{
"http://example.com": {
URL: "http://example.com",
Weight: 1,
},
},
CircuitBreaker: nil,
LoadBalancer: &types.LoadBalancer{
Sticky: false,
Method: "wrr",
},
},
},
Frontends: map[string]*types.Frontend{
"foo/bar": {
@ -1655,6 +1697,23 @@ func TestIngressAnnotations(t *testing.T) {
},
BasicAuth: []string{"myUser:myEncodedPW"},
},
"test/whitelist-source-range": {
Backend: "test/whitelist-source-range",
PassHostHeader: true,
WhitelistSourceRange: []string{
"1.1.1.1/24",
"1234:abcd::42/32",
},
Priority: len("/whitelist-source-range"),
Routes: map[string]types.Route{
"/whitelist-source-range": {
Rule: "PathPrefix:/whitelist-source-range",
},
"test": {
Rule: "Host:test",
},
},
},
},
}

19
provider/string_util.go Normal file
View file

@ -0,0 +1,19 @@
package provider
import "strings"
// SplitAndTrimString splits separatedString at the comma character and trims each
// piece, filtering out empty pieces. Returns the list of pieces or nil if the input
// did not contain a non-empty piece.
func SplitAndTrimString(separatedString string) []string {
listOfStrings := strings.Split(separatedString, ",")
var trimmedListOfStrings []string
for _, s := range listOfStrings {
s = strings.TrimSpace(s)
if len(s) > 0 {
trimmedListOfStrings = append(trimmedListOfStrings, s)
}
}
return trimmedListOfStrings
}

View file

@ -0,0 +1,61 @@
package provider
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestSplitAndTrimString(t *testing.T) {
cases := []struct {
desc string
input string
expected []string
}{
{
desc: "empty string",
input: "",
expected: nil,
}, {
desc: "one piece",
input: "foo",
expected: []string{"foo"},
}, {
desc: "two pieces",
input: "foo,bar",
expected: []string{"foo", "bar"},
}, {
desc: "three pieces",
input: "foo,bar,zoo",
expected: []string{"foo", "bar", "zoo"},
}, {
desc: "two pieces with whitespace",
input: " foo , bar ",
expected: []string{"foo", "bar"},
}, {
desc: "consecutive commas",
input: " foo ,, bar ",
expected: []string{"foo", "bar"},
}, {
desc: "consecutive commas with witespace",
input: " foo , , bar ",
expected: []string{"foo", "bar"},
}, {
desc: "leading and trailing commas",
input: ",, foo , , bar,, , ",
expected: []string{"foo", "bar"},
}, {
desc: "no valid pieces",
input: ", , , ,, ,",
expected: nil,
},
}
for _, test := range cases {
test := test
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
actual := SplitAndTrimString(test.input)
assert.Equal(t, test.expected, actual)
})
}
}

View file

@ -716,6 +716,14 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
negroni.Use(metricsMiddlewareBackend)
}
}
ipWhitelistMiddleware, err := configureIPWhitelistMiddleware(frontend.WhitelistSourceRange)
if err != nil {
log.Fatalf("Error creating IP Whitelister: %s", err)
} else if ipWhitelistMiddleware != nil {
negroni.Use(ipWhitelistMiddleware)
log.Infof("Configured IP Whitelists: %s", frontend.WhitelistSourceRange)
}
if len(frontend.BasicAuth) > 0 {
users := types.Users{}
for _, user := range frontend.BasicAuth {
@ -770,6 +778,21 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
return serverEntryPoints, nil
}
func configureIPWhitelistMiddleware(whitelistSourceRanges []string) (negroni.Handler, error) {
if len(whitelistSourceRanges) > 0 {
ipSourceRanges := whitelistSourceRanges
ipWhitelistMiddleware, err := middlewares.NewIPWhitelister(ipSourceRanges)
if err != nil {
return nil, err
}
return ipWhitelistMiddleware, nil
}
return nil, nil
}
func (server *Server) wireFrontendBackend(serverRoute *serverRoute, handler http.Handler) {
// path replace - This needs to always be the very last on the handler chain (first in the order in this function)
// -- Replacing Path should happen at the very end of the Modifier chain, after all the Matcher+Modifiers ran

View file

@ -14,6 +14,8 @@ import (
"github.com/containous/traefik/testhelpers"
"github.com/containous/traefik/types"
"github.com/davecgh/go-spew/spew"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/vulcand/oxy/roundrobin"
)
@ -242,6 +244,57 @@ func TestServerParseHealthCheckOptions(t *testing.T) {
}
}
func TestNewServerWithWhitelistSourceRange(t *testing.T) {
cases := []struct {
desc string
whitelistStrings []string
middlewareConfigured bool
errMessage string
}{
{
desc: "no whitelists configued",
whitelistStrings: nil,
middlewareConfigured: false,
errMessage: "",
}, {
desc: "whitelists configued",
whitelistStrings: []string{
"1.2.3.4/24",
"fe80::/16",
},
middlewareConfigured: true,
errMessage: "",
}, {
desc: "invalid whitelists configued",
whitelistStrings: []string{
"foo",
},
middlewareConfigured: false,
errMessage: "parsing CIDR whitelist <nil>: invalid CIDR address: foo",
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.desc, func(t *testing.T) {
t.Parallel()
middleware, err := configureIPWhitelistMiddleware(tc.whitelistStrings)
if tc.errMessage != "" {
require.EqualError(t, err, tc.errMessage)
} else {
assert.NoError(t, err)
if tc.middlewareConfigured {
require.NotNil(t, middleware, "not expected middleware to be configured")
} else {
require.Nil(t, middleware, "expected middleware to be configured")
}
}
})
}
}
func TestServerLoadConfigEmptyBasicAuth(t *testing.T) {
globalConfig := GlobalConfiguration{
EntryPoints: EntryPoints{

View file

@ -43,6 +43,11 @@
[frontends."frontend-{{getServiceBackend $container $serviceName}}"]
backend = "backend-{{getServiceBackend $container $serviceName}}"
passHostHeader = {{getServicePassHostHeader $container $serviceName}}
{{if getWhitelistSourceRange $container}}
whitelistSourceRange = [{{range getWhitelistSourceRange $container}}
"{{.}}",
{{end}}]
{{end}}
priority = {{getServicePriority $container $serviceName}}
entryPoints = [{{range getServiceEntryPoints $container $serviceName}}
"{{.}}",
@ -57,6 +62,11 @@
[frontends."frontend-{{$frontend}}"]
backend = "backend-{{getBackend $container}}"
passHostHeader = {{getPassHostHeader $container}}
{{if getWhitelistSourceRange $container}}
whitelistSourceRange = [{{range getWhitelistSourceRange $container}}
"{{.}}",
{{end}}]
{{end}}
priority = {{getPriority $container}}
entryPoints = [{{range getEntryPoints $container}}
"{{.}}",

View file

@ -22,6 +22,9 @@
passHostHeader = {{$frontend.PassHostHeader}}
basicAuth = [{{range $frontend.BasicAuth}}
"{{.}}",
{{end}}]
whitelistSourceRange = [{{range $frontend.WhitelistSourceRange}}
"{{.}}",
{{end}}]
{{range $routeName, $route := $frontend.Routes}}
[frontends."{{$frontendName}}".routes."{{$routeName}}"]

View file

@ -62,6 +62,7 @@ type Frontend struct {
PassHostHeader bool `json:"passHostHeader,omitempty"`
Priority int `json:"priority"`
BasicAuth []string `json:"basicAuth"`
WhitelistSourceRange []string `json:"whitelistSourceRange,omitempty"`
}
// LoadBalancerMethod holds the method of load balancing to use.

View file

@ -15,9 +15,14 @@
</table>
</div>
<div data-bg-show="frontendCtrl.frontend.backend" class="panel-footer">
<span data-ng-repeat="entryPoint in frontendCtrl.frontend.entryPoints"><span class="label label-primary">{{entryPoint}}</span><span data-ng-hide="$last">&nbsp;</span></span>
<span data-ng-repeat="entryPoint in frontendCtrl.frontend.entryPoints">
<span class="label label-primary">{{entryPoint}}</span><span data-ng-hide="$last">&nbsp;</span>
</span>
<span class="label label-warning" role="button" data-toggle="collapse" href="#{{frontendCtrl.frontend.backend}}" aria-expanded="false">Backend:{{frontendCtrl.frontend.backend}}</span>
<span data-ng-show="frontendCtrl.frontend.passHostHeader" class="label label-warning">PassHostHeader</span>
<span data-ng-repeat="whitelistSourceRange in frontendCtrl.frontend.whitelistSourceRange">
<span class="label label-warning">Whitelist {{ whitelistSourceRange }}</span>
</span>
<span data-ng-show="frontendCtrl.frontend.priority" class="label label-warning">Priority:{{frontendCtrl.frontend.priority}}</span>
</div>
</div>