diff --git a/controllers/nginx/Makefile b/controllers/nginx/Makefile index 8bee35692..8805e2d2a 100644 --- a/controllers/nginx/Makefile +++ b/controllers/nginx/Makefile @@ -6,6 +6,7 @@ BUILDTAGS= RELEASE?=0.9.0-beta.2 PREFIX?=gcr.io/google_containers/nginx-ingress-controller GOOS?=linux +DOCKER?=gcloud docker -- REPO_INFO=$(shell git config --get remote.origin.url) @@ -21,10 +22,10 @@ build: clean -o rootfs/nginx-ingress-controller ${PKG}/pkg/cmd/controller container: build - docker build --pull -t $(PREFIX):$(RELEASE) rootfs + $(DOCKER) build --pull -t $(PREFIX):$(RELEASE) rootfs push: container - gcloud docker -- push $(PREFIX):$(RELEASE) + $(DOCKER) push $(PREFIX):$(RELEASE) fmt: @echo "+ $@" diff --git a/controllers/nginx/rootfs/etc/nginx/template/nginx.tmpl b/controllers/nginx/rootfs/etc/nginx/template/nginx.tmpl index 0f3dafbd8..9813bcd24 100644 --- a/controllers/nginx/rootfs/etc/nginx/template/nginx.tmpl +++ b/controllers/nginx/rootfs/etc/nginx/template/nginx.tmpl @@ -246,6 +246,8 @@ http { {{ end }} {{ if not (empty $location.ExternalAuth.Method) }} proxy_method {{ $location.ExternalAuth.Method }}; + proxy_set_header X-Original-URI $request_uri; + proxy_set_header X-Scheme $pass_access_scheme; {{ end }} proxy_set_header Host $host; proxy_pass_request_headers on; @@ -271,6 +273,10 @@ http { auth_request {{ $authPath }}; {{ end }} + {{ if not (empty $location.ExternalAuth.SigninURL) }} + error_page 401 = {{ $location.ExternalAuth.SigninURL }}; + {{ end }} + {{ if (or $location.Redirect.ForceSSLRedirect (and (not (empty $server.SSLCertificate)) $location.Redirect.SSLRedirect)) }} # enforce ssl on server side if ($pass_access_scheme = http) { @@ -317,6 +323,8 @@ http { proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $pass_port; proxy_set_header X-Forwarded-Proto $pass_access_scheme; + proxy_set_header X-Original-URI $request_uri; + proxy_set_header X-Scheme $pass_access_scheme; # mitigate HTTPoxy Vulnerability # https://www.nginx.com/blog/mitigating-the-httpoxy-vulnerability-with-nginx/ diff --git a/core/pkg/ingress/annotations/authreq/main.go b/core/pkg/ingress/annotations/authreq/main.go index 560a73868..91c56b9f6 100644 --- a/core/pkg/ingress/annotations/authreq/main.go +++ b/core/pkg/ingress/annotations/authreq/main.go @@ -28,16 +28,18 @@ import ( 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" + authURL = "ingress.kubernetes.io/auth-url" + authSigninURL = "ingress.kubernetes.io/auth-signin" + authMethod = "ingress.kubernetes.io/auth-method" + authBody = "ingress.kubernetes.io/auth-send-body" ) // External returns external authentication configuration for an Ingress rule type External struct { - URL string `json:"url"` - Method string `json:"method"` - SendBody bool `json:"sendBody"` + URL string `json:"url"` + SigninURL string `json:"signinUrl"` + Method string `json:"method"` + SendBody bool `json:"sendBody"` } var ( @@ -77,6 +79,8 @@ func (a authReq) Parse(ing *extensions.Ingress) (interface{}, error) { return nil, ing_errors.NewLocationDenied("an empty string is not a valid URL") } + signin, _ := parser.GetStringAnnotation(authSigninURL, ing) + ur, err := url.Parse(str) if err != nil { return nil, err @@ -100,8 +104,9 @@ func (a authReq) Parse(ing *extensions.Ingress) (interface{}, error) { sb, _ := parser.GetBoolAnnotation(authBody, ing) return &External{ - URL: str, - Method: m, - SendBody: sb, + URL: str, + SigninURL: signin, + Method: m, + SendBody: sb, }, nil } diff --git a/core/pkg/ingress/annotations/authreq/main_test.go b/core/pkg/ingress/annotations/authreq/main_test.go index 696d8bdc0..75cd6d2b7 100644 --- a/core/pkg/ingress/annotations/authreq/main_test.go +++ b/core/pkg/ingress/annotations/authreq/main_test.go @@ -67,23 +67,25 @@ func TestAnnotations(t *testing.T) { ing.SetAnnotations(data) tests := []struct { - title string - url string - method string - sendBody bool - expErr bool + title string + url string + signinURL 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}, + {"empty", "", "", "", false, true}, + {"no scheme", "bar", "bar", "", false, true}, + {"invalid host", "http://", "http://", "", false, true}, + {"invalid host (multiple dots)", "http://foo..bar.com", "http://foo..bar.com", "", false, true}, + {"valid URL", "http://bar.foo.com/external-auth", "http://bar.foo.com/external-auth", "", false, false}, + {"valid URL - send body", "http://foo.com/external-auth", "http://foo.com/external-auth", "POST", true, false}, + {"valid URL - send body", "http://foo.com/external-auth", "http://foo.com/external-auth", "GET", true, false}, } for _, test := range tests { data[authURL] = test.url + data[authSigninURL] = test.signinURL data[authBody] = fmt.Sprintf("%v", test.sendBody) data[authMethod] = fmt.Sprintf("%v", test.method) @@ -101,6 +103,9 @@ func TestAnnotations(t *testing.T) { if u.URL != test.url { t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.url, u.URL) } + if u.SigninURL != test.signinURL { + t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.signinURL, u.SigninURL) + } if u.Method != test.method { t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.title, test.method, u.Method) } diff --git a/examples/README.md b/examples/README.md index 330e82a0a..69288dd45 100644 --- a/examples/README.md +++ b/examples/README.md @@ -57,7 +57,7 @@ SNI + TCP | TLS routing based on SNI hostname | nginx | Advanced Name | Description | Platform | Complexity Level -----| ----------- | ---------- | ---------------- Basic auth | password protect your website | nginx | Intermediate -External auth plugin | defer to an external auth service | nginx | Intermediate +[External auth plugin](external-auth/nginx/README.md) | defer to an external auth service | nginx | Intermediate ## Protocols diff --git a/examples/external-auth/nginx/README.md b/examples/external-auth/nginx/README.md new file mode 100644 index 000000000..d61e395d4 --- /dev/null +++ b/examples/external-auth/nginx/README.md @@ -0,0 +1,64 @@ +## External Authentication + +### Overview + +The `auth-url` and `auth-signin` annotations allow you to use an external +authentication provider to protect your Ingress resources. + +(Note, this annotation requires `nginx-ingress-controller v0.9.0` or greater.) + +### Key Detail + +This functionality is enabled by deploying multiple Ingress objects for a single host. +One Ingress object has no special annotations and handles authentication. + +Other Ingress objects can then be annotated in such a way that require the user to +authenticate against the first Ingress's endpoint, and can redirect `401`s to the +same endpoint. + +Sample: + +``` +... +metadata: + name: application + annotations: + "ingress.kubernetes.io/auth-url": "https://$host/oauth2/auth" + "ingress.kubernetes.io/signin-url": "https://$host/oauth2/sign_in" +... +``` + +### Example: OAuth2 Proxy + Kubernetes-Dashboard + +This example will show you how to deploy [`oauth2_proxy`](https://github.com/bitly/oauth2_proxy) +into a Kubernetes cluster and use it to protect the Kubernetes Dashboard. + +#### Prepare: + +1. Install the kubernetes dashboard + +```console +kubectl create -f https://raw.githubusercontent.com/kubernetes/kops/master/addons/kubernetes-dashboard/v1.5.0.yaml +``` + +2. Create a custom Github OAuth application https://github.com/settings/applications/new + +- Homepage URL is the FQDN in the Ingress rule, like `https://foo.bar.com` +- Authorization callback URL is the same as the base FQDN plus `/oauth2`, like `https://foo.bar.com/oauth2` + +3. Configure oauth2_proxy values in the file oauth2-proxy.yaml with the values: + +- OAUTH2_PROXY_CLIENT_ID with the github `` +- OAUTH2_PROXY_CLIENT_SECRET with the github `` +- OAUTH2_PROXY_COOKIE_SECRET with value of `python -c 'import os,base64; print base64.b64encode(os.urandom(16))'` + +4. Customize the contents of the file dashboard-ingress.yaml: + +Replace `__INGRESS_HOST__` with a valid FQDN and `__INGRESS_SECRET__` with a Secret with a valid SSL certificate. + +5. Deploy the oauth2 proxy and the ingress rules running: +```console +$ kubectl create -f oauth2-proxy.yaml,dashboard-ingress.yaml +``` + +Test the oauth integration accessing the configured URL, like `https://foo.bar.com` diff --git a/examples/external-auth/nginx/dashboard-ingress.yaml b/examples/external-auth/nginx/dashboard-ingress.yaml new file mode 100644 index 000000000..642e38f5b --- /dev/null +++ b/examples/external-auth/nginx/dashboard-ingress.yaml @@ -0,0 +1,38 @@ +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + annotations: + ingress.kubernetes.io/auth-signin: https://$host/oauth2/sign_in + ingress.kubernetes.io/auth-url: https://$host/oauth2/auth + name: external-auth-oauth2 + namespace: kube-system +spec: + rules: + - host: __INGRESS_HOST__ + http: + paths: + - backend: + serviceName: kubernetes-dashboard + servicePort: 80 + path: / + +--- + +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: oauth2-proxy + namespace: kube-system +spec: + rules: + - host: __INGRESS_HOST__ + http: + paths: + - backend: + serviceName: oauth2-proxy + servicePort: 4180 + path: /oauth2 + tls: + - hosts: + - __INGRESS_HOST__ + secretName: __INGRESS_SECRET__ diff --git a/examples/external-auth/nginx/oauth2-proxy.yaml b/examples/external-auth/nginx/oauth2-proxy.yaml new file mode 100644 index 000000000..1735f4690 --- /dev/null +++ b/examples/external-auth/nginx/oauth2-proxy.yaml @@ -0,0 +1,56 @@ +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + labels: + k8s-app: oauth2-proxy + name: oauth2-proxy + namespace: kube-system +spec: + replicas: 1 + selector: + matchLabels: + k8s-app: oauth2-proxy + template: + metadata: + labels: + k8s-app: oauth2-proxy + spec: + containers: + - args: + - --provider=github + - --email-domain=* + - --upstream=file:///dev/null + - --http-address=0.0.0.0:4180 + # Register a new application + # https://github.com/settings/applications/new + env: + - name: OAUTH2_PROXY_CLIENT_ID + value: + - name: OAUTH2_PROXY_CLIENT_SECRET + value: + # python -c 'import os,base64; print base64.b64encode(os.urandom(16))' + - name: OAUTH2_PROXY_COOKIE_SECRET + value: SECRET + image: docker.io/colemickens/oauth2_proxy:latest + imagePullPolicy: Always + name: oauth2-proxy + ports: + - containerPort: 4180 + protocol: TCP + +--- + +apiVersion: v1 +kind: Service +metadata: + labels: + k8s-app: oauth2-proxy + name: oauth2-proxy +spec: + ports: + - name: http + port: 4180 + protocol: TCP + targetPort: 4180 + selector: + k8s-app: oauth2-proxy