OIDC at the Gateway
สิ่งที่จะสร้าง
หัวข้อที่มีชื่อว่า “สิ่งที่จะสร้าง”บทก่อนหน้า ให้ shopmicro realm และ OIDC client shopmicro-gateway มา แต่ยังไม่มีอะไรบังคับ authentication บทนี้ปิดช่องว่างนั้น
เรา deploy oauth2-proxy ลงใน platform layer แล้วต่อสายเข้ากับ Keycloak client จากนั้นเพิ่ม annotation สองบรรทัดลงใน ShopMicro ingress เพื่อให้ ingress controller โยนการเช็ค auth ของทุก request ไปให้ oauth2-proxy ผลลัพธ์: request ที่ยังไม่ authenticate ไปยัง gateway ได้ 302 ไปหน้า login ของ Keycloak; request จะถึง ShopMicro ก็ต่อเมื่อ login สำเร็จเท่านั้น
service ของ ShopMicro เองไม่ควรต้อง implement OIDC ซ้ำกันทีละตัว authentication เป็น cross-cutting concern ฉะนั้นเราบังคับครั้งเดียวที่ขอบ ที่หน้า gateway — app ยังไม่รู้เลยว่า login ทำงานอย่างไร แค่รับ request ที่ authenticate มาแล้ว
pattern external-auth (ingress annotation ที่โยนไปให้ oauth2-proxy) เหมาะกับตรงนี้เพราะ ShopMicro มี ingress อยู่แล้ว เราไม่ได้ reroute traffic ทั้งหมดผ่าน proxy ตัวที่สอง; แต่ ingress controller ทำ subrequest ไปที่ oauth2-proxy สำหรับแต่ละ request แล้ว forward เฉพาะตัวที่ผ่าน oauth2-proxy พูด OIDC กับ Keycloak, จัดการ session cookie และรับมือกับ redirect dance — ซึ่งไม่มีอันไหนควรอยู่ใน application code เลย
ข้อดีข้อเสีย
หัวข้อที่มีชื่อว่า “ข้อดีข้อเสีย”External auth (ingress annotations) vs. oauth2-proxy as a reverse proxy in the request path
- Pros: ShopMicro เก็บ ingress และ routing เดิมไว้; oauth2-proxy เห็นแค่ auth subrequest และ traffic ของ callback
/oauth2/*ไม่ใช่ทุก byte ของ app; auth เป็น annotation สองบรรทัดที่คุณเพิ่มหรือถอดได้ต่อ route - Cons: ขึ้นอยู่กับว่า ingress controller รองรับ external auth หรือไม่ (nginx รองรับผ่าน
auth-url); subrequest เพิ่ม hop หนึ่งบน hot path การรัน oauth2-proxy แบบ inline เป็นประตูหน้าของ upstream นั้นเข้าใจง่ายกว่า แต่ก็ต้อนทุก traffic ผ่านตัวเอง
oauth2-proxy at the edge vs. per-service token validation in ShopMicro
- Pros: ที่เดียวสำหรับ config และ audit; service ไม่ต้องแตะ token หรือ redirect เลย; คุณ protect ทั้ง app ได้ก่อนแตะโค้ด Go สักบรรทัด
- Cons: ขอบพิสูจน์ได้แค่ว่า user authenticate แล้ว ไม่ได้บอกว่าเขาทำอะไรได้บ้าง — authorization แบบละเอียดยังต้องอยู่ใน service edge auth เป็นประตู ไม่ใช่ policy engine
ติดตั้ง
หัวข้อที่มีชื่อว่า “ติดตั้ง”1. modules/platform/oauth2-proxy.tf
หัวข้อที่มีชื่อว่า “1. modules/platform/oauth2-proxy.tf”Deploy oauth2-proxy via its Helm chart, configured with the keycloak-oidc provider pointed at the realm’s issuer URL. It reads the client id/secret produced by the Keycloak lesson.
resource "kubernetes_secret" "oauth2_proxy" { metadata { name = "oauth2-proxy" namespace = var.platform_namespace } data = { "client-id" = keycloak_openid_client.gateway.client_id "client-secret" = keycloak_openid_client.gateway.client_secret # 32 random bytes, base64url-encoded "cookie-secret" = var.oauth2_proxy_cookie_secret }}
resource "helm_release" "oauth2_proxy" { name = "oauth2-proxy" namespace = var.platform_namespace repository = "https://oauth2-proxy.github.io/manifests" chart = "oauth2-proxy" version = var.oauth2_proxy_chart_version
values = [yamlencode({ config = { existingSecret = kubernetes_secret.oauth2_proxy.metadata[0].name configFile = <<-CFG provider = "keycloak-oidc" oidc_issuer_url = "https://${var.keycloak_hostname}/realms/shopmicro" redirect_url = "https://${var.shopmicro_hostname}/oauth2/callback" email_domains = ["*"] cookie_secure = true reverse_proxy = true CFG } ingress = { enabled = true className = var.ingress_class path = "/oauth2" hosts = [var.shopmicro_hostname] } })]
depends_on = [keycloak_openid_client.gateway]}reverse_proxy = true บอก oauth2-proxy ให้เชื่อ header X-Forwarded-* ที่ ingress set มา route /oauth2/* (รวมถึง /oauth2/callback ที่เราลงทะเบียนเป็น redirect URI ของ client) ถูก expose บน host เดียวกับ ShopMicro ฉะนั้น browser อยู่บน domain เดียวตลอดทั้ง flow
2. modules/platform/shopmicro-auth.tf
หัวข้อที่มีชื่อว่า “2. modules/platform/shopmicro-auth.tf”The annotations that turn on enforcement. These attach to the ShopMicro ingress and tell nginx to check every request against oauth2-proxy first.
resource "kubernetes_annotations" "shopmicro_auth" { api_version = "networking.k8s.io/v1" kind = "Ingress" metadata { name = "shopmicro" namespace = var.shopmicro_namespace } annotations = { "nginx.ingress.kubernetes.io/auth-url" = "https://${var.shopmicro_hostname}/oauth2/auth" "nginx.ingress.kubernetes.io/auth-signin" = "https://${var.shopmicro_hostname}/oauth2/start?rd=$escaped_request_uri" }
depends_on = [helm_release.oauth2_proxy]}auth-url คือ subrequest ที่ nginx ทำต่อ request — 202 แปลว่า “ปล่อยผ่าน” 401 แปลว่า “ยังไม่ authenticate” auth-signin คือจุดที่ nginx ส่ง browser ไปตอนได้ 401: /oauth2/start ของ oauth2-proxy ซึ่งจะ redirect ต่อไปที่ Keycloak และพก rd ไปด้วย เพื่อให้ user กลับมาลงตรงที่เริ่มต้นหลัง login
3. live/aws/platform/terragrunt.hcl
หัวข้อที่มีชื่อว่า “3. live/aws/platform/terragrunt.hcl”Add the new inputs alongside the Keycloak ones from the last lesson:
inputs = { # ...existing keycloak inputs... shopmicro_namespace = "shopmicro" oauth2_proxy_cookie_secret = get_env("OAUTH2_PROXY_COOKIE_SECRET") oauth2_proxy_chart_version = "7.12.0"}ตรวจสอบผล
หัวข้อที่มีชื่อว่า “ตรวจสอบผล”Apply, confirm oauth2-proxy is running, then prove the redirect works:
cd live/aws/platformterragrunt apply
kubectl -n platform get pods -l app.kubernetes.io/name=oauth2-proxy# NAME READY STATUS RESTARTS AGE# oauth2-proxy-6c8d...-abcde 1/1 Running 0 90sNow hit the gateway with no session cookie. An unauthenticated request must be redirected to login, not served the app:
curl -sI https://shop.aws.clouddeploy.example.com/# HTTP/2 302# location: https://shop.aws.clouddeploy.example.com/oauth2/start?rd=%2FFollow the sign-in path and confirm it hands off to Keycloak:
curl -sI "https://shop.aws.clouddeploy.example.com/oauth2/start?rd=%2F"# HTTP/2 302# location: https://id.aws.clouddeploy.example.com/realms/shopmicro/protocol/openid-connect/auth?client_id=shopmicro-gateway&...location สุดท้ายที่ชี้ไปที่ endpoint openid-connect/auth ของ realm คือหลักฐาน: ตอนนี้ gateway ส่ง traffic แบบ anonymous ไปที่ Keycloak แทนที่จะเข้า ShopMicro เปิด URL นั้นใน browser, login แล้วคุณจะกลับมาลงที่ ShopMicro แบบ authenticate แล้ว
ตรวจสอบความเข้าใจ:
- ใน pattern external-auth nginx ส่งอะไรไปให้ oauth2-proxy จริง ๆ ในแต่ละ request และ response
202กับ401หมายความว่าอะไร? - ทำไม route
/oauth2/*ของ oauth2-proxy ถึงต้อง serve บน hostname เดียวกัน กับ ShopMicro? - edge auth พิสูจน์ว่า user login แล้ว แต่จงใจ ไม่ ทำอะไร และความรับผิดชอบนั้นตกไปอยู่ที่ไหน?
- query parameter
rdในauth-signinมีไว้ทำอะไร และถ้าไม่มี user จะเจอประสบการณ์แบบไหน?
คุณ deploy oauth2-proxy ลงใน platform layer, ต่อสายเข้ากับ Keycloak OIDC client แล้วเปิด enforcement ด้วย ingress annotation สองบรรทัด — จากนั้น verify ว่า request ที่ยังไม่ authenticate ตอนนี้ได้ 302 ไปหน้า login ของ Keycloak ShopMicro ถูก authenticate ที่ขอบ เหมือนกันบนทุก cloud โดยตัว app เองไม่เปลี่ยนเลย
เท่านี้ก็จบเรื่อง identity ต่อไปคืออีกครึ่งของการควบคุม runtime — การตัดสินใจว่า อะไร ที่ app ทำโดยไม่ต้อง redeploy: Feature Flags (GrowthBook) →