From 7212d0081bd761477acb3b98ed4e7c44dd2fc5f8 Mon Sep 17 00:00:00 2001 From: Pavel Sinkevych Date: Mon, 27 Aug 2018 16:50:04 +0300 Subject: [PATCH] Provide possibility to block CIDRs, User-Agents and Referers globally --- .../nginx-configuration/configmap.md | 26 ++++ internal/ingress/controller/config/config.go | 13 ++ .../ingress/controller/template/configmap.go | 23 +++ .../ingress/controller/template/template.go | 1 + rootfs/etc/nginx/template/nginx.tmpl | 56 +++++++ test/e2e/settings/global_access_block.go | 144 ++++++++++++++++++ 6 files changed, 263 insertions(+) create mode 100644 test/e2e/settings/global_access_block.go diff --git a/docs/user-guide/nginx-configuration/configmap.md b/docs/user-guide/nginx-configuration/configmap.md index 169f0df4fd..cf18cc6d35 100644 --- a/docs/user-guide/nginx-configuration/configmap.md +++ b/docs/user-guide/nginx-configuration/configmap.md @@ -144,6 +144,9 @@ The following table shows a configuration option's name, type, and the default v |[limit-req-status-code](#limit-req-status-code)|int|503| |[no-tls-redirect-locations](#no-tls-redirect-locations)|string|"/.well-known/acme-challenge"| |[no-auth-locations](#no-auth-locations)|string|"/.well-known/acme-challenge"| +|[block-cidrs](#block-cidrs)|[]string|""| +|[block-user-agents](#block-user-agents)|[]string|""| +|[block-referers](#block-referers)|[]string|""| ## add-headers @@ -791,3 +794,26 @@ _**default:**_ "/.well-known/acme-challenge" A comma-separated list of locations that should not get authenticated. _**default:**_ "/.well-known/acme-challenge" + +## block-cidrs + +A comma-separated list of IP addresses (or subnets), requestst from which have to be blocked globally. + +_References:_ +[http://nginx.org/en/docs/http/ngx_http_access_module.html#deny](http://nginx.org/en/docs/http/ngx_http_access_module.html#deny) + +## block-user-agents + +A comma-separated list of User-Agent, requestst from which have to be blocked globally. +It's possible to use here full strings and regular expressions. More details about valid patterns can be found at `map` Nginx directive documentation. + +_References:_ +[http://nginx.org/en/docs/http/ngx_http_map_module.html#map](http://nginx.org/en/docs/http/ngx_http_map_module.html#map) + +## block-referers + +A comma-separated list of Referers, requestst from which have to be blocked globally. +It's possible to use here full strings and regular expressions. More details about valid patterns can be found at `map` Nginx directive documentation. + +_References:_ +[http://nginx.org/en/docs/http/ngx_http_map_module.html#map](http://nginx.org/en/docs/http/ngx_http_map_module.html#map) diff --git a/internal/ingress/controller/config/config.go b/internal/ingress/controller/config/config.go index ee58f6d26c..41e9e38332 100644 --- a/internal/ingress/controller/config/config.go +++ b/internal/ingress/controller/config/config.go @@ -533,12 +533,22 @@ type Configuration struct { // Checksum contains a checksum of the configmap configuration Checksum string `json:"-"` + + // Block all requests from given IPs + BlockCIDRs []string `json:"block-cidrs"` + + // Block all requests with given User-Agent headers + BlockUserAgents []string `json:"block-user-agents"` + + // Block all requests with given Referer headers + BlockReferers []string `json:"block-referers"` } // NewDefault returns the default nginx configuration func NewDefault() Configuration { defIPCIDR := make([]string, 0) defBindAddress := make([]string, 0) + defBlockEntity := make([]string, 0) defNginxStatusIpv4Whitelist := make([]string, 0) defNginxStatusIpv6Whitelist := make([]string, 0) @@ -552,6 +562,9 @@ func NewDefault() Configuration { AccessLogPath: "/var/log/nginx/access.log", WorkerCpuAffinity: "", ErrorLogPath: "/var/log/nginx/error.log", + BlockCIDRs: defBlockEntity, + BlockUserAgents: defBlockEntity, + BlockReferers: defBlockEntity, BrotliLevel: 4, BrotliTypes: brotliTypes, ClientHeaderBufferSize: "1k", diff --git a/internal/ingress/controller/template/configmap.go b/internal/ingress/controller/template/configmap.go index 604d208134..d4515fad8f 100644 --- a/internal/ingress/controller/template/configmap.go +++ b/internal/ingress/controller/template/configmap.go @@ -41,6 +41,9 @@ const ( proxyRealIPCIDR = "proxy-real-ip-cidr" bindAddress = "bind-address" httpRedirectCode = "http-redirect-code" + blockCIDRs = "block-cidrs" + blockUserAgents = "block-user-agents" + blockReferers = "block-referers" proxyStreamResponses = "proxy-stream-responses" hideHeaders = "hide-headers" nginxStatusIpv4Whitelist = "nginx-status-ipv4-whitelist" @@ -71,6 +74,10 @@ func ReadConfig(src map[string]string) config.Configuration { bindAddressIpv4List := make([]string, 0) bindAddressIpv6List := make([]string, 0) + blockCIDRList := make([]string, 0) + blockUserAgentList := make([]string, 0) + blockRefererList := make([]string, 0) + if val, ok := conf[customHTTPErrors]; ok { delete(conf, customHTTPErrors) for _, i := range strings.Split(val, ",") { @@ -116,6 +123,19 @@ func ReadConfig(src map[string]string) config.Configuration { } } + if val, ok := conf[blockCIDRs]; ok { + delete(conf, blockCIDRs) + blockCIDRList = strings.Split(val, ",") + } + if val, ok := conf[blockUserAgents]; ok { + delete(conf, blockUserAgents) + blockUserAgentList = strings.Split(val, ",") + } + if val, ok := conf[blockReferers]; ok { + delete(conf, blockReferers) + blockRefererList = strings.Split(val, ",") + } + if val, ok := conf[httpRedirectCode]; ok { delete(conf, httpRedirectCode) j, err := strconv.Atoi(val) @@ -184,6 +204,9 @@ func ReadConfig(src map[string]string) config.Configuration { to.ProxyRealIPCIDR = proxyList to.BindAddressIpv4 = bindAddressIpv4List to.BindAddressIpv6 = bindAddressIpv6List + to.BlockCIDRs = blockCIDRList + to.BlockUserAgents = blockUserAgentList + to.BlockReferers = blockRefererList to.HideHeaders = hideHeadersList to.ProxyStreamResponses = streamResponses to.DisableIpv6DNS = !ing_net.IsIPv6Enabled() diff --git a/internal/ingress/controller/template/template.go b/internal/ingress/controller/template/template.go index 39cfa5ddbe..47bcdcd15e 100644 --- a/internal/ingress/controller/template/template.go +++ b/internal/ingress/controller/template/template.go @@ -141,6 +141,7 @@ var ( "contains": strings.Contains, "hasPrefix": strings.HasPrefix, "hasSuffix": strings.HasSuffix, + "trimSpace": strings.TrimSpace, "toUpper": strings.ToUpper, "toLower": strings.ToLower, "formatIP": formatIP, diff --git a/rootfs/etc/nginx/template/nginx.tmpl b/rootfs/etc/nginx/template/nginx.tmpl index 79b5f24de8..afbb24b6a4 100644 --- a/rootfs/etc/nginx/template/nginx.tmpl +++ b/rootfs/etc/nginx/template/nginx.tmpl @@ -491,6 +491,28 @@ http { {{ $zone }} {{ end }} + # Global filters + {{ range $ip := $cfg.BlockCIDRs }}deny {{ trimSpace $ip }}; + {{ end }} + + {{ if gt (len $cfg.BlockUserAgents) 0 }} + map $http_user_agent $block_ua { + default 0; + + {{ range $ua := $cfg.BlockUserAgents }}{{ trimSpace $ua }} 1; + {{ end }} + } + {{ end }} + + {{ if gt (len $cfg.BlockReferers) 0 }} + map $http_referer $block_ref { + default 0; + + {{ range $ref := $cfg.BlockReferers }}{{ trimSpace $ref }} 1; + {{ end }} + } + {{ end }} + {{/* Build server redirects (from/to www) */}} {{ range $hostname, $to := .RedirectServers }} server { @@ -512,6 +534,17 @@ http { {{ end }} server_name {{ $hostname }}; + {{ if gt (len $cfg.BlockUserAgents) 0 }} + if ($block_ua) { + return 403; + } + {{ end }} + {{ if gt (len $cfg.BlockReferers) 0 }} + if ($block_ref) { + return 403; + } + {{ end }} + {{ if ne $all.ListenPorts.HTTPS 443 }} {{ $redirect_port := (printf ":%v" $all.ListenPorts.HTTPS) }} return {{ $all.Cfg.HTTPRedirectCode }} $scheme://{{ $to }}{{ $redirect_port }}$request_uri; @@ -526,6 +559,18 @@ http { ## start server {{ $server.Hostname }} server { server_name {{ $server.Hostname }} {{ $server.Alias }}; + + {{ if gt (len $cfg.BlockUserAgents) 0 }} + if ($block_ua) { + return 403; + } + {{ end }} + {{ if gt (len $cfg.BlockReferers) 0 }} + if ($block_ref) { + return 403; + } + {{ end }} + {{ template "SERVER" serverConfig $all $server }} {{ if not (empty $cfg.ServerSnippet) }} @@ -545,6 +590,17 @@ http { {{ if $IsIPV6Enabled }}listen [::]:{{ $all.ListenPorts.Status }} default_server {{ if $all.Cfg.ReusePort }}reuseport{{ end }} backlog={{ $all.BacklogSize }};{{ end }} set $proxy_upstream_name "-"; + {{ if gt (len $cfg.BlockUserAgents) 0 }} + if ($block_ua) { + return 403; + } + {{ end }} + {{ if gt (len $cfg.BlockReferers) 0 }} + if ($block_ref) { + return 403; + } + {{ end }} + location {{ $healthzURI }} { {{ if $cfg.EnableOpentracing }} opentracing off; diff --git a/test/e2e/settings/global_access_block.go b/test/e2e/settings/global_access_block.go new file mode 100644 index 0000000000..3caec7328b --- /dev/null +++ b/test/e2e/settings/global_access_block.go @@ -0,0 +1,144 @@ +/* +Copyright 2017 The Kubernetes Authors. + +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 settings + +import ( + "net/http" + "strings" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/parnurzeal/gorequest" + + "k8s.io/ingress-nginx/test/e2e/framework" +) + +var _ = framework.IngressNginxDescribe("Global access block", func() { + f := framework.NewDefaultFramework("global-access-block") + + host := "global-access-block" + + BeforeEach(func() { + err := f.NewEchoDeploymentWithReplicas(1) + Expect(err).NotTo(HaveOccurred()) + + ing, err := f.EnsureIngress(framework.NewSingleIngress(host, "/", host, f.IngressController.Namespace, "http-svc", 80, nil)) + Expect(err).NotTo(HaveOccurred()) + Expect(ing).NotTo(BeNil()) + }) + + AfterEach(func() { + }) + + It("should block CIDRs defined in the ConfigMap", func() { + err := f.UpdateNginxConfigMapData("block-cidrs", "172.16.0.0/12,192.168.0.0/16,10.0.0.0/8") + Expect(err).NotTo(HaveOccurred()) + + err = f.WaitForNginxConfiguration( + func(cfg string) bool { + return strings.Contains(cfg, "deny 172.16.0.0/12;") && + strings.Contains(cfg, "deny 192.168.0.0/16;") && + strings.Contains(cfg, "deny 10.0.0.0/8;") + }) + Expect(err).NotTo(HaveOccurred()) + + // This test works for minikube, but may have problems with real kubernetes clusters, + // especially if connection is done via Internet. In this case, the test should be disabled/removed. + resp, _, errs := gorequest.New(). + Get(f.IngressController.HTTPURL). + Set("Host", host). + End() + Expect(errs).To(BeNil()) + Expect(resp.StatusCode).Should(Equal(http.StatusForbidden)) + }) + + It("should block User-Agents defined in the ConfigMap", func() { + err := f.UpdateNginxConfigMapData("block-user-agents", "~*chrome\\/68\\.0\\.3440\\.106\\ safari\\/537\\.36,AlphaBot") + Expect(err).NotTo(HaveOccurred()) + + err = f.WaitForNginxConfiguration( + func(cfg string) bool { + return strings.Contains(cfg, "~*chrome\\/68\\.0\\.3440\\.106\\ safari\\/537\\.36 1;") && + strings.Contains(cfg, "AlphaBot 1;") + }) + Expect(err).NotTo(HaveOccurred()) + + // Should be blocked + resp, _, errs := gorequest.New(). + Get(f.IngressController.HTTPURL). + Set("Host", host). + Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36"). + End() + Expect(errs).To(BeNil()) + Expect(resp.StatusCode).Should(Equal(http.StatusForbidden)) + + resp, _, errs = gorequest.New(). + Get(f.IngressController.HTTPURL). + Set("Host", host). + Set("User-Agent", "AlphaBot"). + End() + Expect(errs).To(BeNil()) + Expect(resp.StatusCode).Should(Equal(http.StatusForbidden)) + + // Shouldn't be blocked + resp, _, errs = gorequest.New(). + Get(f.IngressController.HTTPURL). + Set("Host", host). + Set("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 11_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.0 Mobile/15E148 Safari/604.1"). + End() + Expect(errs).To(BeNil()) + Expect(resp.StatusCode).Should(Equal(http.StatusOK)) + }) + + It("should block Referers defined in the ConfigMap", func() { + err := f.UpdateNginxConfigMapData("block-referers", "~*example\\.com,qwerty") + Expect(err).NotTo(HaveOccurred()) + + err = f.WaitForNginxConfiguration( + func(cfg string) bool { + return strings.Contains(cfg, "~*example\\.com 1;") && + strings.Contains(cfg, "qwerty 1;") + }) + Expect(err).NotTo(HaveOccurred()) + + // Should be blocked + resp, _, errs := gorequest.New(). + Get(f.IngressController.HTTPURL). + Set("Host", host). + Set("Referer", "example.com"). + End() + Expect(errs).To(BeNil()) + Expect(resp.StatusCode).Should(Equal(http.StatusForbidden)) + + resp, _, errs = gorequest.New(). + Get(f.IngressController.HTTPURL). + Set("Host", host). + Set("Referer", "qwerty"). + End() + Expect(errs).To(BeNil()) + Expect(resp.StatusCode).Should(Equal(http.StatusForbidden)) + + // Shouldn't be blocked + resp, _, errs = gorequest.New(). + Get(f.IngressController.HTTPURL). + Set("Host", host). + Set("Referer", "qwerty123"). + End() + Expect(errs).To(BeNil()) + Expect(resp.StatusCode).Should(Equal(http.StatusOK)) + }) +})