Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 2 commits into from
May 19, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
16 changes: 15 additions & 1 deletion docs/toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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`.
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion glide.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions glide.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ import:
- package: github.com/streamrail/concurrent-map
- package: github.com/stretchr/testify
subpackages:
- assert
- mock
- require
- package: github.com/thoas/stats
version: 152b5d051953fdb6e45f14b6826962aadc032324
- package: github.com/unrolled/render
Expand Down
308 changes: 308 additions & 0 deletions middlewares/ip_whitelister_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
})
}
}