From 541928e27da928ea713322b72ae2e23c25810d3e Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Fri, 19 Aug 2016 11:51:40 -0300 Subject: [PATCH] Add external authentication using auth_request --- controllers/nginx/configuration.md | 23 ++- controllers/nginx/controller.go | 25 ++- .../nginx/examples/external-auth/README.md | 148 ++++++++++++++++++ .../nginx/examples/external-auth/ingress.yaml | 15 ++ controllers/nginx/main.go | 16 +- controllers/nginx/nginx.tmpl | 29 +++- controllers/nginx/nginx/authreq/main.go | 138 ++++++++++++++++ controllers/nginx/nginx/authreq/main_test.go | 114 ++++++++++++++ controllers/nginx/nginx/ingress/nginx.go | 20 +-- controllers/nginx/nginx/main.go | 8 +- controllers/nginx/nginx/template/template.go | 18 +++ controllers/nginx/nginx/utils.go | 13 +- controllers/nginx/nginx/utils_test.go | 10 ++ 13 files changed, 537 insertions(+), 40 deletions(-) create mode 100644 controllers/nginx/examples/external-auth/README.md create mode 100644 controllers/nginx/examples/external-auth/ingress.yaml create mode 100644 controllers/nginx/nginx/authreq/main.go create mode 100644 controllers/nginx/nginx/authreq/main_test.go diff --git a/controllers/nginx/configuration.md b/controllers/nginx/configuration.md index 109e4c7d2..b361fc459 100644 --- a/controllers/nginx/configuration.md +++ b/controllers/nginx/configuration.md @@ -37,17 +37,18 @@ The following annotations are supported: |Name |type| |---------------------------|------| -|[ingress.kubernetes.io/rewrite-target](#rewrite)|URI| |[ingress.kubernetes.io/add-base-url](#rewrite)|true or false| +|[ingress.kubernetes.io/auth-realm](#authentication)|string| +|[ingress.kubernetes.io/auth-secret](#authentication)|string| +|[ingress.kubernetes.io/auth-type](#authentication)|basic or digest| +|[ingress.kubernetes.io/auth-url](#external-authentication)|string| |[ingress.kubernetes.io/limit-connections](#rate-limiting)|number| |[ingress.kubernetes.io/limit-rps](#rate-limiting)|number| -|[ingress.kubernetes.io/auth-type](#authentication)|basic or digest| -|[ingress.kubernetes.io/auth-secret](#authentication)|string| -|[ingress.kubernetes.io/auth-realm](#authentication)|string| +|[ingress.kubernetes.io/rewrite-target](#rewrite)|URI| +|[ingress.kubernetes.io/secure-backends](#secure-backends)|true or false| |[ingress.kubernetes.io/ssl-redirect](#server-side-https-enforcement-through-redirect)|true or false| |[ingress.kubernetes.io/upstream-max-fails](#custom-nginx-upstream-checks)|number| |[ingress.kubernetes.io/upstream-fail-timeout](#custom-nginx-upstream-checks)|number| -|[ingress.kubernetes.io/secure-backends](#secure-backends)|true or false| |[ingress.kubernetes.io/whitelist-source-range](#whitelist-source-range)|CIDR| @@ -119,6 +120,18 @@ ingress.kubernetes.io/auth-realm:"realm string" Please check the [auth](examples/auth/README.md) example +### External Authentication + +To use an existing service that provides authentication the Ingress rule can be annotated with `ingress.kubernetes.io/auth-url` to indicate the URL where the HTTP request should be sent. +Additionally is possible to set `ingress.kubernetes.io/auth-method` to specify the HTTP method to use (GET or POST) and `ingress.kubernetes.io/auth-send-body` to true or false (default). + +``` +ingress.kubernetes.io/auth-url:"URL to the authentication service" +``` + +Please check the [external-auth](examples/external-auth/README.md) example + + ### Rewrite In some scenarios the exposed URL in the backend service differs from the specified path in the Ingress rule. Without a rewrite any request will return 404. diff --git a/controllers/nginx/controller.go b/controllers/nginx/controller.go index c1118e569..097e458b7 100644 --- a/controllers/nginx/controller.go +++ b/controllers/nginx/controller.go @@ -42,6 +42,7 @@ import ( "k8s.io/contrib/ingress/controllers/nginx/nginx" "k8s.io/contrib/ingress/controllers/nginx/nginx/auth" + "k8s.io/contrib/ingress/controllers/nginx/nginx/authreq" "k8s.io/contrib/ingress/controllers/nginx/nginx/config" "k8s.io/contrib/ingress/controllers/nginx/nginx/cors" "k8s.io/contrib/ingress/controllers/nginx/nginx/healthcheck" @@ -723,6 +724,12 @@ func (lbc *loadBalancerController) getUpstreamServers(ngxCfg config.Configuratio glog.V(3).Infof("error reading CORS annotation in Ingress %v/%v: %v", ing.GetNamespace(), ing.GetName(), err) } + ra, err := authreq.ParseAnnotations(ing) + glog.V(3).Infof("nginx auth request %v", ra) + if err != nil { + glog.V(3).Infof("error reading auth request annotation in Ingress %v/%v: %v", ing.GetNamespace(), ing.GetName(), err) + } + host := rule.Host if host == "" { host = defServerName @@ -756,6 +763,7 @@ func (lbc *loadBalancerController) getUpstreamServers(ngxCfg config.Configuratio loc.SecureUpstream = secUpstream loc.Whitelist = *wl loc.EnableCORS = eCORS + loc.ExternalAuthURL = ra addLoc = false continue @@ -771,14 +779,15 @@ func (lbc *loadBalancerController) getUpstreamServers(ngxCfg config.Configuratio if addLoc { server.Locations = append(server.Locations, &ingress.Location{ - Path: nginxPath, - Upstream: *ups, - Auth: *nginxAuth, - RateLimit: *rl, - Redirect: *locRew, - SecureUpstream: secUpstream, - Whitelist: *wl, - EnableCORS: eCORS, + Path: nginxPath, + Upstream: *ups, + Auth: *nginxAuth, + RateLimit: *rl, + Redirect: *locRew, + SecureUpstream: secUpstream, + Whitelist: *wl, + EnableCORS: eCORS, + ExternalAuthURL: ra, }) } } diff --git a/controllers/nginx/examples/external-auth/README.md b/controllers/nginx/examples/external-auth/README.md new file mode 100644 index 000000000..db522c1d2 --- /dev/null +++ b/controllers/nginx/examples/external-auth/README.md @@ -0,0 +1,148 @@ +# External authentication + +### Example 1: + +Use an external service (Basic Auth) located in `https://httpbin.org` + +``` +$ kubectl create -f ingress.yaml +ingress "external-auth" created +$ kubectl get ing external-auth +NAME HOSTS ADDRESS PORTS AGE +external-auth external-auth-01.sample.com 172.17.4.99 80 13s +$ kubectl get ing external-auth -o yaml +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + annotations: + ingress.kubernetes.io/auth-url: https://httpbin.org/basic-auth/user/passwd + creationTimestamp: 2016-10-03T13:50:35Z + generation: 1 + name: external-auth + namespace: default + resourceVersion: "2068378" + selfLink: /apis/extensions/v1beta1/namespaces/default/ingresses/external-auth + uid: 5c388f1d-8970-11e6-9004-080027d2dc94 +spec: + rules: + - host: external-auth-01.sample.com + http: + paths: + - backend: + serviceName: echoheaders + servicePort: 80 + path: / +status: + loadBalancer: + ingress: + - ip: 172.17.4.99 +$ +``` + +Test 1: no username/password (expect code 401) +``` +$ curl -k http://172.17.4.99 -v -H 'Host: external-auth-01.sample.com' +* Rebuilt URL to: http://172.17.4.99/ +* Trying 172.17.4.99... +* Connected to 172.17.4.99 (172.17.4.99) port 80 (#0) +> GET / HTTP/1.1 +> Host: external-auth-01.sample.com +> User-Agent: curl/7.50.1 +> Accept: */* +> +< HTTP/1.1 401 Unauthorized +< Server: nginx/1.11.3 +< Date: Mon, 03 Oct 2016 14:52:08 GMT +< Content-Type: text/html +< Content-Length: 195 +< Connection: keep-alive +< WWW-Authenticate: Basic realm="Fake Realm" +< + +401 Authorization Required + +

401 Authorization Required

+
nginx/1.11.3
+ + +* Connection #0 to host 172.17.4.99 left intact +``` + +Test 2: valid username/password (expect code 200) +``` +$ curl -k http://172.17.4.99 -v -H 'Host: external-auth-01.sample.com' -u 'user:passwd' +* Rebuilt URL to: http://172.17.4.99/ +* Trying 172.17.4.99... +* Connected to 172.17.4.99 (172.17.4.99) port 80 (#0) +* Server auth using Basic with user 'user' +> GET / HTTP/1.1 +> Host: external-auth-01.sample.com +> Authorization: Basic dXNlcjpwYXNzd2Q= +> User-Agent: curl/7.50.1 +> Accept: */* +> +< HTTP/1.1 200 OK +< Server: nginx/1.11.3 +< Date: Mon, 03 Oct 2016 14:52:50 GMT +< Content-Type: text/plain +< Transfer-Encoding: chunked +< Connection: keep-alive +< +CLIENT VALUES: +client_address=10.2.60.2 +command=GET +real path=/ +query=nil +request_version=1.1 +request_uri=http://external-auth-01.sample.com:8080/ + +SERVER VALUES: +server_version=nginx: 1.9.11 - lua: 10001 + +HEADERS RECEIVED: +accept=*/* +authorization=Basic dXNlcjpwYXNzd2Q= +connection=close +host=external-auth-01.sample.com +user-agent=curl/7.50.1 +x-forwarded-for=10.2.60.1 +x-forwarded-host=external-auth-01.sample.com +x-forwarded-port=80 +x-forwarded-proto=http +x-real-ip=10.2.60.1 +BODY: +* Connection #0 to host 172.17.4.99 left intact +-no body in request- +``` + +Test 3: invalid username/password (expect code 401) +``` +curl -k http://172.17.4.99 -v -H 'Host: external-auth-01.sample.com' -u 'user:user' +* Rebuilt URL to: http://172.17.4.99/ +* Trying 172.17.4.99... +* Connected to 172.17.4.99 (172.17.4.99) port 80 (#0) +* Server auth using Basic with user 'user' +> GET / HTTP/1.1 +> Host: external-auth-01.sample.com +> Authorization: Basic dXNlcjp1c2Vy +> User-Agent: curl/7.50.1 +> Accept: */* +> +< HTTP/1.1 401 Unauthorized +< Server: nginx/1.11.3 +< Date: Mon, 03 Oct 2016 14:53:04 GMT +< Content-Type: text/html +< Content-Length: 195 +< Connection: keep-alive +* Authentication problem. Ignoring this. +< WWW-Authenticate: Basic realm="Fake Realm" +< + +401 Authorization Required + +

401 Authorization Required

+
nginx/1.11.3
+ + +* Connection #0 to host 172.17.4.99 left intact +``` diff --git a/controllers/nginx/examples/external-auth/ingress.yaml b/controllers/nginx/examples/external-auth/ingress.yaml new file mode 100644 index 000000000..1cf779ce2 --- /dev/null +++ b/controllers/nginx/examples/external-auth/ingress.yaml @@ -0,0 +1,15 @@ +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + annotations: + ingress.kubernetes.io/auth-url: "https://httpbin.org/basic-auth/user/passwd" + name: external-auth +spec: + rules: + - host: external-auth-01.sample.com + http: + paths: + - backend: + serviceName: echoheaders + servicePort: 80 + path: / \ No newline at end of file diff --git a/controllers/nginx/main.go b/controllers/nginx/main.go index 5b0bc50c4..42d47c0e7 100644 --- a/controllers/nginx/main.go +++ b/controllers/nginx/main.go @@ -95,14 +95,16 @@ func main() { glog.Fatalf("Please specify --default-backend-service") } - config, err := clientConfig.ClientConfig() + kubeClient, err := unversioned.NewInCluster() if err != nil { - glog.Fatalf("error connecting to the client: %v", err) - } - kubeClient, err := unversioned.New(config) - - if err != nil { - glog.Fatalf("failed to create client: %v", err) + config, err := clientConfig.ClientConfig() + if err != nil { + glog.Fatalf("error configuring the client: %v", err) + } + kubeClient, err = unversioned.New(config) + if err != nil { + glog.Fatalf("failed to create client: %v", err) + } } runtimePodInfo, err := getPodDetails(kubeClient) diff --git a/controllers/nginx/nginx.tmpl b/controllers/nginx/nginx.tmpl index 8ee54b8b6..d543cfe99 100644 --- a/controllers/nginx/nginx.tmpl +++ b/controllers/nginx/nginx.tmpl @@ -90,6 +90,7 @@ http { {{ if not (empty .defResolver) }}# Custom dns resolver. resolver {{ .defResolver }} valid=30s; + resolver_timeout 10s; {{ end }} map $http_upgrade $connection_upgrade { @@ -183,27 +184,47 @@ http { server { server_name {{ $server.Name }}; listen 80{{ if $cfg.useProxyProtocol }} proxy_protocol{{ end }}; - {{ if $server.SSL }}listen 443 {{ if $cfg.useProxyProtocol }}proxy_protocol{{ end }} ssl {{ if $cfg.enableSpdy }}spdy{{ end }} {{ if $cfg.useHttp2 }}http2{{ end }}; + {{- if $server.SSL }}listen 443 {{ if $cfg.useProxyProtocol }}proxy_protocol{{ end }} ssl {{ if $cfg.enableSpdy }}spdy{{ end }} {{ if $cfg.useHttp2 }}http2{{ end }}; {{/* comment PEM sha is required to detect changes in the generated configuration and force a reload */}} # PEM sha: {{ $server.SSLPemChecksum }} ssl_certificate {{ $server.SSLCertificate }}; ssl_certificate_key {{ $server.SSLCertificateKey }}; {{- end }} - {{ if (and $server.SSL $cfg.hsts) -}} + {{- if (and $server.SSL $cfg.hsts) }} more_set_headers "Strict-Transport-Security: max-age={{ $cfg.hstsMaxAge }}{{ if $cfg.hstsIncludeSubdomains }}; includeSubDomains{{ end }}; preload"; {{- end }} - {{ if $cfg.enableVtsStatus }}vhost_traffic_status_filter_by_set_key $geoip_country_code country::$server_name;{{ end }} + {{- if $cfg.enableVtsStatus }}vhost_traffic_status_filter_by_set_key $geoip_country_code country::$server_name;{{ end -}} {{- range $location := $server.Locations }} {{ $path := buildLocation $location }} + {{ $authPath := buildAuthLocation $location }} + {{- if not (empty $authPath) }} + location = {{ $authPath }} { + internal; + {{ if not $location.ExternalAuthURL.SendBody }} + proxy_pass_request_body off; + proxy_set_header Content-Length ""; + {{ end -}} + {{ if not (empty $location.ExternalAuthURL.Method) }} + proxy_method {{ $location.ExternalAuthURL.Method }}; + {{ end -}} + proxy_set_header Host $host; + proxy_pass_request_headers on; + proxy_pass {{ $location.ExternalAuthURL.URL }}; + } + {{ end }} location {{ $path }} { {{ if gt (len $location.Whitelist.CIDR) 0 }} {{- range $ip := $location.Whitelist.CIDR }} allow {{ $ip }};{{ end }} deny all; {{ end -}} + {{ if not (empty $authPath) }} + # this location requires authentication + auth_request {{ $authPath }}; + {{ end }} {{ if (and $server.SSL $location.Redirect.SSLRedirect) -}} # enforce ssl on server side @@ -272,7 +293,7 @@ http { {{ if eq $server.Name "_" }} # health checks in cloud providers require the use of port 80 - location {{ $cfg.healthzUrl }} { + location {{ $cfg.HealthzURL }} { access_log off; return 200; } diff --git a/controllers/nginx/nginx/authreq/main.go b/controllers/nginx/nginx/authreq/main.go new file mode 100644 index 000000000..0f53e5bd2 --- /dev/null +++ b/controllers/nginx/nginx/authreq/main.go @@ -0,0 +1,138 @@ +/* +Copyright 2015 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 authreq + +import ( + "errors" + "fmt" + "net/url" + "strconv" + "strings" + + "k8s.io/kubernetes/pkg/apis/extensions" +) + +const ( + // external URL that provides the authentication + authURL = "ingress.kubernetes.io/auth-url" + authMethod = "ingress.kubernetes.io/auth-method" + authBody = "ingress.kubernetes.io/auth-send-body" +) + +var ( + // ErrMissingAnnotations is returned when the ingress rule + // does not contain annotations related with authentication + ErrMissingAnnotations = errors.New("missing authentication annotations") +) + +// Auth returns external authentication configuration for an Ingress rule +type Auth struct { + URL string + Method string + SendBody bool +} + +type ingAnnotations map[string]string + +func (a ingAnnotations) url() (string, error) { + val, ok := a[authURL] + if !ok { + return "", ErrMissingAnnotations + } + + return val, nil +} + +func (a ingAnnotations) method() string { + val, ok := a[authMethod] + if !ok { + return "" + } + + return val +} + +func (a ingAnnotations) sendBody() bool { + val, ok := a[authBody] + if ok { + if b, err := strconv.ParseBool(val); err == nil { + return b + } + } + return false +} + +var ( + methods = []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "CONNECT", "OPTIONS", "TRACE"} +) + +func validMethod(method string) bool { + if len(method) == 0 { + return false + } + + for _, m := range methods { + if method == m { + return true + } + } + return false +} + +// ParseAnnotations parses the annotations contained in the ingress +// rule used to use an external URL as source for authentication +func ParseAnnotations(ing *extensions.Ingress) (Auth, error) { + if ing.GetAnnotations() == nil { + return Auth{}, ErrMissingAnnotations + } + + str, err := ingAnnotations(ing.GetAnnotations()).url() + if err != nil { + return Auth{}, err + } + if str == "" { + return Auth{}, fmt.Errorf("an empty string is not a valid URL") + } + + ur, err := url.Parse(str) + if err != nil { + return Auth{}, err + } + if ur.Scheme == "" { + return Auth{}, fmt.Errorf("url scheme is empty") + } + if ur.Host == "" { + return Auth{}, fmt.Errorf("url host is empty") + } + + if strings.Index(ur.Host, "..") != -1 { + return Auth{}, fmt.Errorf("invalid url host") + } + + m := ingAnnotations(ing.GetAnnotations()).method() + if len(m) != 0 && !validMethod(m) { + return Auth{}, fmt.Errorf("invalid HTTP method") + } + + sb := ingAnnotations(ing.GetAnnotations()).sendBody() + + return Auth{ + URL: str, + Method: m, + SendBody: sb, + }, nil +} diff --git a/controllers/nginx/nginx/authreq/main_test.go b/controllers/nginx/nginx/authreq/main_test.go new file mode 100644 index 000000000..f2936f72b --- /dev/null +++ b/controllers/nginx/nginx/authreq/main_test.go @@ -0,0 +1,114 @@ +/* +Copyright 2015 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 authreq + +import ( + "fmt" + "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()).url() + if err == nil { + t.Error("Expected a validation error") + } + + data := map[string]string{} + ing.SetAnnotations(data) + + tests := []struct { + title string + url string + method string + sendBody bool + expErr bool + }{ + {"empty", "", "", false, true}, + {"no scheme", "bar", "", false, true}, + {"invalid host", "http://", "", false, true}, + {"invalid host (multiple dots)", "http://foo..bar.com", "", false, true}, + {"valid URL", "http://bar.foo.com/external-auth", "", false, false}, + {"valid URL - send body", "http://foo.com/external-auth", "POST", true, false}, + {"valid URL - send body", "http://foo.com/external-auth", "GET", true, false}, + } + + for _, test := range tests { + data[authURL] = test.url + data[authBody] = fmt.Sprintf("%v", test.sendBody) + data[authMethod] = fmt.Sprintf("%v", test.method) + + u, err := ParseAnnotations(ing) + + if test.expErr { + if err == nil { + t.Errorf("%v: expected error but retuned nil", test.title) + } + continue + } + + if u.URL != test.url { + t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.url, u.URL) + } + if u.Method != test.method { + t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.method, u.Method) + } + if u.SendBody != test.sendBody { + t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.sendBody, u.SendBody) + } + } +} diff --git a/controllers/nginx/nginx/ingress/nginx.go b/controllers/nginx/nginx/ingress/nginx.go index 132b7b585..5dfd7dbff 100644 --- a/controllers/nginx/nginx/ingress/nginx.go +++ b/controllers/nginx/nginx/ingress/nginx.go @@ -18,6 +18,7 @@ package ingress import ( "k8s.io/contrib/ingress/controllers/nginx/nginx/auth" + "k8s.io/contrib/ingress/controllers/nginx/nginx/authreq" "k8s.io/contrib/ingress/controllers/nginx/nginx/ipwhitelist" "k8s.io/contrib/ingress/controllers/nginx/nginx/ratelimit" "k8s.io/contrib/ingress/controllers/nginx/nginx/rewrite" @@ -93,15 +94,16 @@ func (c ServerByName) Less(i, j int) bool { // Location describes an NGINX location type Location struct { - Path string - IsDefBackend bool - Upstream Upstream - Auth auth.Nginx - RateLimit ratelimit.RateLimit - Redirect rewrite.Redirect - SecureUpstream bool - Whitelist ipwhitelist.SourceRange - EnableCORS bool + Path string + IsDefBackend bool + Upstream Upstream + Auth auth.Nginx + RateLimit ratelimit.RateLimit + Redirect rewrite.Redirect + SecureUpstream bool + Whitelist ipwhitelist.SourceRange + EnableCORS bool + ExternalAuthURL authreq.Auth } // LocationByPath sorts location by path diff --git a/controllers/nginx/nginx/main.go b/controllers/nginx/nginx/main.go index 438e26415..944181b1d 100644 --- a/controllers/nginx/nginx/main.go +++ b/controllers/nginx/nginx/main.go @@ -71,17 +71,21 @@ func NewManager(kubeClient *client.Client) *Manager { ngx := &Manager{ ConfigFile: "/etc/nginx/nginx.conf", defCfg: config.NewDefault(), - defResolver: strings.Join(getDNSServers(), " "), reloadLock: &sync.Mutex{}, reloadRateLimiter: flowcontrol.NewTokenBucketRateLimiter(0.1, 1), } + res, err := getDNSServers() + if err != nil { + glog.Warningf("error reading nameservers: %v", err) + } + ngx.defResolver = strings.Join(res, " ") + ngx.createCertsDir(config.SSLDirectory) ngx.sslDHParam = ngx.SearchDHParamFile(config.SSLDirectory) var onChange func() - onChange = func() { template, err := ngx_template.NewTemplate(tmplPath, onChange) if err != nil { diff --git a/controllers/nginx/nginx/template/template.go b/controllers/nginx/nginx/template/template.go index d4f22a371..a7e7a18a1 100644 --- a/controllers/nginx/nginx/template/template.go +++ b/controllers/nginx/nginx/template/template.go @@ -18,6 +18,7 @@ package template import ( "bytes" + "encoding/base64" "encoding/json" "fmt" "regexp" @@ -49,6 +50,7 @@ var ( return true }, "buildLocation": buildLocation, + "buildAuthLocation": buildAuthLocation, "buildProxyPass": buildProxyPass, "buildRateLimitZones": buildRateLimitZones, "buildRateLimit": buildRateLimit, @@ -193,6 +195,22 @@ func buildLocation(input interface{}) string { return path } +func buildAuthLocation(input interface{}) string { + location, ok := input.(*ingress.Location) + if !ok { + return "" + } + + if location.ExternalAuthURL.URL == "" { + return "" + } + + str := base64.URLEncoding.EncodeToString([]byte(location.Path)) + // avoid locations containing the = char + str = strings.Replace(str, "=", "", -1) + return fmt.Sprintf("/_external-auth-%v", str) +} + // buildProxyPass produces the proxy pass string, if the ingress has redirects // (specified through the ingress.kubernetes.io/rewrite-to annotation) // If the annotation ingress.kubernetes.io/add-base-url:"true" is specified it will diff --git a/controllers/nginx/nginx/utils.go b/controllers/nginx/nginx/utils.go index 5bc9d3533..40a6dffd6 100644 --- a/controllers/nginx/nginx/utils.go +++ b/controllers/nginx/nginx/utils.go @@ -38,15 +38,14 @@ const ( ) // getDNSServers returns the list of nameservers located in the file /etc/resolv.conf -func getDNSServers() []string { +func getDNSServers() ([]string, error) { + var nameservers []string file, err := ioutil.ReadFile("/etc/resolv.conf") if err != nil { - return []string{} + return nameservers, err } // Lines of the form "nameserver 1.2.3.4" accumulate. - nameservers := []string{} - lines := strings.Split(string(file), "\n") for l := range lines { trimmed := strings.TrimSpace(lines[l]) @@ -63,7 +62,7 @@ func getDNSServers() []string { } glog.V(3).Infof("nameservers to use: %v", nameservers) - return nameservers + return nameservers, nil } // getConfigKeyToStructKeyMap returns a map with the ConfigMapKey as key and the StructName as value. @@ -143,6 +142,10 @@ func (ngx *Manager) ReadConfig(conf *api.ConfigMap) config.Configuration { cfgDefault.CustomHTTPErrors = ngx.filterErrors(cErrors) cfgDefault.SkipAccessLogURLs = cSkipUrls + // no custom resolver means use the system resolver + if cfgDefault.Resolver == "" { + cfgDefault.Resolver = ngx.defResolver + } return cfgDefault } diff --git a/controllers/nginx/nginx/utils_test.go b/controllers/nginx/nginx/utils_test.go index 294b3dd01..e9c37edd0 100644 --- a/controllers/nginx/nginx/utils_test.go +++ b/controllers/nginx/nginx/utils_test.go @@ -89,3 +89,13 @@ func TestManagerReadConfigStringNothing(t *testing.T) { t.Errorf("Failed to set string value true actual='%s' expected='%s'", configNginx.SSLSessionTimeout, exp) } } + +func TestGetDNSServers(t *testing.T) { + s, err := getDNSServers() + if err != nil { + t.Fatalf("unexpected error reading /etc/resolv.conf file: %v", err) + } + if len(s) < 1 { + t.Error("expected at least 1 nameserver in /etc/resolv.conf") + } +}