Single Sign-On Part 3 - oauth2-proxy
In my last post, I stated that I would be moving on to deploying the first “real” application which will bring all of this together. In fact, I decided that Nextcloud would be that first deployment. However, while reading the installation documentation and looking at how it would integrate with an external authentication provider, I decided that I needed to learn how authentication works on the web works a little better.
I decided to figure out how take an existing “test” workload such as whoami and try to secure it with Keycloak. That led me to learn about oauth2-proxy, the Kubernetes nginx-ingress (which is different than the nginx Kuberntees ingress controller even though the names are similar), and external authentication.
Kubernetes nginx-ingress allows for external authentication through the use of annotations:
- nginx.ingress.kubernetes.io/auth-url - the location where a subrequest for authorization should be made and the response code checked
- nginx.ingress.kubernetes.io/auth-signin - the location of the error page to sent the browser if the subrequest returns a 4xx (unauthorized) response code.
To put this into practice, the whoami ingress rule looks something like this:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
cert-manager.io/cluster-issuer: letsencrypt
field.cattle.io/ingressState: '{"d2999W1pL2RlZmF1bHQvd2hvYW1pLm5lcmR3b3Jrcy5jYXNhLy8vODA=":"deployment:default:whoami","d2hvYW1pLW999R3b3Jrcy1jZXJ0":"default:whoami-domain-cert"}'
field.cattle.io/publicEndpoints: '[{"addresses":["192.168.xx.xx"],"port":443,"protocol":"HTTPS","serviceName":"default:ingress-9995efd1aa4150e3f4772fb2a0f14548","ingressName":"default:whoami","hostname":"whoami.domain.tld","path":"/","allNodes":true}]'
creationTimestamp: "2020-03-18T19:32:53Z"
generation: 15
labels:
cattle.io/creator: norman
name: whoami
namespace: default
resourceVersion: "410059"
selfLink: /apis/extensions/v1beta1/namespaces/default/ingresses/whoami
uid: 1f09946b-5d71-49a9-a49c-01e2e7c21cd8
spec:
rules:
- host: whoami.domain.tld
http:
paths:
- backend:
serviceName: whoami
servicePort: 80
path: /
tls:
- hosts:
- whoami.domain.tld
secretName: whoami-domain-cert
status:
loadBalancer:
ingress:
- ip: 192.168.xx.xx
To add external authentication, I would add annotations like this:
metadata:
annotations:
nginx.ingress.kubernetes.io/auth-url: "https://$host/oauth2/auth"
nginx.ingress.kubernetes.io/auth-signin: "https://$host/oauth2/start?rd=$escaped_request_uri"
These annotations tells nginx-ingress that the user should be authenticated by redirecting the browser to https://whoami.domain.tld/oauth2/auth for authorization. If the response back is a 2xx response code, the request continues to the workload. If the responde code is 4xx (unauthorized) then then request is redirected to the error page in auth-signin.
OAuth2-Proxy
OAuth2-proxy is a small reverse proxy which can be used to integrate with an oauth2 and/or OpenID Connect external authentication provider for web applications which do not support it natively.
I can use oauth2-proxy to secure the whoami service by creating another path to the ingress rule which sends requests for whoami.domain.tld/oauth2 to port 4180 of the oauth2-proxy service.
First, I want to create a new client in the Keycloak under the domain realm, clients, new:
- Client ID - oauth2-proxy
- Client Protocol - openid-connect
- Root URL - empty
Then add additional configuration under settings:
- Name - oauth2-proxy
- Access Type - confidential
- Valid Redirect URIs - https://whoami.domain.tld/oauth2/callback
Create a mapper with a Mapper Type of ‘Group Membership’ and Token Claim Name ‘groups’ and set “Set Full Group Path” to false to simplify things. Then I create a group for users which can be authenticated with this oauth2-proxy workload which I creatively called ‘oauth2-proxy’.
Finally, I go to the Installation Tab and select the JSON format in order to grab the client secret and use it in the oauth2-proxy client secret key.
Next, I’ll configure the oauth2-proxy workload which starts with creating an oauth2-proxy-realm-secret in the same namespace as whoami to store the environment variables keys and values:
- OAUTH2_PROXY_PROVIDER - keycloak
- OAUTH2_PROXY_CLIENT_ID - oauth2-proxy
- OAUTH2_PROXY_CLIENT_SECRET -
- OAUTH2_PROXY_HTTP_ADDRESS= http://:4180
- OATH2_PROXY_UPSTREAM=file:///dev/null
- OAUTH2_PROXY_LOGIN_URL - http://sso.domain.tld/realms/domain/protocol/openid-connect/auth
- OAUTH2_PROXY_REDEEM_URL - http://sso.domain.tld/realms/domain/protocol/openid-connect/token
- OAUTH2_PROXY_VALIDATE_URL - http://sso.domain.tld/realms/domain/protocol/openid-connect/userinfo
- OAUTH2_PROXY_KEYCLOAK_GROUP - oauth2-proxy
- OAUTH2_PROXY_EMAIL_DOMAINS - *
- OAUTH2_PROXY_COOKIE_SECRETS - <generate value by executing docker run -ti –rm python:3-alpine python -c ‘import secrets,base64; print(base64.b64encode(base64.b64encode(secrets.token_bytes(16))));'>
I need to define an oauth2-proxy workload in the same namespace of the service to be secured:
- Name - oauth2-proxy-realm
- Docker Image - quay.io/oauth2-proxy/oauth2-proxy
- Namespace -
- Port Mapping - keycloak, 4180, TCP, ClusterIP
- Add the secret under Environment Variables
Finally, add a new ingress which matches whoami, but with the /oauth2 path to the whoami ingress rule which redirect to port 4180 of the oauth2-proxy workload. It will not have the external authentication annotations:
- Name - whoami-oauth2-proxy
- Namespace -
- Hostname - whoami.domain.tld
- Path - /oauth2
- Target - oaut2-proxy-domain
- Port - 4180
- Certificate -
- Set the annotation for cert-manager.io/cluster-manager to “letsencrypt”
I removed the certificate for the original whoami workload and the cert-manger annotation since the certificate can be renewed whenever the oauth calls are made.
With all of this done, going to https://whoami.domain.tld presents me with a Keyguard login page including my two-factor TOTP code and then shows me the whoami output. The output now incldues a cookie containing the oauth2 proxy cookie:
Hostname: whoami-75686b6c58-b8w6v
IP: 127.0.0.1
IP: 10.42.4.31
RemoteAddr: 10.42.1.0:40692
GET / HTTP/1.1
Host: whoami.domain.tld
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:74.0) Gecko/20100101 Firefox/74.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.5
Cache-Control: max-age=0
Cookie: _oauth2_proxy=<cookie redacted>
Upgrade-Insecure-Requests: 1
X-Forwarded-For: 192.168.xx.xx
X-Forwarded-Host: whoami.domain.tld
X-Forwarded-Port: 443
X-Forwarded-Proto: https
X-Original-Uri: /
X-Real-Ip: 192.168.xx.xx
X-Request-Id: b1834c720c3ee99982e6e4a353b89
X-Scheme: https