Skip to content
This repository has been archived by the owner on Apr 17, 2019. It is now read-only.

[nginx-ingress-controller] Add cidr whitelist support #1144

Merged
merged 2 commits into from
Jun 14, 2016
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions ingress/controllers/nginx/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import (
"k8s.io/contrib/ingress/controllers/nginx/nginx/auth"
"k8s.io/contrib/ingress/controllers/nginx/nginx/config"
"k8s.io/contrib/ingress/controllers/nginx/nginx/healthcheck"
"k8s.io/contrib/ingress/controllers/nginx/nginx/ipwhitelist"
"k8s.io/contrib/ingress/controllers/nginx/nginx/ratelimit"
"k8s.io/contrib/ingress/controllers/nginx/nginx/rewrite"
"k8s.io/contrib/ingress/controllers/nginx/nginx/secureupstream"
Expand Down Expand Up @@ -697,6 +698,12 @@ func (lbc *loadBalancerController) getUpstreamServers(ngxCfg config.Configuratio
glog.V(3).Infof("error parsing rewrite annotations for Ingress rule %v/%v: %v", ing.GetNamespace(), ing.GetName(), err)
}

wl, err := ipwhitelist.ParseAnnotations(ngxCfg.WhitelistSourceRange, ing)
glog.V(3).Infof("nginx white list %v", wl)
if err != nil {
glog.V(3).Infof("error reading white list annotation in Ingress %v/%v: %v", ing.GetNamespace(), ing.GetName(), err)
}

host := rule.Host
if host == "" {
host = defServerName
Expand Down Expand Up @@ -728,6 +735,7 @@ func (lbc *loadBalancerController) getUpstreamServers(ngxCfg config.Configuratio
loc.RateLimit = *rl
loc.Redirect = *locRew
loc.SecureUpstream = secUpstream
loc.Whitelist = *wl

addLoc = false
continue
Expand All @@ -750,6 +758,7 @@ func (lbc *loadBalancerController) getUpstreamServers(ngxCfg config.Configuratio
RateLimit: *rl,
Redirect: *locRew,
SecureUpstream: secUpstream,
Whitelist: *wl,
})
}
}
Expand Down
122 changes: 122 additions & 0 deletions ingress/controllers/nginx/examples/whitelist/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@

This example shows how is possible to restrict access

echo "
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: whitelist
annotations:
ingress.kubernetes.io/whitelist-source-range: "1.1.1.1/24"
spec:
rules:
- host: foo.bar.com
http:
paths:
- path: /
backend:
serviceName: echoheaders
servicePort: 80
" | kubectl create -f -


