I have several services deployed and exposed on my Kubernetes cluster. Some of them need to be publicly accessible, while others should remain private. On top of that, I don’t fully trust the built-in security of some apps, so I decided to add an extra layer of protection with OAuth2-Proxy.


Setup

I already had a Zitadel instance running, along with an Ingress controller and Cert-Manager.

The first service I wanted to secure was the Kubernetes Dashboard. I deployed OAuth2-Proxy and configured it. At first, I mistakenly set up an internal upstream. This worked fine for the dashboard itself, but not for other apps - since OAuth2-Proxy only supports a single upstream.

That’s when I realized I had configured OAuth2-Proxy as a reverse proxy, instead of just as an authentication layer. I removed the upstream setting and instead defined allowed domains.

One important detail: when setting domain values, you must include the leading dot (e.g., .domain.tld). Without it, subdomains won’t be allowed through OAuth2-Proxy.

Here’s my final configuration:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
provider = "oidc"
oidc_issuer_url = "https://zitadel.domain-a.tld"
scope = "openid email profile"
code_challenge_method = "S256" # Required for OIDC providers that use PKCE
pass_access_token = true
skip_provider_button = false
ssl_insecure_skip_verify = false
proxy_prefix = "/oauth2"

email_domains = [".domain-a.tld", ".domain-b.tld", "domain-a.tld", "domain-b.tld"]
whitelist_domains = [".domain-a.tld", ".domain-b.tld", "domain-a.tld", "domain-b.tld"]
cookie_domains = [".domain-a.tld", ".domain-b.tld", "domain-a.tld", "domain-b.tld"]
cookie_name = "_oauth2_proxy"
cookie_expire = "168h"
cookie_secure = true
cookie_httponly = true
cookie_samesite = "lax"
cookie_csrf_per_request = true
cookie_csrf_expire = "5m"
set_xauthrequest = true
pass_user_headers = true
pass_authorization_header = true
pass_basic_auth = false
reverse_proxy = false
show_debug_on_error = false

You also need to provide the following environment variables. While you could set them directly in the config file, I prefer to keep them in 1Password and sync them into my namespace as Kubernetes secrets — for better security and easier management:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
env:
  - name: OAUTH2_PROXY_CLIENT_ID
    valueFrom:
      secretKeyRef:
        name: oauth2-proxy-credentials
        key: client-id
  - name: OAUTH2_PROXY_CLIENT_SECRET
    valueFrom:
      secretKeyRef:
        name: oauth2-proxy-credentials
        key: client-secret
  - name: OAUTH2_PROXY_COOKIE_SECRET
    valueFrom:
      secretKeyRef:
        name: oauth2-proxy-credentials
        key: cookie-secret

Note: If you’re using Let’s Encrypt staging certificates, you’ll need to set ssl_insecure_skip_verify = true. Once you switch to production certificates, you can (and should) set it back to false.


Adding OAuth2-Proxy to Ingress

Once OAuth2-Proxy was properly configured and I could log in with my Zitadel account, I integrated it with my Ingress. After a bit of trial and error, I ended up with this setup, which now works reliably across all my services:

1
2
3
4
5
6
7
kubernetes.io/ingress.class: "nginx"
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/auth-url: "https://oauth-proxy.domain.tld/oauth2/auth"
nginx.ingress.kubernetes.io/auth-signin: >-
  https://oauth-proxy.domain.tld/oauth2/start?rd=$scheme://$host$request_uri

Depending on the service, I sometimes add these extra annotations:

1
2
nginx.ingress.kubernetes.io/backend-protocol: "HTTPS" # or "HTTP" if the service doesn’t use TLS
nginx.ingress.kubernetes.io/proxy-buffer-size: "8k"

This setup is based on the official Ingress NGINX OAuth2-Proxy guide, with a few tweaks to fit my use case.