Securing Kubernetes Ingress with oauth2-proxy and Azure Entra ID

Protecting specific routes in your Kubernetes applications is essential—especially when handling sensitive internal tools or admin panels. One effective way to do this is by using oauth2-proxy at the ingress level, allowing you to offload authentication and authorization directly at the edge. This not only simplifies your app logic but also aligns well with enterprise-grade identity providers like Azure Entra ID (formerly Azure AD).

In this article, we’ll walk through how to protect selected URLs in your Kubernetes clusters using oauth2-proxy, with configurations for both Traefik (k3s-native) and nginx ingress, which differ in key areas. We’ll use Azure Entra ID for identity and AD security groups for access control—enabling seamless integration with existing Microsoft-based environments.

oath2 flow. refremce https://oauth2-proxy.github.io/oauth2-proxy/assets/images/simplified-architecture-2a6ee6443dc78a5a28dfdb49f07f981e.svg

We’ll start by enabling HTTPS with Let’s Encrypt, touch briefly on the OAuth2 flow, then move on to practical step-by-step implementations for both Traefik and nginx.

Setting Up SSL with Let’s Encrypt and cert-manager

To enable HTTPS, we first install cert-manager and configure a Let’s Encrypt ClusterIssuer. This automates certificate provisioning and renewal for our ingress resources.

create letsencrypt-issuer.yaml :

# letsencrypt-issuer.yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
  namespace: kube-system
spec:
  acme:
    email: [email protected]  # adjust it to your real email adress
    server: https://acme-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: letsencrypt-prod-account-key
    solvers:
    - http01:
        ingress:
          class: traefik # or nginx
YAML

Install cert-manager and apply the issuer:

kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.13.3/cert-manager.yaml

kubectl apply -f letsencrypt-issuer.yaml
Bash

We use a simple Traefik deployment and service to test HTTPS.

Deploy an app to test

Let’s just deploy a simple app for the reference to for demo, all you need is a service (in our example named “nginx-demo-service”) so you feel free to use your own deployment but here for refrence for those who want to test.
Also, remember that you need a valid domain to point to your Kubernetes ingress IP. I am not covering that here.

This creates an nginx web page app (not to be mistaken with nginx ingress) with 2 replicas and a kuberneties service to point to them. nginx-demo.yaml :

# nginx-demo.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-demo
  labels:
    app: nginx-demo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx-demo
  template:
    metadata:
      labels:
        app: nginx-demo
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - containerPort: 80
---

apiVersion: v1
kind: Service
metadata:
  name: nginx-demo-service
spec:
  selector:
    app: nginx-demo
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80

---
YAML

Set up SSL-enabled Ingress

We use a simple nginx deployment and service to test HTTPS.

Here’s the key part of the ingress definition:

#ingress.yaml
metadata:
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    traefik.ingress.kubernetes.io/router.entrypoints: "websecure" # traefik only
    traefik.ingress.kubernetes.io/router.tls: "true" # traefik only
  # nginx.ingress.kubernetes.io/ssl-redirect: "true"
  # nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
spec:
  ingressClassName: traefik # or nginx
  tls:
  - hosts:
    - www.your-domain.com
    secretName: your-domain-com-tls # or call it what you want but remember it! 
YAML

This tells cert-manager to:

  • Use the letsencrypt-prod Cluster Issuer.
  • Request a TLS certificate for test-cluster.your-domain.com.
  • Store it in the your-domain-com-tls secret.

Internally, cert-manager solves the ACME HTTP-01 challenge by creating an endpoint (/.well-known/acme-challenge/...) via your ingress. Let’s Encrypt verifies it by making an HTTP call to your domain before issuing the certificate.

kubectl apply -f nginx-demo.yaml -n nginx-demo # if you want 

kubectl apply -f ingress.yaml -n nginx-demo
Bash

You should be able to see the call https://[your-domain.com]/ and get an SSL-protected responce

Installing oauth2-proxy in Kubernetes

oauth2-proxy is a reverse proxy that handles authentication using providers like Azure Entra ID, Google, GitHub, and more. It sits in front of your application and enforces OAuth2-based login. Only authenticated users can access the protected URLs.

In this setup, oauth2-proxy intercepts requests at the ingress level and checks for valid authentication tokens. If none are present, it redirects the user to your identity provider (e.g., Entra ID) for login. Once authenticated, the request proceeds to your backend service.


Configuring oauth2-proxy Helm Chart for Azure Entra ID


The Helm values file is the heart of your oauth2-proxy deployment. It tells the proxy how to authenticate users, what OAuth2 provider to use, and how to expose the proxy via Ingress. Here’s a breakdown of the critical settings when using Azure Entra ID as your identity provider.

📚 For more providers (Google, GitHub, generic OIDC, etc.), refer to the official docs:
https://oauth2-proxy.github.io/oauth2-proxy/configuration/providers/


Set Up Azure Entra ID:

You need to create an App registration in your Azure tenant:

Follow the official oauth2-proxy guide via this YouTube tutorial.

Get the Client ID, Client Secret, and Tenant ID.

🔐 Generate a Cookie Secret

This is used to sign the session cookie:

openssl rand -base64 32
Bash

📄 Set Up Values traefik

oauth2-proxy.values.yaml (traefik only)

# oauth2-proxy.values.yaml
replicaCount: 1

config:
  clientID: "your-client-id"
  clientSecret: "your-client-secret"
  cookieSecret: "your-generated-cookie-secret"
  configFile: |
    provider="entra-id"
    oidc_issuer_url="https://login.microsoftonline.com/<TENANT_ID>/v2.0"
    scope="openid"
    allowed_groups=["<GROUP_OBJECT_ID>"]
    email_domains=["your-domain.com"]
    whitelist_domains=["other-domain.com", "another-domain.com"]
    skip_provider_button= true

ingress:
  enabled: true
  className: "traefik"
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    traefik.ingress.kubernetes.io/router.entrypoints: "websecure" 
    traefik.ingress.kubernetes.io/router.tls: "true" 
  hosts:
    - your-domain.com #(setup with www?)
    - your-second-domain.com
  tls:
    - secretName: your-domain-com-tls # from our earlier serup of ingress
      hosts:
        - your-domain.com
    - secretName: your-second-domain-com-tls
      hosts:
        - your-second-domain.com
Bash

📄 Set Up Values nginx

oauth2-proxy.values.yaml (nginx only)

# oauth2-proxy.values.yaml
replicaCount: 1

config:
  clientID: "your-client-id"
  clientSecret: "your-client-secret"
  cookieSecret: "your-generated-cookie-secret"
  configFile: |
    provider="entra-id"
    oidc_issuer_url="https://login.microsoftonline.com/<TENANT_ID>/v2.0"
    scope="openid"
    allowed_groups= ["<GROUP_OBJECT_ID>"]
    email_domains= ["your-domain.com"]
    whitelist_domains= [ "other-domain.com", "another-domain.com" ]
    skip_provider_button= true

ingress:
  enabled: true
  className: "nginx"
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
    nginx.ingress.kubernetes.io/proxy-buffer-size: "8k"
    nginx.ingress.kubernetes.io/proxy-buffers-number: "4"
  hosts:
    - your-domain.com #(setup with www?)
    - your-second-domain.com
  tls:
    - secretName: your-domain-com-tls # from our earlier serup of ingress
      hosts:
        - your-domain.com
    - secretName: your-second-domain-com-tls
      hosts:
        - your-second-domain.com
Bash

🔎 Key Notes:

  • clientID, clientSecret, and cookieSecret go under config not inside configFile.
  • Everything else goes inside configFile in the exact syntax oauth2-proxy expects.
  • oidc_issuer_url is constructed using your Azure Tenant ID.
  • allowed_groups supports Azure security group object IDs (not names).
  • email_domains and whitelist_domains are optional filters for domain-based access control.
  • The ingress section sets up SSL and routing using Traefik (can be adapted for nginx by changing className and annotations).

This structure avoids confusion around what belongs where in the values file, which is often unclear in the official docs.

Install oauth2-proxy in your cluster

helm repo add oauth2-proxy https://oauth2-proxy.github.io/manifests

kubectl create namespace oauth2-proxy

helm install oauth2-proxy oauth2-proxy/oauth2-proxy \
  --values oauth2-proxy.values.yaml \
  -n oauth2-proxy 
Bash

🔐 Traefik + oauth2-proxy: How It Works

Traefik integrates with oauth2-proxy using a special object called a Middleware, specifically the forwardAuth feature.