Check the annotation is present in the Ingress rule:
```
$ kubectl get ingress whitelist -o yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
ingress.kubernetes.io/whitelist-source-range: 1.1.1.1/24
creationTimestamp: 2016-06-09T21:39:06Z
generation: 2
name: whitelist
namespace: default
resourceVersion: "419363"
selfLink: /apis/extensions/v1beta1/namespaces/default/ingresses/whitelist
uid: 97b74737-2e8a-11e6-90db-080027d2dc94
spec:
rules:
- host: whitelist.bar.com
http:
paths:
- backend:
serviceName: echoheaders
servicePort: 80
path: /
status:
loadBalancer:
ingress:
- ip: 172.17.4.99
``

Finally test is not possible to access the URL

```
$ curl -v http://172.17.4.99/ -H 'Host: whitelist.bar.com'
* Trying 172.17.4.99...
* Connected to 172.17.4.99 (172.17.4.99) port 80 (#0)
> GET / HTTP/1.1
> Host: whitelist.bar.com
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 403 Forbidden
< Server: nginx/1.11.1
< Date: Thu, 09 Jun 2016 21:56:17 GMT
< Content-Type: text/html
< Content-Length: 169
< Connection: keep-alive
<
<html>
<head><title>403 Forbidden</title></head>
<body bgcolor="white">
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.11.1</center>
</body>
</html>
* Connection #0 to host 172.17.4.99 left intact
```

Removing the annotation removes the restriction

```
* Trying 172.17.4.99...
* Connected to 172.17.4.99 (172.17.4.99) port 80 (#0)
> GET / HTTP/1.1
> Host: whitelist.bar.com
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.11.1
< Date: Thu, 09 Jun 2016 21:57:44 GMT
< Content-Type: text/plain
< Transfer-Encoding: chunked
< Connection: keep-alive
<
CLIENT VALUES:
client_address=10.2.89.7
command=GET
real path=/
query=nil
request_version=1.1
request_uri=http://whitelist.bar.com:8080/

SERVER VALUES:
server_version=nginx: 1.9.11 - lua: 10001

HEADERS RECEIVED:
accept=*/*
connection=close
host=whitelist.bar.com
user-agent=curl/7.43.0
x-forwarded-for=10.2.89.1
x-forwarded-host=whitelist.bar.com
x-forwarded-port=80
x-forwarded-proto=http
x-real-ip=10.2.89.1
BODY:
* Connection #0 to host 172.17.4.99 left intact
```

6 changes: 6 additions & 0 deletions ingress/controllers/nginx/nginx.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,12 @@ http {
{{- range $location := $server.Locations }}
{{ $path := buildLocation $location }}
location {{ $path }} {
{{ if gt (len $location.Whitelist.CIDR) 0 }}
{{- range $ip := $location.Whitelist.CIDR }}
allow {{ $ip }};{{ end }}
deny all;
{{ end -}}

{{ if (and $server.SSL $location.Redirect.SSLRedirect) -}}
# enforce ssl on server side
if ($scheme = http) {
Expand Down
5 changes: 5 additions & 0 deletions ingress/controllers/nginx/nginx/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,10 @@ type Configuration struct {
// Responses with the “text/html” type are always compressed if UseGzip is enabled
GzipTypes string `structs:"gzip-types,omitempty"`

// WhitelistSourceRange allows limiting access to certain client addresses
// http://nginx.org/en/docs/http/ngx_http_access_module.html
WhitelistSourceRange []string `structs:"whitelist-source-range,omitempty"`

// Defines the number of worker processes. By default auto means number of available CPU cores
// http://nginx.org/en/docs/ngx_core_module.html#worker_processes
WorkerProcesses string `structs:"worker-processes,omitempty"`
Expand Down Expand Up @@ -270,6 +274,7 @@ func NewDefault() Configuration {
VtsStatusZoneSize: "10m",
UseHTTP2: true,
CustomHTTPErrors: make([]int, 0),
WhitelistSourceRange: make([]string, 0),
}

if glog.V(5) {
Expand Down
83 changes: 83 additions & 0 deletions ingress/controllers/nginx/nginx/ipwhitelist/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package ipwhitelist

import (
"errors"
"strings"

"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/kubernetes/pkg/util/net/sets"
)

const (
whitelist = "ingress.kubernetes.io/whitelist-source-range"
)

var (
// ErrMissingWhitelist returned error when the ingress does not contains the
// whitelist annotation
ErrMissingWhitelist = errors.New("whitelist annotation is missing")

// ErrInvalidCIDR returned error when the whitelist annotation does not
// contains a valid IP or network address
ErrInvalidCIDR = errors.New("the annotation does not contains a valid IP address or network")
)

// SourceRange returns the CIDR
type SourceRange struct {
CIDR []string
}

type ingAnnotations map[string]string

func (a ingAnnotations) whitelist() ([]string, error) {
val, ok := a[whitelist]
if !ok {
return nil, ErrMissingWhitelist
}

values := strings.Split(val, ",")
ipnets, err := sets.ParseIPNets(values...)
if err != nil {
return nil, ErrInvalidCIDR
}

cidrs := make([]string, 0)
for k := range ipnets {
cidrs = append(cidrs, k)
}

return cidrs, nil
}

// ParseAnnotations parses the annotations contained in the ingress
// rule used to limit access to certain client addresses or networks.
// Multiple ranges can specified using commas as separator
// e.g. `18.0.0.0/8,56.0.0.0/8`
func ParseAnnotations(whiteList []string, ing *extensions.Ingress) (*SourceRange, error) {
if ing.GetAnnotations() == nil {
return &SourceRange{whiteList}, ErrMissingWhitelist
}

wl, err := ingAnnotations(ing.GetAnnotations()).whitelist()
if err != nil {
wl = whiteList
}

return &SourceRange{wl}, err
}
98 changes: 98 additions & 0 deletions ingress/controllers/nginx/nginx/ipwhitelist/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
Copyright 2016 The Kubernetes Authors All rights reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package ipwhitelist

import (
"reflect"
"testing"

"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/kubernetes/pkg/util/intstr"
)

func buildIngress() *extensions.Ingress {
defaultBackend := extensions.IngressBackend{
ServiceName: "default-backend",
ServicePort: intstr.FromInt(80),
}

return &extensions.Ingress{
ObjectMeta: api.ObjectMeta{
Name: "foo",
Namespace: api.NamespaceDefault,
},
Spec: extensions.IngressSpec{
Backend: &extensions.IngressBackend{
ServiceName: "default-backend",
ServicePort: intstr.FromInt(80),
},
Rules: []extensions.IngressRule{
{
Host: "foo.bar.com",
IngressRuleValue: extensions.IngressRuleValue{
HTTP: &extensions.HTTPIngressRuleValue{
Paths: []extensions.HTTPIngressPath{
{
Path: "/foo",
Backend: defaultBackend,
},
},
},
},
},
},
},
}
}

func TestAnnotations(t *testing.T) {
ing := buildIngress()

_, err := ingAnnotations(ing.GetAnnotations()).whitelist()
if err == nil {
t.Error("Expected a validation error")
}

testNet := "10.0.0.0/24"
enet := []string{testNet}

data := map[string]string{}
data[whitelist] = testNet
ing.SetAnnotations(data)

wl, err := ingAnnotations(ing.GetAnnotations()).whitelist()
if err != nil {
t.Errorf("Unexpected error: %v", err)
}

if !reflect.DeepEqual(wl, enet) {
t.Errorf("Expected %v but returned %s", enet, wl)
}

data[whitelist] = "10.0.0.0/24,10.0.1.0/25"
ing.SetAnnotations(data)

wl, err = ingAnnotations(ing.GetAnnotations()).whitelist()
if err != nil {
t.Errorf("Unexpected error: %v", err)
}

if len(wl) != 2 {
t.Errorf("Expected 2 netwotks but %v was returned", len(wl))
}
}
2 changes: 2 additions & 0 deletions ingress/controllers/nginx/nginx/nginx.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package nginx

import (
"k8s.io/contrib/ingress/controllers/nginx/nginx/auth"
"k8s.io/contrib/ingress/controllers/nginx/nginx/ipwhitelist"
"k8s.io/contrib/ingress/controllers/nginx/nginx/ratelimit"
"k8s.io/contrib/ingress/controllers/nginx/nginx/rewrite"
)
Expand Down Expand Up @@ -99,6 +100,7 @@ type Location struct {
RateLimit ratelimit.RateLimit
Redirect rewrite.Redirect
SecureUpstream bool
Whitelist ipwhitelist.SourceRange
}

// LocationByPath sorts location by path
Expand Down