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
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -31,6 +31,8 @@ const (
|
|||
ruleTypePathStrip = "PathStrip"
|
||||
ruleTypePath = "Path"
|
||||
ruleTypePathPrefix = "PathPrefix"
|
||||
|
||||
annotationKubernetesWhitelistSourceRange = "ingress.kubernetes.io/whitelist-source-range"
|
||||
)
|
||||
|
||||
const traefikDefaultRealm = "traefik"
|
||||
|
@ -171,17 +173,21 @@ 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 {
|
||||
return nil, err
|
||||
}
|
||||
templateObjects.Frontends[r.Host+pa.Path] = &types.Frontend{
|
||||
Backend: r.Host + pa.Path,
|
||||
PassHostHeader: PassHostHeader,
|
||||
Routes: make(map[string]types.Route),
|
||||
Priority: len(pa.Path),
|
||||
BasicAuth: basicAuthCreds,
|
||||
Backend: r.Host + pa.Path,
|
||||
PassHostHeader: PassHostHeader,
|
||||
Routes: make(map[string]types.Route),
|
||||
Priority: len(pa.Path),
|
||||
BasicAuth: basicAuthCreds,
|
||||
WhitelistSourceRange: whitelistSourceRange,
|
||||
}
|
||||
}
|
||||
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{
|
||||
{
|
||||
|
@ -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
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)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue