Add external authentication using auth_request
This commit is contained in:
parent
25bf00a1fc
commit
541928e27d
13 changed files with 537 additions and 40 deletions
|
@ -37,17 +37,18 @@ The following annotations are supported:
|
||||||
|
|
||||||
|Name |type|
|
|Name |type|
|
||||||
|---------------------------|------|
|
|---------------------------|------|
|
||||||
|[ingress.kubernetes.io/rewrite-target](#rewrite)|URI|
|
|
||||||
|[ingress.kubernetes.io/add-base-url](#rewrite)|true or false|
|
|[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-connections](#rate-limiting)|number|
|
||||||
|[ingress.kubernetes.io/limit-rps](#rate-limiting)|number|
|
|[ingress.kubernetes.io/limit-rps](#rate-limiting)|number|
|
||||||
|[ingress.kubernetes.io/auth-type](#authentication)|basic or digest|
|
|[ingress.kubernetes.io/rewrite-target](#rewrite)|URI|
|
||||||
|[ingress.kubernetes.io/auth-secret](#authentication)|string|
|
|[ingress.kubernetes.io/secure-backends](#secure-backends)|true or false|
|
||||||
|[ingress.kubernetes.io/auth-realm](#authentication)|string|
|
|
||||||
|[ingress.kubernetes.io/ssl-redirect](#server-side-https-enforcement-through-redirect)|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-max-fails](#custom-nginx-upstream-checks)|number|
|
||||||
|[ingress.kubernetes.io/upstream-fail-timeout](#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|
|
|[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
|
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
|
### 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.
|
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.
|
||||||
|
|
|
@ -42,6 +42,7 @@ import (
|
||||||
|
|
||||||
"k8s.io/contrib/ingress/controllers/nginx/nginx"
|
"k8s.io/contrib/ingress/controllers/nginx/nginx"
|
||||||
"k8s.io/contrib/ingress/controllers/nginx/nginx/auth"
|
"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/config"
|
||||||
"k8s.io/contrib/ingress/controllers/nginx/nginx/cors"
|
"k8s.io/contrib/ingress/controllers/nginx/nginx/cors"
|
||||||
"k8s.io/contrib/ingress/controllers/nginx/nginx/healthcheck"
|
"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)
|
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
|
host := rule.Host
|
||||||
if host == "" {
|
if host == "" {
|
||||||
host = defServerName
|
host = defServerName
|
||||||
|
@ -756,6 +763,7 @@ func (lbc *loadBalancerController) getUpstreamServers(ngxCfg config.Configuratio
|
||||||
loc.SecureUpstream = secUpstream
|
loc.SecureUpstream = secUpstream
|
||||||
loc.Whitelist = *wl
|
loc.Whitelist = *wl
|
||||||
loc.EnableCORS = eCORS
|
loc.EnableCORS = eCORS
|
||||||
|
loc.ExternalAuthURL = ra
|
||||||
|
|
||||||
addLoc = false
|
addLoc = false
|
||||||
continue
|
continue
|
||||||
|
@ -779,6 +787,7 @@ func (lbc *loadBalancerController) getUpstreamServers(ngxCfg config.Configuratio
|
||||||
SecureUpstream: secUpstream,
|
SecureUpstream: secUpstream,
|
||||||
Whitelist: *wl,
|
Whitelist: *wl,
|
||||||
EnableCORS: eCORS,
|
EnableCORS: eCORS,
|
||||||
|
ExternalAuthURL: ra,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
148
controllers/nginx/examples/external-auth/README.md
Normal file
148
controllers/nginx/examples/external-auth/README.md
Normal file
|
@ -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"
|
||||||
|
<
|
||||||
|
<html>
|
||||||
|
<head><title>401 Authorization Required</title></head>
|
||||||
|
<body bgcolor="white">
|
||||||
|
<center><h1>401 Authorization Required</h1></center>
|
||||||
|
<hr><center>nginx/1.11.3</center>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
* 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"
|
||||||
|
<
|
||||||
|
<html>
|
||||||
|
<head><title>401 Authorization Required</title></head>
|
||||||
|
<body bgcolor="white">
|
||||||
|
<center><h1>401 Authorization Required</h1></center>
|
||||||
|
<hr><center>nginx/1.11.3</center>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
* Connection #0 to host 172.17.4.99 left intact
|
||||||
|
```
|
15
controllers/nginx/examples/external-auth/ingress.yaml
Normal file
15
controllers/nginx/examples/external-auth/ingress.yaml
Normal file
|
@ -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: /
|
|
@ -95,15 +95,17 @@ func main() {
|
||||||
glog.Fatalf("Please specify --default-backend-service")
|
glog.Fatalf("Please specify --default-backend-service")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kubeClient, err := unversioned.NewInCluster()
|
||||||
|
if err != nil {
|
||||||
config, err := clientConfig.ClientConfig()
|
config, err := clientConfig.ClientConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Fatalf("error connecting to the client: %v", err)
|
glog.Fatalf("error configuring the client: %v", err)
|
||||||
}
|
}
|
||||||
kubeClient, err := unversioned.New(config)
|
kubeClient, err = unversioned.New(config)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Fatalf("failed to create client: %v", err)
|
glog.Fatalf("failed to create client: %v", err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
runtimePodInfo, err := getPodDetails(kubeClient)
|
runtimePodInfo, err := getPodDetails(kubeClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -90,6 +90,7 @@ http {
|
||||||
|
|
||||||
{{ if not (empty .defResolver) }}# Custom dns resolver.
|
{{ if not (empty .defResolver) }}# Custom dns resolver.
|
||||||
resolver {{ .defResolver }} valid=30s;
|
resolver {{ .defResolver }} valid=30s;
|
||||||
|
resolver_timeout 10s;
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
map $http_upgrade $connection_upgrade {
|
map $http_upgrade $connection_upgrade {
|
||||||
|
@ -183,27 +184,47 @@ http {
|
||||||
server {
|
server {
|
||||||
server_name {{ $server.Name }};
|
server_name {{ $server.Name }};
|
||||||
listen 80{{ if $cfg.useProxyProtocol }} proxy_protocol{{ end }};
|
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 */}}
|
{{/* comment PEM sha is required to detect changes in the generated configuration and force a reload */}}
|
||||||
# PEM sha: {{ $server.SSLPemChecksum }}
|
# PEM sha: {{ $server.SSLPemChecksum }}
|
||||||
ssl_certificate {{ $server.SSLCertificate }};
|
ssl_certificate {{ $server.SSLCertificate }};
|
||||||
ssl_certificate_key {{ $server.SSLCertificateKey }};
|
ssl_certificate_key {{ $server.SSLCertificateKey }};
|
||||||
{{- end }}
|
{{- 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";
|
more_set_headers "Strict-Transport-Security: max-age={{ $cfg.hstsMaxAge }}{{ if $cfg.hstsIncludeSubdomains }}; includeSubDomains{{ end }}; preload";
|
||||||
{{- end }}
|
{{- 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 }}
|
{{- range $location := $server.Locations }}
|
||||||
{{ $path := buildLocation $location }}
|
{{ $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 }} {
|
location {{ $path }} {
|
||||||
{{ if gt (len $location.Whitelist.CIDR) 0 }}
|
{{ if gt (len $location.Whitelist.CIDR) 0 }}
|
||||||
{{- range $ip := $location.Whitelist.CIDR }}
|
{{- range $ip := $location.Whitelist.CIDR }}
|
||||||
allow {{ $ip }};{{ end }}
|
allow {{ $ip }};{{ end }}
|
||||||
deny all;
|
deny all;
|
||||||
{{ end -}}
|
{{ end -}}
|
||||||
|
{{ if not (empty $authPath) }}
|
||||||
|
# this location requires authentication
|
||||||
|
auth_request {{ $authPath }};
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
{{ if (and $server.SSL $location.Redirect.SSLRedirect) -}}
|
{{ if (and $server.SSL $location.Redirect.SSLRedirect) -}}
|
||||||
# enforce ssl on server side
|
# enforce ssl on server side
|
||||||
|
@ -272,7 +293,7 @@ http {
|
||||||
|
|
||||||
{{ if eq $server.Name "_" }}
|
{{ if eq $server.Name "_" }}
|
||||||
# health checks in cloud providers require the use of port 80
|
# health checks in cloud providers require the use of port 80
|
||||||
location {{ $cfg.healthzUrl }} {
|
location {{ $cfg.HealthzURL }} {
|
||||||
access_log off;
|
access_log off;
|
||||||
return 200;
|
return 200;
|
||||||
}
|
}
|
||||||
|
|
138
controllers/nginx/nginx/authreq/main.go
Normal file
138
controllers/nginx/nginx/authreq/main.go
Normal file
|
@ -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
|
||||||
|
}
|
114
controllers/nginx/nginx/authreq/main_test.go
Normal file
114
controllers/nginx/nginx/authreq/main_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ package ingress
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"k8s.io/contrib/ingress/controllers/nginx/nginx/auth"
|
"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/ipwhitelist"
|
||||||
"k8s.io/contrib/ingress/controllers/nginx/nginx/ratelimit"
|
"k8s.io/contrib/ingress/controllers/nginx/nginx/ratelimit"
|
||||||
"k8s.io/contrib/ingress/controllers/nginx/nginx/rewrite"
|
"k8s.io/contrib/ingress/controllers/nginx/nginx/rewrite"
|
||||||
|
@ -102,6 +103,7 @@ type Location struct {
|
||||||
SecureUpstream bool
|
SecureUpstream bool
|
||||||
Whitelist ipwhitelist.SourceRange
|
Whitelist ipwhitelist.SourceRange
|
||||||
EnableCORS bool
|
EnableCORS bool
|
||||||
|
ExternalAuthURL authreq.Auth
|
||||||
}
|
}
|
||||||
|
|
||||||
// LocationByPath sorts location by path
|
// LocationByPath sorts location by path
|
||||||
|
|
|
@ -71,17 +71,21 @@ func NewManager(kubeClient *client.Client) *Manager {
|
||||||
ngx := &Manager{
|
ngx := &Manager{
|
||||||
ConfigFile: "/etc/nginx/nginx.conf",
|
ConfigFile: "/etc/nginx/nginx.conf",
|
||||||
defCfg: config.NewDefault(),
|
defCfg: config.NewDefault(),
|
||||||
defResolver: strings.Join(getDNSServers(), " "),
|
|
||||||
reloadLock: &sync.Mutex{},
|
reloadLock: &sync.Mutex{},
|
||||||
reloadRateLimiter: flowcontrol.NewTokenBucketRateLimiter(0.1, 1),
|
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.createCertsDir(config.SSLDirectory)
|
||||||
|
|
||||||
ngx.sslDHParam = ngx.SearchDHParamFile(config.SSLDirectory)
|
ngx.sslDHParam = ngx.SearchDHParamFile(config.SSLDirectory)
|
||||||
|
|
||||||
var onChange func()
|
var onChange func()
|
||||||
|
|
||||||
onChange = func() {
|
onChange = func() {
|
||||||
template, err := ngx_template.NewTemplate(tmplPath, onChange)
|
template, err := ngx_template.NewTemplate(tmplPath, onChange)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -18,6 +18,7 @@ package template
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
@ -49,6 +50,7 @@ var (
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
"buildLocation": buildLocation,
|
"buildLocation": buildLocation,
|
||||||
|
"buildAuthLocation": buildAuthLocation,
|
||||||
"buildProxyPass": buildProxyPass,
|
"buildProxyPass": buildProxyPass,
|
||||||
"buildRateLimitZones": buildRateLimitZones,
|
"buildRateLimitZones": buildRateLimitZones,
|
||||||
"buildRateLimit": buildRateLimit,
|
"buildRateLimit": buildRateLimit,
|
||||||
|
@ -193,6 +195,22 @@ func buildLocation(input interface{}) string {
|
||||||
return path
|
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
|
// buildProxyPass produces the proxy pass string, if the ingress has redirects
|
||||||
// (specified through the ingress.kubernetes.io/rewrite-to annotation)
|
// (specified through the ingress.kubernetes.io/rewrite-to annotation)
|
||||||
// If the annotation ingress.kubernetes.io/add-base-url:"true" is specified it will
|
// If the annotation ingress.kubernetes.io/add-base-url:"true" is specified it will
|
||||||
|
|
|
@ -38,15 +38,14 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
// getDNSServers returns the list of nameservers located in the file /etc/resolv.conf
|
// 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")
|
file, err := ioutil.ReadFile("/etc/resolv.conf")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []string{}
|
return nameservers, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lines of the form "nameserver 1.2.3.4" accumulate.
|
// Lines of the form "nameserver 1.2.3.4" accumulate.
|
||||||
nameservers := []string{}
|
|
||||||
|
|
||||||
lines := strings.Split(string(file), "\n")
|
lines := strings.Split(string(file), "\n")
|
||||||
for l := range lines {
|
for l := range lines {
|
||||||
trimmed := strings.TrimSpace(lines[l])
|
trimmed := strings.TrimSpace(lines[l])
|
||||||
|
@ -63,7 +62,7 @@ func getDNSServers() []string {
|
||||||
}
|
}
|
||||||
|
|
||||||
glog.V(3).Infof("nameservers to use: %v", nameservers)
|
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.
|
// 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.CustomHTTPErrors = ngx.filterErrors(cErrors)
|
||||||
cfgDefault.SkipAccessLogURLs = cSkipUrls
|
cfgDefault.SkipAccessLogURLs = cSkipUrls
|
||||||
|
// no custom resolver means use the system resolver
|
||||||
|
if cfgDefault.Resolver == "" {
|
||||||
|
cfgDefault.Resolver = ngx.defResolver
|
||||||
|
}
|
||||||
return cfgDefault
|
return cfgDefault
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -89,3 +89,13 @@ func TestManagerReadConfigStringNothing(t *testing.T) {
|
||||||
t.Errorf("Failed to set string value true actual='%s' expected='%s'", configNginx.SSLSessionTimeout, exp)
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue