ข้ามไปยังเนื้อหา

OIDC at the Gateway

บทก่อนหน้า ให้ shopmicro realm และ OIDC client shopmicro-gateway มา แต่ยังไม่มีอะไรบังคับ authentication บทนี้ปิดช่องว่างนั้น

เรา deploy oauth2-proxy ลงใน platform layer ต่อสายเข้ากับ Keycloak client แล้ววาง oauth2-proxy ไว้ตรง ๆ บนเส้นทาง request ของ ShopMicro: ingress ของ oauth2-proxy เองรับหน้าที่แทน ShopMicro บน hostname นั้น แล้ว proxy traffic ที่ authenticate แล้วต่อไปยัง gateway Service ที่อยู่ข้างหลัง ผลลัพธ์: request ที่ยังไม่ authenticate ไปยัง gateway ได้ 302 ไปหน้า login ของ Keycloak; request จะถึง ShopMicro ก็ต่อเมื่อ login สำเร็จเท่านั้น

service ของ ShopMicro เองไม่ควรต้อง implement OIDC ซ้ำกันทีละตัว authentication เป็น cross-cutting concern ฉะนั้นเราบังคับครั้งเดียวที่ขอบ ที่หน้า gateway — app ยังไม่รู้เลยว่า login ทำงานอย่างไร แค่รับ request ที่ authenticate มาแล้ว

มีสองวิธีที่จะต่อ oauth2-proxy เข้ากับ ingress: annotation แบบ external-auth ที่ให้ ingress controller ทำ subrequest ไปที่ oauth2-proxy สำหรับทุก request แล้ว forward เฉพาะตัวที่ผ่าน หรือวาง oauth2-proxy ไว้ตรง ๆ บนเส้นทาง request เป็น backend ของ ingress เอง แล้วให้ oauth2-proxy proxy ต่อไปยัง service จริงเอง external-auth เบากว่า แต่มันเป็น feature เฉพาะของ nginx-ingress-controller (nginx.ingress.kubernetes.io/auth-url) ที่ไม่มีบน AWS Load Balancer Controller, GKE ingress หรือ AGIC — สาม controller ที่คอร์สนี้ติดตั้งจริง (ดู Ingress and per-cloud config) CloudDeploy ไม่เพิ่ม ingress-nginx เป็น controller ตัวที่สี่แค่เพื่อ annotation อันเดียวนั้น: มันกำลังจะ retire ที่ upstream ในเดือนมีนาคม 2026 และการรันมันควบคู่กับ controller แบบ native ของสาม cloud จะทำลายจุดยืนที่ตรงไปตรงมาที่บท ingress เพิ่งวางไว้ ฉะนั้น CloudDeploy วาง oauth2-proxy ไว้บนเส้นทาง request แทน — pattern เดียวที่ทำงานเหมือนกันหลัง ALB, GKE ingress และ AGIC oauth2-proxy พูด OIDC กับ Keycloak, จัดการ session cookie, รับมือกับ redirect dance และ forward ทุกอย่างที่ authenticate แล้วต่อไปยัง ShopMicro gateway โดยตรง — ซึ่งไม่มีอันไหนควรอยู่ใน application code เลย

oauth2-proxy as a reverse proxy in the request path vs. an ingress controller’s external-auth annotation

  • Pros: ทำงานเหมือนกันหลัง ingress controller ไหนก็ได้ — ALB, GKE ingress, AGIC หรืออื่น ๆ — เพราะ “ingress backend เป็น Service ที่ proxy ต่อไป” ไม่ต้องพึ่ง feature เฉพาะของ controller ตัวไหนเลย pattern เดียว ไม่มีความต่างต่อ cloud และไม่ต้องเพิ่ม ingress controller ตัวที่สี่
  • Cons: ทุก request ไม่ใช่แค่ตอนเช็ค auth ต้องผ่าน oauth2-proxy เพิ่มอีกหนึ่ง hop ก่อนถึง gateway annotation แบบ external-auth เบากว่าบน hot path และปล่อยให้ ingress ของ ShopMicro เองคุม routing อยู่ — แต่มันมีเฉพาะบน ingress controller ตระกูล nginx ซึ่งคอร์สนี้ไม่ได้รัน (ดู บท ingress)

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

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"
upstreams = ["http://shopmicro-gateway.shopmicro.svc.cluster.local:80"]
email_domains = ["*"]
cookie_secure = true
reverse_proxy = true
CFG
}
ingress = {
enabled = true
className = var.ingress_class
path = "/"
hosts = [var.shopmicro_hostname]
}
})]
depends_on = [keycloak_openid_client.gateway]
}

upstreams คือสิ่งที่ทำให้นี่เป็น pattern แบบ request-path: ingress ของ oauth2-proxy ที่ path = "/" รับหน้าที่ทั้ง hostname ของ ShopMicro แล้ว oauth2-proxy เองก็ proxy ทุกอย่างที่มี session ที่ valid ต่อไปยัง Service shopmicro-gateway โดยตรง — ไม่ใช่แค่ route callback /oauth2/* reverse_proxy = true บอก oauth2-proxy ให้เชื่อ header X-Forwarded-* ที่ ingress set มา

เพราะ ingress ของ oauth2-proxy รับหน้าที่ var.shopmicro_hostname ที่ path = "/" แล้ว ingress ของ ShopMicro เอง บน host เดียวกันต้องหลีกทาง — หนึ่ง hostname มี ingress ได้หนึ่งตัว ปิดมันในไฟล์ values ต่อ cloud:

# charts/shopmicro/values-aws.yaml (amendment)
ingress:
enabled: false # oauth2-proxy's ingress fronts this host instead

ทำแบบเดียวกันใน values-gcp.yaml และ values-azure.yaml Service ของ ShopMicro ไม่เปลี่ยนแปลงและยังเข้าถึงได้จากใน cluster เหมือนเดิม — เปลี่ยนแค่ ingress สาธารณะของ hostname นั้นไปอยู่ที่ oauth2-proxy

Add the new inputs alongside the Keycloak ones from the last lesson:

inputs = {
# ...existing keycloak inputs...
oauth2_proxy_cookie_secret = get_env("OAUTH2_PROXY_COOKIE_SECRET")
oauth2_proxy_chart_version = "10.7.0"
}

Apply, confirm oauth2-proxy is running, then prove the redirect works:

Terminal window
cd live/aws/platform
terragrunt 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 90s

Now hit the gateway with no session cookie. An unauthenticated request must be redirected to login, not served the app:

Terminal window
curl -sI https://shop.aws.clouddeploy.example.com/
# HTTP/2 302
# location: https://shop.aws.clouddeploy.example.com/oauth2/start?rd=%2F

Follow the sign-in path and confirm it hands off to Keycloak:

Terminal window
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 แล้ว

ตรวจสอบความเข้าใจ:

  1. ทำไมการวาง oauth2-proxy ไว้ตรง ๆ บนเส้นทาง request ถึงทำงานเหมือนกันหลัง ALB, GKE ingress และ AGIC ในขณะที่ pattern แบบ external-auth annotation ทำไม่ได้?
  2. ทำไม ingress ของ oauth2-proxy กับ ingress ของ ShopMicro เองถึงรับหน้าที่ var.shopmicro_hostname พร้อมกันไม่ได้ และการเปลี่ยนบรรทัดเดียวอะไรที่ยก hostname ให้ oauth2-proxy?
  3. edge auth พิสูจน์ว่า user login แล้ว แต่จงใจ ไม่ ทำอะไร และความรับผิดชอบนั้นตกไปอยู่ที่ไหน?
  4. upstreams ชี้ oauth2-proxy ไปที่ shopmicro-gateway.shopmicro.svc.cluster.local ซึ่งเป็น in-cluster Service address ไม่ใช่ public ingress hostname ทำไมถึงเป็นตัวเลือกที่ถูกต้อง?

คุณ deploy oauth2-proxy ลงใน platform layer, ต่อสายเข้ากับ Keycloak OIDC client แล้ววางมันไว้ตรง ๆ บนเส้นทาง request ของ ShopMicro — ingress ของมันรับหน้าที่ hostname แล้ว proxy traffic ที่ authenticate แล้วต่อไปยัง gateway Service โดยตรง — จากนั้น verify ว่า request ที่ยังไม่ authenticate ได้ 302 ไปหน้า login ของ Keycloak ShopMicro ถูก authenticate ที่ขอบ เหมือนกันหลัง ALB, GKE ingress และ AGIC โดยตัว app เองไม่เปลี่ยนเลย

เท่านี้ก็จบเรื่อง identity ต่อไปคืออีกครึ่งของการควบคุม runtime — การตัดสินใจว่า อะไร ที่ app ทำโดยไม่ต้อง redeploy: Feature Flags (GrowthBook) →