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:

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 - <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 - <whoami 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 - <whoami namespace>
  • Hostname - whoami.domain.tld
  • Path - /oauth2
  • Target - oaut2-proxy-domain
  • Port - 4180
  • Certificate - <Same as whoami ingress>
  • 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