Skip to content

Commit 6156cfe

Browse files
thriqonMo3m3n
authored andcommitted
MINOR: Accept patternfiles in whitelist/blacklists
As a special case, if whitelist or blacklist start with `patterns/`, use this map directly. This enables centrally managing IP ranges and using them in many Ingresses. For example: ``` -- kind: ConfigMap apiVersion: v1 metadata: name: patternfiles namespace: haproxy-controller data: ips: | 192.168.0.0/24 --- kind: Ingress apiVersion: networking.k8s.io/v1 metadata: name: http-restricted-echo annotations: ingress.class: haproxy haproxy.org/whitelist: patterns/ips spec: rules: - host: localhost http: paths: - path: /restricted pathType: Prefix backend: service: name: http-echo port: name: http ``` E2e test is marked sequential as it modifies global state (patternfiles).
1 parent ef62b8a commit 6156cfe

File tree

8 files changed

+256
-9
lines changed

8 files changed

+256
-9
lines changed

controller/annotations/ingress/accessControl.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,23 @@ func (a *AccessControl) Process(k store.K8s, annotations ...map[string]string) (
3636
if input == "" {
3737
return
3838
}
39+
40+
if strings.HasPrefix(input, "patterns/") {
41+
a.rules.Add(&rules.ReqDeny{
42+
SrcIPsMap: maps.Path(input),
43+
Whitelist: a.whitelist,
44+
})
45+
46+
return
47+
}
48+
3949
var mapName maps.Name
40-
var whitelist bool
4150
if a.whitelist {
4251
mapName = maps.Name("whitelist-" + utils.Hash([]byte(input)))
43-
whitelist = true
4452
} else {
4553
mapName = maps.Name("blacklist-" + utils.Hash([]byte(input)))
4654
}
55+
4756
if !a.maps.Exists(mapName) {
4857
for _, address := range strings.Split(input, ",") {
4958
address = strings.TrimSpace(address)
@@ -57,7 +66,7 @@ func (a *AccessControl) Process(k store.K8s, annotations ...map[string]string) (
5766
}
5867
a.rules.Add(&rules.ReqDeny{
5968
SrcIPsMap: maps.GetPath(mapName),
60-
Whitelist: whitelist,
69+
Whitelist: a.whitelist,
6170
})
6271
return
6372
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright 2019 HAProxy Technologies LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
//go:build e2e_sequential
16+
17+
package accesscontrol
18+
19+
import (
20+
"net/http"
21+
22+
"github.com/haproxytech/kubernetes-ingress/deploy/tests/e2e"
23+
)
24+
25+
func (suite *AccessControlSuite) eventuallyReturns(clientIP string, httpStatus int) {
26+
suite.Eventually(func() bool {
27+
suite.client.Req.Header = map[string][]string{
28+
"X-Client-IP": {clientIP},
29+
}
30+
res, cls, err := suite.client.Do()
31+
if err != nil {
32+
suite.T().Logf("Connection ERROR: %s", err.Error())
33+
return false
34+
}
35+
defer cls()
36+
return res.StatusCode == httpStatus
37+
}, e2e.WaitDuration, e2e.TickDuration, "waiting for call with client IP %v to return %v expired", clientIP, httpStatus)
38+
}
39+
40+
func (suite *AccessControlSuite) Test_Whitelist() {
41+
suite.Run("Inline", func() {
42+
suite.tmplData.IngAnnotations = []struct{ Key, Value string }{
43+
{"src-ip-header", " X-Client-IP"},
44+
{"whitelist", " 192.168.2.0/24"},
45+
}
46+
47+
suite.NoError(suite.test.Apply("config/deploy.yaml.tmpl", suite.test.GetNS(), suite.tmplData))
48+
49+
suite.eventuallyReturns("192.168.2.3", http.StatusOK)
50+
suite.eventuallyReturns("192.168.5.3", http.StatusForbidden)
51+
})
52+
53+
suite.Run("Patternfile", func() {
54+
suite.tmplData.IngAnnotations = []struct{ Key, Value string }{
55+
{"src-ip-header", " X-Client-IP"},
56+
{"whitelist", " patterns/ips"},
57+
}
58+
59+
suite.NoError(suite.test.Apply("config/deploy.yaml.tmpl", suite.test.GetNS(), suite.tmplData))
60+
suite.NoError(suite.test.Apply("config/patternfile-a.yml", "", nil))
61+
62+
suite.eventuallyReturns("192.168.0.3", http.StatusOK)
63+
suite.eventuallyReturns("192.168.2.3", http.StatusForbidden)
64+
})
65+
}
66+
67+
func (suite *AccessControlSuite) Test_Blacklist() {
68+
suite.Run("Inline", func() {
69+
suite.tmplData.IngAnnotations = []struct{ Key, Value string }{
70+
{"src-ip-header", " X-Client-IP"},
71+
{"blacklist", " 192.168.2.0/24"},
72+
}
73+
74+
suite.NoError(suite.test.Apply("config/deploy.yaml.tmpl", suite.test.GetNS(), suite.tmplData))
75+
76+
suite.eventuallyReturns("192.168.2.3", http.StatusForbidden)
77+
suite.eventuallyReturns("192.168.5.3", http.StatusOK)
78+
})
79+
80+
suite.Run("Patternfile", func() {
81+
suite.tmplData.IngAnnotations = []struct{ Key, Value string }{
82+
{"src-ip-header", " X-Client-IP"},
83+
{"blacklist", " patterns/ips"},
84+
}
85+
86+
suite.NoError(suite.test.Apply("config/deploy.yaml.tmpl", suite.test.GetNS(), suite.tmplData))
87+
suite.NoError(suite.test.Apply("config/patternfile-a.yml", "", nil))
88+
89+
suite.eventuallyReturns("192.168.0.3", http.StatusForbidden)
90+
suite.eventuallyReturns("192.168.2.3", http.StatusOK)
91+
})
92+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
---
2+
kind: Deployment
3+
apiVersion: apps/v1
4+
metadata:
5+
name: http-echo
6+
spec:
7+
replicas: 1
8+
selector:
9+
matchLabels:
10+
app: http-echo
11+
template:
12+
metadata:
13+
labels:
14+
app: http-echo
15+
spec:
16+
containers:
17+
- name: http-echo
18+
image: haproxytech/http-echo:latest
19+
imagePullPolicy: Never
20+
args:
21+
- --default-response=hostname
22+
ports:
23+
- name: http
24+
containerPort: 8888
25+
protocol: TCP
26+
- name: https
27+
containerPort: 8443
28+
protocol: TCP
29+
---
30+
kind: Service
31+
apiVersion: v1
32+
metadata:
33+
name: http-echo
34+
spec:
35+
ports:
36+
- name: http
37+
protocol: TCP
38+
port: 80
39+
targetPort: http
40+
- name: https
41+
protocol: TCP
42+
port: 443
43+
targetPort: https
44+
selector:
45+
app: http-echo
46+
---
47+
kind: Ingress
48+
apiVersion: networking.k8s.io/v1beta1
49+
metadata:
50+
name: http-echo
51+
annotations:
52+
ingress.class: haproxy
53+
{{range .IngAnnotations}}
54+
{{ .Key }}: {{ .Value }}
55+
{{end}}
56+
spec:
57+
rules:
58+
- host: {{ .Host }}
59+
http:
60+
paths:
61+
- path: /
62+
backend:
63+
serviceName: http-echo
64+
servicePort: http
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
apiVersion: v1
2+
kind: ConfigMap
3+
metadata:
4+
name: patternfiles
5+
namespace: haproxy-controller
6+
data:
7+
ips: |
8+
192.168.0.0/24
9+
192.168.1.0/24
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
apiVersion: v1
2+
kind: ConfigMap
3+
metadata:
4+
name: patternfiles
5+
namespace: haproxy-controller
6+
data: {}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright 2019 HAProxy Technologies LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
//go:build e2e_sequential
16+
17+
package accesscontrol
18+
19+
import (
20+
"testing"
21+
22+
"github.com/stretchr/testify/suite"
23+
24+
"github.com/haproxytech/kubernetes-ingress/deploy/tests/e2e"
25+
)
26+
27+
type AccessControlSuite struct {
28+
suite.Suite
29+
test e2e.Test
30+
client *e2e.Client
31+
tmplData tmplData
32+
}
33+
34+
type tmplData struct {
35+
IngAnnotations []struct{ Key, Value string }
36+
Host string
37+
}
38+
39+
func (suite *AccessControlSuite) SetupSuite() {
40+
var err error
41+
suite.test, err = e2e.NewTest()
42+
suite.NoError(err)
43+
suite.tmplData = tmplData{Host: suite.test.GetNS() + ".test"}
44+
suite.client, err = e2e.NewHTTPClient(suite.tmplData.Host)
45+
suite.NoError(err)
46+
47+
suite.NoError(suite.test.Apply("config/patternfile-empty.yml", "", nil))
48+
}
49+
50+
func (suite *AccessControlSuite) TearDownSuite() {
51+
suite.test.Apply("config/patternfile-empty.yml", "", nil)
52+
suite.test.TearDown()
53+
}
54+
55+
func TestAccessControlSuite(t *testing.T) {
56+
suite.Run(t, new(AccessControlSuite))
57+
}

documentation/README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ This is autogenerated from [doc.yaml](doc.yaml). Description can be found in [ge
1717
| [auth-type](#authentication) | string | | |:large_blue_circle:|:large_blue_circle:|:white_circle:|
1818
| [auth-secret](#authentication) | string | | auth-type |:large_blue_circle:|:large_blue_circle:|:white_circle:|
1919
| [auth-realm](#authentication) | string | "Protected Content" | auth-type, auth-secret |:large_blue_circle:|:large_blue_circle:|:white_circle:|
20-
| [blacklist](#access-control) | IPs or CIDRs | | |:large_blue_circle:|:large_blue_circle:|:white_circle:|
20+
| [blacklist](#access-control) | IPs/CIDRs or pattern file | | |:large_blue_circle:|:large_blue_circle:|:white_circle:|
2121
| [check](#backend-checks) | [bool](#bool) | "true" | |:large_blue_circle:|:large_blue_circle:|:large_blue_circle:|
2222
| [check-http](#backend-checks) | string | | check |:large_blue_circle:|:large_blue_circle:|:large_blue_circle:|
2323
| [check-interval](#backend-checks) | [time](#time) | | check |:large_blue_circle:|:large_blue_circle:|:large_blue_circle:|
@@ -84,7 +84,7 @@ This is autogenerated from [doc.yaml](doc.yaml). Description can be found in [ge
8484
| [timeout-server](#timeouts) | [time](#time) | "50s" | |:large_blue_circle:|:white_circle:|:white_circle:|
8585
| [timeout-server-fin](#timeouts) | [time](#time) | | |:large_blue_circle:|:white_circle:|:white_circle:|
8686
| [timeout-tunnel](#timeouts) | [time](#time) | "1h" | |:large_blue_circle:|:white_circle:|:white_circle:|
87-
| [whitelist](#access-control) | IPs or CIDRs | | |:large_blue_circle:|:large_blue_circle:|:white_circle:|
87+
| [whitelist](#access-control) | IPs/CIDRs or pattern file | | |:large_blue_circle:|:large_blue_circle:|:white_circle:|
8888
| [tls-alpn](#https) | string | "h2,http/1.1" | |:large_blue_circle:|:white_circle:|:white_circle:|
8989

9090
> :information_source: Annotations have hierarchy: `default` <- `Configmap` <- `Ingress` <- `Service`
@@ -234,9 +234,12 @@ cors-max-age: "1m"
234234

235235
Available on: `configmap` `ingress`
236236

237+
:information_source: The value is treated as a pattern file (see `--configmap-patternfiles`) if it starts with `patterns/`. It should consist of a list of IPs or CIDRs, one per line.
238+
237239
Possible values:
238240

239241
- Comma-separated list of IP addresses and/or CIDR ranges
242+
- Path to a pattern file, e.g. `pattern/ips`
240243

241244
Example:
242245

@@ -250,9 +253,12 @@ blacklist: "192.168.1.0/24, 192.168.2.100"
250253

251254
Available on: `configmap` `ingress`
252255

256+
:information_source: The value is treated as a pattern file (see `--configmap-patternfiles`) if it starts with `patterns/`. It should consist of a list of IPs or CIDRs, one per line.
257+
253258
Possible values:
254259

255260
- Comma-separated list of IP addresses and/or CIDR ranges
261+
- Path to a pattern file, e.g. `pattern/ips`
256262

257263
Example:
258264

documentation/doc.yaml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -490,15 +490,17 @@ annotations:
490490
version_min: "1.5"
491491
example: ["auth-realm: Admin Area"]
492492
- title: blacklist
493-
type: IPs or CIDRs
493+
type: IPs/CIDRs or pattern file
494494
group: access-control
495495
dependencies: ""
496496
default: ""
497497
description:
498498
- Blocks given IP addresses and/or IP address ranges.
499-
tip: []
499+
tip:
500+
- The value is treated as a pattern file (see `--configmap-patternfiles`) if it starts with `patterns/`. It should consist of a list of IPs or CIDRs, one per line.
500501
values:
501502
- Comma-separated list of IP addresses and/or CIDR ranges
503+
- Path to a pattern file, e.g. `pattern/ips`
502504
applies_to:
503505
- configmap
504506
- ingress
@@ -1726,15 +1728,17 @@ annotations:
17261728
version_min: "1.4"
17271729
example: ["timeout-tunnel: 30m"]
17281730
- title: whitelist
1729-
type: IPs or CIDRs
1731+
type: IPs/CIDRs or pattern file
17301732
group: access-control
17311733
dependencies: ""
17321734
default: ""
17331735
description:
17341736
- Blocks all IP addresses except the whitelisted ones (annotation value).
1735-
tip: []
1737+
tip:
1738+
- The value is treated as a pattern file (see `--configmap-patternfiles`) if it starts with `patterns/`. It should consist of a list of IPs or CIDRs, one per line.
17361739
values:
17371740
- Comma-separated list of IP addresses and/or CIDR ranges
1741+
- Path to a pattern file, e.g. `pattern/ips`
17381742
applies_to:
17391743
- configmap
17401744
- ingress

0 commit comments

Comments
 (0)