Securing Kubernetes with OAuth

Featured image

I needed a way to secure my services now that I had migrated them to Kubernetes. Previously I was using Cloudflare Access which I would highly recommend if you need to secure resources on the internet. Cloudflare made the service free while many people were working from home, which was a great idea. I didn’t feel the need to keep using it once the free period had ended given my use case was fairly small and only for my personal services. I also wanted to see if I could set up an equivalent service myself.

Enter OAuth2 Proxy. OAuth2 Proxy is

A reverse proxy and static file server that provides authentication using Providers (Google, GitHub, and others) to validate accounts by email, domain or group.

This proxy works with the Kubernetes NGINX Ingress Controller meaning I could secure my services at the Kubernetes layer rather than at the CLoudflare layer. Thankfully others have done the same thing and I followed the instructions on the External OAUTH Authentication page to get it set up with the details stepped out below.

Create an OAuth App

I used gitHub as my OAuth provider largely because that’s what the example used and it’s what I was using with Cloudflare Access. To create a new OAuth app on GitHub navigate to Settings -> Developer Settings -> OAuth Apps -> New OAuth App. It’s largely self explanatory with the Authorization callback URL pointing to the OAuth2 Proxy. I stored the client_id and client_secret in a Secret called github-oauth-dev-blog-secret which is used in the configuration below.

Create the OAuth2 Proxy instance

To set up the proxy we need a Deployment and an Ingress. I set up the Ingress to be on the same hostname as the dev instance of my blog mounted at /oauth2. I limited the proxy to only allow my GitHub username access.

# OAuth Proxy
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    k8s-app: devblog-oauth2-proxy
  name: devblog-oauth2-proxy
  namespace: aselford-dev
spec:
  replicas: 1
  selector:
    matchLabels:
      k8s-app: devblog-oauth2-proxy
  template:
    metadata:
      labels:
        k8s-app: devblog-oauth2-proxy
    spec:
      containers:
      - name: devblog-oauth2-proxy
        image: quay.io/oauth2-proxy/oauth2-proxy:v6.0.0
        imagePullPolicy: Always
        args:
        - --provider=github
        - --upstream=file:///dev/null
        - --http-address=0.0.0.0:4180
        - --github-user="ezzizzle"  # Restrict just to my user
        env:  # Secret created externally and saved in github-oauth-dev-blog-secret
        - name: OAUTH2_PROXY_CLIENT_ID
          valueFrom:
            secretKeyRef:
              name: github-oauth-dev-blog-secret
              key: OAUTH2_PROXY_CLIENT_ID
        - name: OAUTH2_PROXY_CLIENT_SECRET
          valueFrom:
            secretKeyRef:
              name: github-oauth-dev-blog-secret
              key: OAUTH2_PROXY_CLIENT_SECRET
        - name: OAUTH2_PROXY_COOKIE_SECRET
          valueFrom:
            secretKeyRef:
              name: github-oauth-dev-blog-secret
              key: OAUTH2_PROXY_COOKIE_SECRET
        ports:
        - containerPort: 4180
          protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
  labels:
    k8s-app: devblog-oauth2-proxy
  name: devblog-oauth2-proxy
  namespace: aselford-dev
spec:
  ports:
  - name: http
    port: 4180
    protocol: TCP
    targetPort: 4180
  selector:
    k8s-app: devblog-oauth2-proxy
---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: devblog-oauth2-proxy
  namespace: aselford-dev
spec:
  rules:
  - host: dev-blog.aselford.dev
    http:
      paths:
      - path: /oauth2
        backend:
          serviceName: devblog-oauth2-proxy
          servicePort: 4180
  tls:
  - hosts:
    - dev-blog.aselford.dev
    secretName: dev-blog-cert

The Ingress for the blog itself needed a couple of annotations added to add the OAuth2 proxy as an auth provider.

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: dev-blog-ingress
  namespace: aselford-dev
  annotations:
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    cert-manager.io/cluster-issuer: "letsencrypt-production"
    # The following annotations redirect requests to the OAuth provider when not authenticated
    nginx.ingress.kubernetes.io/auth-url: "https://$host/oauth2/auth"
    nginx.ingress.kubernetes.io/auth-signin: "https://$host/oauth2/start?rd=$escaped_request_uri"
spec:
  tls:
  - hosts:
    - dev-blog.aselford.dev
    secretName: dev-blog-cert
  rules:
  - host: dev-blog.aselford.dev
    http:
      paths:
      - path: /
        backend:
          serviceName: devblog
          servicePort: 80

And That’s It

That’s basically all I had to do.

For a while I could get the OAuth login to work however NGINX was returning a 503 Service Unavailable error once authenticated. This was really frustrating. Turns out during my playing around with getting things set up I had not deleted a stale ingress that pointed at a non-existant service. Deleting this fixed the issue and the full authentication flow worked.

There was just one wrinkle..

Safari 🤬

When I enabled OAuth protection for the dev instance of my blog for some reason CSS files weren’t loading.

Missing CSS

I like minimal, not this minimal

It was limited to some local CSS files but not all. Looking in to the server logs there was a 401 for those particular CSS resources which was then attempting to redirect the user to GitHub—this was failing with a CORS error. The error was limited to Safari on macOS and iOS1.

<!-- Working -->
<link rel="stylesheet" href="/css/images.css">
<link rel="stylesheet" href="/css/article.css">
<!-- Not Working -->
<link rel="stylesheet" href="/css/coder.min.32..Qs=" crossorigin="anonymous" media="screen">
<link rel="stylesheet" href="/css/coder-dark.min.e7...PY=" crossorigin="anonymous" media="screen">

The CSS files that weren’t working had a crossorigin="anonymous" tag. According to MDN:

The “anonymous” keyword means that there will be no exchange of user credentials via cookies, client-side SSL certificates or HTTP authentication as described in the Terminology section of the CORS specification, unless it is in the same origin.

Unfortunately for me, there’s a bug in Safari where the crossorigin="anonymous" setting refuses to send credentials even if it’s from the same origin… 😞 This bug was reported in 2017 and only fixed in May this year; it’s fixed in the Safari Technology Preview but not yet in the main version of Safari.

Sadly I will have to wait until that fix makes it in to production before I can access the dev version of my blog from my main browser.


  1. Chrome worked on macOS but not on iOS presumably because it’s using the WebKit engine that all iOS browsers have to use. ↩︎