IP Whitelists for Frontend (with Docker- & Kubernetes-Provider Support)
This commit is contained in:
parent
55f610422a
commit
5f0b215e90
16 changed files with 731 additions and 14 deletions
16
docs/toml.md
16
docs/toml.md
|
@ -479,6 +479,13 @@ defaultEntryPoints = ["http", "https"]
|
||||||
backend = "backend1"
|
backend = "backend1"
|
||||||
passHostHeader = true
|
passHostHeader = true
|
||||||
priority = 10
|
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
|
entrypoints = ["https"] # overrides defaultEntryPoints
|
||||||
[frontends.frontend2.routes.test_1]
|
[frontends.frontend2.routes.test_1]
|
||||||
rule = "Host:{subdomain:[a-z]+}.localhost"
|
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.priority=10`: override default frontend priority
|
||||||
- `traefik.frontend.entryPoints=http,https`: assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`.
|
- `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.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
|
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`.
|
- `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).
|
- `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
|
### Authentication
|
||||||
|
|
||||||
Is possible to add additional authentication annotations in the Ingress rule.
|
Is possible to add additional authentication annotations in the Ingress rule.
|
||||||
|
|
2
glide.lock
generated
2
glide.lock
generated
|
@ -1,4 +1,4 @@
|
||||||
hash: 1aa32496b865dda72d76c7cba3458f1c2c467acf0b99aab4609323f109aa64f6
|
hash: e59e8244152a823cd3633fb09cdd583c4e5be78d7b50fb7047ba6b6a9ed5e8ec
|
||||||
updated: 2017-05-02T11:46:23.91434995-04:00
|
updated: 2017-05-02T11:46:23.91434995-04:00
|
||||||
imports:
|
imports:
|
||||||
- name: cloud.google.com/go
|
- name: cloud.google.com/go
|
||||||
|
|
308
middlewares/ip_whitelister_test.go
Normal file
308
middlewares/ip_whitelister_test.go
Normal 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")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
83
middlewares/ip_witelister.go
Normal file
83
middlewares/ip_witelister.go
Normal 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)
|
||||||
|
}
|
|
@ -272,6 +272,7 @@ func (p *Provider) loadDockerConfig(containersInspected []dockerData) *types.Con
|
||||||
"getServicePassHostHeader": p.getServicePassHostHeader,
|
"getServicePassHostHeader": p.getServicePassHostHeader,
|
||||||
"getServicePriority": p.getServicePriority,
|
"getServicePriority": p.getServicePriority,
|
||||||
"getServiceBackend": p.getServiceBackend,
|
"getServiceBackend": p.getServiceBackend,
|
||||||
|
"getWhitelistSourceRange": p.getWhitelistSourceRange,
|
||||||
}
|
}
|
||||||
// filter containers
|
// filter containers
|
||||||
filteredContainers := fun.Filter(func(container dockerData) bool {
|
filteredContainers := fun.Filter(func(container dockerData) bool {
|
||||||
|
@ -663,6 +664,15 @@ func (p *Provider) getPassHostHeader(container dockerData) string {
|
||||||
return "true"
|
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 {
|
func (p *Provider) getPriority(container dockerData) string {
|
||||||
if priority, err := getLabel(container, "traefik.frontend.priority"); err == nil {
|
if priority, err := getLabel(container, "traefik.frontend.priority"); err == nil {
|
||||||
return priority
|
return priority
|
||||||
|
|
|
@ -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) {
|
func TestDockerGetLabel(t *testing.T) {
|
||||||
containers := []struct {
|
containers := []struct {
|
||||||
container docker.ContainerJSON
|
container docker.ContainerJSON
|
||||||
|
|
|
@ -31,6 +31,8 @@ const (
|
||||||
ruleTypePathStrip = "PathStrip"
|
ruleTypePathStrip = "PathStrip"
|
||||||
ruleTypePath = "Path"
|
ruleTypePath = "Path"
|
||||||
ruleTypePathPrefix = "PathPrefix"
|
ruleTypePathPrefix = "PathPrefix"
|
||||||
|
|
||||||
|
annotationKubernetesWhitelistSourceRange = "ingress.kubernetes.io/whitelist-source-range"
|
||||||
)
|
)
|
||||||
|
|
||||||
const traefikDefaultRealm = "traefik"
|
const traefikDefaultRealm = "traefik"
|
||||||
|
@ -171,6 +173,9 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
|
||||||
return nil, errors.New("no realm customization supported")
|
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 {
|
if _, exists := templateObjects.Frontends[r.Host+pa.Path]; !exists {
|
||||||
basicAuthCreds, err := handleBasicAuthConfig(i, k8sClient)
|
basicAuthCreds, err := handleBasicAuthConfig(i, k8sClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -182,6 +187,7 @@ func (p *Provider) loadIngresses(k8sClient Client) (*types.Configuration, error)
|
||||||
Routes: make(map[string]types.Route),
|
Routes: make(map[string]types.Route),
|
||||||
Priority: len(pa.Path),
|
Priority: len(pa.Path),
|
||||||
BasicAuth: basicAuthCreds,
|
BasicAuth: basicAuthCreds,
|
||||||
|
WhitelistSourceRange: whitelistSourceRange,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(r.Host) > 0 {
|
if len(r.Host) > 0 {
|
||||||
|
|
|
@ -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{
|
services := []*v1.Service{
|
||||||
{
|
{
|
||||||
|
@ -1613,6 +1642,19 @@ func TestIngressAnnotations(t *testing.T) {
|
||||||
Method: "wrr",
|
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{
|
Frontends: map[string]*types.Frontend{
|
||||||
"foo/bar": {
|
"foo/bar": {
|
||||||
|
@ -1655,6 +1697,23 @@ func TestIngressAnnotations(t *testing.T) {
|
||||||
},
|
},
|
||||||
BasicAuth: []string{"myUser:myEncodedPW"},
|
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
19
provider/string_util.go
Normal 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
|
||||||
|
}
|
61
provider/string_util_test.go
Normal file
61
provider/string_util_test.go
Normal 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -716,6 +716,14 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
|
||||||
negroni.Use(metricsMiddlewareBackend)
|
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 {
|
if len(frontend.BasicAuth) > 0 {
|
||||||
users := types.Users{}
|
users := types.Users{}
|
||||||
for _, user := range frontend.BasicAuth {
|
for _, user := range frontend.BasicAuth {
|
||||||
|
@ -770,6 +778,21 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo
|
||||||
return serverEntryPoints, nil
|
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) {
|
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)
|
// 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
|
// -- Replacing Path should happen at the very end of the Modifier chain, after all the Matcher+Modifiers ran
|
||||||
|
|
|
@ -14,6 +14,8 @@ import (
|
||||||
"github.com/containous/traefik/testhelpers"
|
"github.com/containous/traefik/testhelpers"
|
||||||
"github.com/containous/traefik/types"
|
"github.com/containous/traefik/types"
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/vulcand/oxy/roundrobin"
|
"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) {
|
func TestServerLoadConfigEmptyBasicAuth(t *testing.T) {
|
||||||
globalConfig := GlobalConfiguration{
|
globalConfig := GlobalConfiguration{
|
||||||
EntryPoints: EntryPoints{
|
EntryPoints: EntryPoints{
|
||||||
|
|
|
@ -43,6 +43,11 @@
|
||||||
[frontends."frontend-{{getServiceBackend $container $serviceName}}"]
|
[frontends."frontend-{{getServiceBackend $container $serviceName}}"]
|
||||||
backend = "backend-{{getServiceBackend $container $serviceName}}"
|
backend = "backend-{{getServiceBackend $container $serviceName}}"
|
||||||
passHostHeader = {{getServicePassHostHeader $container $serviceName}}
|
passHostHeader = {{getServicePassHostHeader $container $serviceName}}
|
||||||
|
{{if getWhitelistSourceRange $container}}
|
||||||
|
whitelistSourceRange = [{{range getWhitelistSourceRange $container}}
|
||||||
|
"{{.}}",
|
||||||
|
{{end}}]
|
||||||
|
{{end}}
|
||||||
priority = {{getServicePriority $container $serviceName}}
|
priority = {{getServicePriority $container $serviceName}}
|
||||||
entryPoints = [{{range getServiceEntryPoints $container $serviceName}}
|
entryPoints = [{{range getServiceEntryPoints $container $serviceName}}
|
||||||
"{{.}}",
|
"{{.}}",
|
||||||
|
@ -57,6 +62,11 @@
|
||||||
[frontends."frontend-{{$frontend}}"]
|
[frontends."frontend-{{$frontend}}"]
|
||||||
backend = "backend-{{getBackend $container}}"
|
backend = "backend-{{getBackend $container}}"
|
||||||
passHostHeader = {{getPassHostHeader $container}}
|
passHostHeader = {{getPassHostHeader $container}}
|
||||||
|
{{if getWhitelistSourceRange $container}}
|
||||||
|
whitelistSourceRange = [{{range getWhitelistSourceRange $container}}
|
||||||
|
"{{.}}",
|
||||||
|
{{end}}]
|
||||||
|
{{end}}
|
||||||
priority = {{getPriority $container}}
|
priority = {{getPriority $container}}
|
||||||
entryPoints = [{{range getEntryPoints $container}}
|
entryPoints = [{{range getEntryPoints $container}}
|
||||||
"{{.}}",
|
"{{.}}",
|
||||||
|
|
|
@ -22,6 +22,9 @@
|
||||||
passHostHeader = {{$frontend.PassHostHeader}}
|
passHostHeader = {{$frontend.PassHostHeader}}
|
||||||
basicAuth = [{{range $frontend.BasicAuth}}
|
basicAuth = [{{range $frontend.BasicAuth}}
|
||||||
"{{.}}",
|
"{{.}}",
|
||||||
|
{{end}}]
|
||||||
|
whitelistSourceRange = [{{range $frontend.WhitelistSourceRange}}
|
||||||
|
"{{.}}",
|
||||||
{{end}}]
|
{{end}}]
|
||||||
{{range $routeName, $route := $frontend.Routes}}
|
{{range $routeName, $route := $frontend.Routes}}
|
||||||
[frontends."{{$frontendName}}".routes."{{$routeName}}"]
|
[frontends."{{$frontendName}}".routes."{{$routeName}}"]
|
||||||
|
|
|
@ -62,6 +62,7 @@ type Frontend struct {
|
||||||
PassHostHeader bool `json:"passHostHeader,omitempty"`
|
PassHostHeader bool `json:"passHostHeader,omitempty"`
|
||||||
Priority int `json:"priority"`
|
Priority int `json:"priority"`
|
||||||
BasicAuth []string `json:"basicAuth"`
|
BasicAuth []string `json:"basicAuth"`
|
||||||
|
WhitelistSourceRange []string `json:"whitelistSourceRange,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadBalancerMethod holds the method of load balancing to use.
|
// LoadBalancerMethod holds the method of load balancing to use.
|
||||||
|
|
|
@ -15,9 +15,14 @@
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div data-bg-show="frontendCtrl.frontend.backend" class="panel-footer">
|
<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"> </span></span>
|
<span data-ng-repeat="entryPoint in frontendCtrl.frontend.entryPoints">
|
||||||
|
<span class="label label-primary">{{entryPoint}}</span><span data-ng-hide="$last"> </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 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-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>
|
<span data-ng-show="frontendCtrl.frontend.priority" class="label label-warning">Priority:{{frontendCtrl.frontend.priority}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue