- Đăng vào
Thiết lập SSO cho Ingress Traefik sử dụng Oauth2 Proxy
Bài viết này hướng dẫn cách bảo vệ một static webapp bằng SSO (Single Sign-On) sử dụng OAuth2 Proxy và Microsoft Entra ID (trước đây là Azure AD) làm identity provider, với Traefik làm Ingress Controller trên Kubernetes.
Giới thiệu
Traefik là gì?
Traefik là một reverse proxy và Ingress Controller hiện đại, mã nguồn mở được thiết kế đặc biệt cho các hệ thống container và microservices. Nó hoạt động như một "cánh cổng" (gateway) để quản lý lưu lượng truy cập vào các ứng dụng chạy trên Kubernetes.
- Ưu điểm của Traefik trong môi trường Kubernetes:
- Tự động phát hiện service
- Hỗ trợ nhiều protocol (HTTP, TCP, UDP)
- Middleware mạnh mẽ
Kiến trúc Traefik trên Kubernetes có thể tóm lược lại như dưới đây
┌─────────────────────────────────────────────────┐
│ Kubernetes Cluster │
│ │
│ ┌───────────┐ ┌───────────┐ ┌──────────┐ │
│ │ Ingress │ │ Service │ │ Pods │ │
│ │ Traefik │◄───┤ (LB) │◄───┤ (App) │ │
│ └───────────┘ └───────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────┘
OAuth2 Proxy là gì?
- Là reverse proxy cung cấp authentication sử dụng OAuth2
- Hỗ trợ nhiều provider: Google, GitHub, Azure AD,...
- Chức năng chính:
- Xác thực người dùng
- Lưu session
- Chuyển header thông tin user
Mình đã có nhiều bài viết về việc tích hợp SSO sử dụng OAuth2 Proxy cũng như Microsoft EntraID:
- Thiết lập SSO cho Kuberentes dashboard dùng Oauth2 Proxy
- Cài đặt và thiết lập SSO (Microsoft Entra ID) cho Minio trên Kubernetes
- Tích hợp SSO cho Backstage dùng Azure AD (Entra ID)
- Thiết lập SSO cho ArgoCD sử dụng Azure AD
Tích hợp SSO cho một static webapp
Trong quá trình làm việc, mình gặp phải bài toán là phải phân cấp, phân quyền cho tài liệu nội bộ của dự án, tài liệu kiểu Docs as Code sử dụng sphinx, mkdocs để build ra các static webapp html, css,... và tích hợp với account Microsoft của dự án.

Hiện tại document đã được host trên Kuberentes cluster, cluster này sử dụng Traefik.
Để dễ minh hoạ mình lấy một static web đơn giản là echo server.
Static webapp echo server
Dưới đây là manifest để khởi tạo: namespace, deploy, service:
apiVersion: v1
kind: Namespace
metadata:
name: echoserver
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: echoserver
namespace: echoserver
spec:
replicas: 1
selector:
matchLabels:
app: echoserver
template:
metadata:
labels:
app: echoserver
spec:
containers:
- image: docker.io/ealen/echo-server:latest
imagePullPolicy: IfNotPresent
name: echoserver
ports:
- containerPort: 80
env:
- name: PORT
value: "80"
---
apiVersion: v1
kind: Service
metadata:
name: echoserver
namespace: echoserver
spec:
ports:
- port: 80
targetPort: 80
protocol: TCP
type: ClusterIP
selector:
app: echoserver
Tiếp theo mình sẽ thiết lập TLS, để cho đơn giản mình lấy ví dụ sử dụng Letsencrypt. Hiển nhiên trong môi trường production mình sẽ dử dụng SSL của công ty, cái này chỉ để ví dụ
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: acme
spec:
acme:
# Use staging by default.
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: acme
solvers:
- http01:
ingress:
class: traefik
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: test-domain
namespace: echoserver
spec:
secretName: test-nvtienanh # <=== Name of secret where the generated certificate will be stored.
dnsNames:
- "test.nvtienanh.info"
issuerRef:
name: acme
kind: ClusterIssuer
Cuối cùng đây là ingress traefik (khi chưa tích hợp SSO)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: echoserver
namespace: echoserver
annotations:
kubernetes.io/ingress.class: traefik
traefik.ingress.kubernetes.io/router.entrypoints: websecure
spec:
ingressClassName: traefik
rules:
- host: test.nvtienanh.info
http:
paths:
- path: /
pathType: ImplementationSpecific
backend:
service:
name: echoserver
port:
number: 80
tls:
- secretName: test-nvtienanh # <=== Use the name defined in Certificate resource
hosts:
- test.nvtienanh.info
Khi bạn access vào nó sẽ trả về kiểu
{
"host": {
"hostname": "test.nvtienanh.info",
"ip": "::ffff:10.1.66.82",
"ips": []
},
"http": {
"method": "GET",
"baseUrl": "",
"originalUrl": "/",
"protocol": "http"
},
"request": {
"params": {
"0": "/"
},
"query": {
},
"cookies": {
},
"body": {
},
"headers": {
"host": "test.nvtienanh.info",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edg/137.0.0.0",
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"accept-encoding": "gzip, deflate, br, zstd",
"accept-language": "en-US,en;q=0.9,vi;q=0.8,nl;q=0.7",
"cache-control": "max-age=0",
"dnt": "1",
"if-none-match": "W/\"3065-JVAYDNERjqvhkiqwzxjw20qHMcU\"",
"priority": "u=0, i",
"sec-ch-ua": "\"Microsoft Edge\";v=\"137\", \"Chromium\";v=\"137\", \"Not/A)Brand\";v=\"24\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\"",
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "none",
"sec-fetch-user": "?1",
"upgrade-insecure-requests": "1",
"x-forwarded-for": "10.233.64.31",
"x-forwarded-host": "test.nvtienanh.info",
"x-forwarded-port": "443",
"x-forwarded-proto": "https",
"x-forwarded-server": "traefik-5b758dfc99-8x7mw",
"x-real-ip": "10.233.64.31"
}
},
"environment": {
"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"TERM": "xterm",
"HOSTNAME": "echoserver-fbd589788-lchwz",
"PORT": "80",
"KUBERNETES_PORT_443_TCP_PORT": "443",
"ECHOSERVER_PORT": "tcp://10.233.52.18:80",
"ECHOSERVER_PORT_80_TCP_PORT": "80",
"ECHOSERVER_PORT_80_TCP_ADDR": "10.233.52.18",
"KUBERNETES_SERVICE_HOST": "10.233.0.1",
"KUBERNETES_SERVICE_PORT_HTTPS": "443",
"KUBERNETES_PORT": "tcp://10.233.0.1:443",
"KUBERNETES_PORT_443_TCP_PROTO": "tcp",
"KUBERNETES_SERVICE_PORT": "443",
"ECHOSERVER_SERVICE_HOST": "10.233.52.18",
"ECHOSERVER_SERVICE_PORT": "80",
"KUBERNETES_PORT_443_TCP": "tcp://10.233.0.1:443",
"KUBERNETES_PORT_443_TCP_ADDR": "10.233.0.1",
"ECHOSERVER_PORT_80_TCP": "tcp://10.233.52.18:80",
"ECHOSERVER_PORT_80_TCP_PROTO": "tcp",
"NODE_VERSION": "20.11.0",
"YARN_VERSION": "1.22.19",
"HOME": "/root"
}
}
Thiết lập Traefik
Mình giả sử rằng cluster của bạn đã được thiết lập sẵn Traefik và có môt ingressClass là traefik
. Lưu ý rằng bạn nên thiết lập allowCrossNamespace cho Traefik:
helm upgrade --install traefik traefik/traefik --namespace=traefik --create-namespace \
.... \
--set providers.kubernetesCRD.allowCrossNamespace=true \
...
Thiết lập Oauth2 Proxy
Có nhiều cách cài đặt Oauth2 Proxy trên Kubernetes, thông thường là dùng Helm chart dưới đây để đơn giản mình dùng trực tiếp manifest, nếu bạn cài bằng Helm thì nhớ cập nhật lại values.yaml cho phù hợp.
Đồng thời nhớ cập nhật <CLIENT_SECRET>
, <CLIENT_ID>
và <TENANT_ID>
cho đúng với EntraID của bạn
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/name: oauth2-proxy
name: oauth2-proxy
namespace: echoserver
spec:
selector:
matchLabels:
app.kubernetes.io/name: oauth2-proxy
template:
metadata:
labels:
app.kubernetes.io/name: oauth2-proxy
spec:
containers:
- args:
- --provider=entra-id
- --cookie-name=_proxycookie
- --email-domain=*
- --upstream=static://200
- --http-address=0.0.0.0:4180
- --oidc-issuer-url=https://login.microsoftonline.com/<TENANT_ID>/v2.0
- --set-xauthrequest=true
- --cookie-csrf-expire=9m
- --cookie-csrf-per-request=false
- --cookie-expire=1h
- --cookie-refresh=59m
- --set-authorization-header=true
- --whitelist-domain=.nvtienanh.info:*
- --scope=openid email profile
- --reverse-proxy=true
- --skip-provider-button=true
- --pass-user-headers=true
env:
- name: OAUTH2_PROXY_CLIENT_ID
value: <CLIENT_ID>
- name: OAUTH2_PROXY_CLIENT_SECRET
value: <CLIENT_SECRET>
- name: OAUTH2_PROXY_COOKIE_SECRET
value: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" # Replace with your own secret
image: quay.io/oauth2-proxy/oauth2-proxy:v7.9.0
imagePullPolicy: IfNotPresent
name: oauth2-proxy
ports:
- containerPort: 4180
protocol: TCP
resources:
limits:
cpu: 200m
memory: 128Mi
requests:
cpu: 100m
memory: 128Mi
---
apiVersion: v1
kind: Service
metadata:
labels:
app.kubernetes.io/name: oauth2-proxy
name: oauth2-proxy
namespace: echoserver
spec:
ports:
- name: http
port: 4180
protocol: TCP
targetPort: 4180
selector:
app.kubernetes.io/name: oauth2-proxy
Thiết lập Traefik Middleware
Dưới đây là cấu hình middleware của mình:
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oauth-auth-redirect
namespace: echoserver
spec:
forwardAuth:
address: http://oauth2-proxy.echoserver.svc.cluster.local:4180/
trustForwardHeader: true
authResponseHeaders:
- X-Auth-Request-User
- Set-Cookie
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oauth-errors
namespace: echoserver
spec:
errors:
status:
- "401-403"
service:
name: oauth2-proxy
port: 4180
query: "/oauth2/sign_in"
Bước cuối cùng, chúng ta sẽ cập nhật lại cấu hình Ingress
Thiết lập Ingress Traefik
Bây giờ chúng ta sẽ thêm traefik.ingress.kubernetes.io/router.middlewares
vào annotations của ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: echoserver
namespace: echoserver
annotations:
kubernetes.io/ingress.class: traefik
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.middlewares: "echoserver-oauth-errors@kubernetescrd,echoserver-oauth-auth-redirect@kubernetescrd"
spec:
ingressClassName: traefik
rules:
- host: test.nvtienanh.info
http:
paths:
- path: /
pathType: ImplementationSpecific
backend:
service:
name: echoserver
port:
number: 80
tls:
- secretName: test-nvtienanh # <=== Use the name defined in Certificate resource
hosts:
- test.nvtienanh.info
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
kubernetes.io/ingress.class: traefik
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.middlewares: "echoserver-oauth-errors@kubernetescrd"
name: oauth2-proxy
namespace: echoserver
spec:
ingressClassName: traefik
tls:
- secretName: test-nvtienanh # <=== Use the name defined in Certificate resource
hosts:
- test.nvtienanh.info
rules:
- host: test.nvtienanh.info
http:
paths:
- path: /oauth2
pathType: ImplementationSpecific
backend:
service:
name: oauth2-proxy
port:
number: 4180
Bây giờ khi bạn truy cập vào, nó sẽ đá qua trang login vào trang login của Microsoft, sau đó sẽ redirect lại trang ứng dụng của bạn, với ứng dụng echoserver kết quả trả về sẽ có dạng:
{
"host": {
"hostname": "test.nvtienanh.info",
"ip": "::ffff:10.233.66.81",
"ips": []
},
"http": {
"method": "GET",
"baseUrl": "",
"originalUrl": "/",
"protocol": "http"
},
"request": {
"params": {
"0": "/"
},
"query": {
},
"cookies": {
"_proxycookie_0": "XBi0nCRa0iulheY2YpUFB-",
"_proxycookie_1": "keKeqRgOr1uZ1jwi3RO3cfZVaFLG-"
},
"body": {
},
"headers": {
"host": "test.nvtienanh.info",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36 Edg/137.0.0.0",
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"accept-encoding": "gzip, deflate, br, zstd",
"accept-language": "en-US,en;q=0.9,vi;q=0.8,nl;q=0.7",
"cookie": "_proxycookie_0=XBi0nCRa0iulheY2YpUFB-pfSu4DUN_20GXtgE_T9bhSa7H8T2oGOZa-",
"dnt": "1",
"priority": "u=0, i",
"sec-ch-ua": "\"Microsoft Edge\";v=\"137\", \"Chromium\";v=\"137\", \"Not/A)Brand\";v=\"24\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\"",
"sec-fetch-dest": "document",
"sec-fetch-mode": "navigate",
"sec-fetch-site": "none",
"sec-fetch-user": "?1",
"upgrade-insecure-requests": "1",
"x-auth-request-user": "afq_UjrKE5DWf-cnqKem0q_6MZ9wUBdRWSjRtRLTVnE",
"x-forwarded-for": "10.233.64.31",
"x-forwarded-host": "test.nvtienanh.info",
"x-forwarded-port": "443",
"x-forwarded-proto": "https",
"x-forwarded-server": "traefik-5b758dfc99-8x7mw",
"x-real-ip": "10.233.64.31"
}
},
"environment": {
"PATH": "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"TERM": "xterm",
"HOSTNAME": "echoserver-fbd589788-lchwz",
"PORT": "80",
"KUBERNETES_PORT_443_TCP_PORT": "443",
"ECHOSERVER_PORT": "tcp://10.233.52.18:80",
"ECHOSERVER_PORT_80_TCP_PORT": "80",
"ECHOSERVER_PORT_80_TCP_ADDR": "10.233.52.18",
"KUBERNETES_SERVICE_HOST": "10.233.0.1",
"KUBERNETES_SERVICE_PORT_HTTPS": "443",
"KUBERNETES_PORT": "tcp://10.233.0.1:443",
"KUBERNETES_PORT_443_TCP_PROTO": "tcp",
"KUBERNETES_SERVICE_PORT": "443",
"ECHOSERVER_SERVICE_HOST": "10.233.52.18",
"ECHOSERVER_SERVICE_PORT": "80",
"KUBERNETES_PORT_443_TCP": "tcp://10.233.0.1:443",
"KUBERNETES_PORT_443_TCP_ADDR": "10.233.0.1",
"ECHOSERVER_PORT_80_TCP": "tcp://10.233.52.18:80",
"ECHOSERVER_PORT_80_TCP_PROTO": "tcp",
"NODE_VERSION": "20.11.0",
"YARN_VERSION": "1.22.19",
"HOME": "/root"
}
}