🧩 How It Works

  1. Request arrives at /secure-path on yourdomain.com.
  2. Traefik attaches the Middleware to that route.
  3. Middleware forwards the request to oauth2-proxy’s /oauth2/auth endpoint.
  4. oauth2-proxy checks:
    • Is the user authenticated?
    • Is the session valid?
    • Do they belong to an allowed group/domain?
  5. If valid ✅ → the request proceeds to your service. If invalid ❌ → the user gets redirected to the Azure login flow.

🔧 Middleware Configuration

apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
  name: oauth2
  namespace: nginx-demo
spec:
  forwardAuth:
    address: http://oauth2-proxy.oauth2-proxy.svc.cluster.local/oauth2/auth
    trustForwardHeader: false
    
  # ALTERNATIVE SET UP   
  # trustForwardHeader: true
  # authResponseHeaders:
  #    -  X-Auth-Request-Access-Token
  #    -  Authorization

Bash
  • http://oauth2-proxy.oauth2-proxy.svc.cluster.local is the kubenties internal service URL, the format is [service name].[namespace].[svc.cluster.local]
  • address: internal cluster address of oauth2-proxy’s /oauth2/auth endpoint.
  • trustForwardHeader: false: disables trusting forwarded headers, increasing security, switch to alternative if you need the headers down in your app.

🛡️ Ingress Example

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: media-ingress
  namespace: nginx-demo
  annotations:
    traefik.ingress.kubernetes.io/router.entrypoints: "websecure"
    traefik.ingress.kubernetes.io/router.tls: "true"
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    traefik.ingress.kubernetes.io/router.middlewares: "nginx-demo-oauth2@kubernetescrd" # <namespace>-<name>@kubernetescrd
spec:
  ingressClassName: traefik
  tls:
    - hosts:
        - www.your-domain.com #(we did not to without www in this setup)
      secretName: your-domain-com-tls  # from our earlier serup of ingress
  rules:
    - host: www.your-domain.com
      http:
        paths:
          - path: /secure-path
            pathType: Prefix
            backend:
              service:
                name: nginx-demo-service # or your-service
                port:
                  number: 80
Bash

🧠 Summary

This setup ensures that only authenticated users (via Azure Entra ID) can access https://yourdomain.com/secure-path/**. Everything else remains publicly accessible unless you attach the middleware to more paths.

🔐 Using oauth2-proxy with NGINX Ingress

Unlike Traefik, NGINX does not use Middleware objects. Instead, it handles OAuth2 authentication natively via ingress annotations that define how to forward requests to the oauth2-proxy for validation.


🧩 How It Works

  1. Requests to /secure-path are intercepted by NGINX Ingress.
  2. NGINX uses the auth-url annotation to send a subrequest to oauth2-proxy’s /oauth2/auth.
  3. If not authenticated, the user is redirected via auth-signin to the Azure login.
  4. Once authenticated, oauth2-proxy sets session cookies and sends headers back.
  5. The request is forwarded to your service.

🛡️ Secure Path Ingress

Use the following ingress to protect /secure-path/**:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: media-ingress
  namespace: nginx-demo
  annotations:
    nginx.ingress.kubernetes.io/auth-url: "http://oauth2-proxy.oauth2-proxy.svc.cluster.local/oauth2/auth"
    nginx.ingress.kubernetes.io/auth-signin: "http://oauth2-proxy.oauth2-proxy.svc.cluster.local/oauth2/start?rd=$escaped_request_uri"
    nginx.ingress.kubernetes.io/auth-response-headers: "X-Auth-Request-Access-Token,Authorization"
    nginx.ingress.kubernetes.io/proxy-buffer-size: "8k"
    nginx.ingress.kubernetes.io/proxy-buffers-number: "4"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - www.your-domain.com #(we did not to without www in this setup)
      secretName: your-domain-com-tls  # from our earlier serup of ingress
  rules:
    - host: www.your-domain.com
      http:
        paths:
          - path: /secure-path
            pathType: Prefix
            backend:
              service:
                name: nginx-demo-service # or your-service
                port:
                  number: 80
Bash

🧠 Summary

With NGINX, everything is handled via annotations — no extra Middleware objects. Just configure oauth2-proxy properly and use the right ingress annotations, and your paths will be protected via Azure Entra ID authentication.


Posted

in

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *