AWS Thai Blog

นำ OIDC Identity Provider มาเชื่อมต่อกับ Amazon EKS เพื่อใช้ในการยืนยันตัวตนได้อีกช่องทาง

บทความนี้ส่วนหนึ่งแปลมาจาก Introducing OIDC identity provider authentication for Amazon EKS ที่เขียนร่วมโดย Rashmi Dwaraka, Mike Stefaniak และ Paavan Mistry จาก AWS

ในช่วงปี 2018 Amazon EKS พึ่งเปิดตัวใหม่ ในส่วน authentication ของ Amazon EKS นั้น รองรับแค่ AWS IAM แต่พอผู้ใช้งาน Amazon EKS มีมากขึ้นไม่ว่าจากองค์กรระดับเล็กจนถึงระดับใหญ่ และทาง AWS ได้รับข้อเสนอแนะของลูกค้าเหล่านั้น ว่ามีความต้องการจะใช้งานการยืนยันตัวตน (authentication) จาก Identity Provider ที่มีใช้งานอยู่แล้ว แต่ถ้าต้องมาจัดการผู้ใช้ผ่าน AWS IAM อีกช่องทาง จะทำให้ดูแลลำบากขึ้นเมื่อผู้ใช้งานคลัสเตอร์มีจำนวนมากขึ้น ทาง AWS ได้ตระหนักถึงความต้องการนี้ จึงได้พัฒนา Amazon EKS ให้สามารถรองรับการยืนยันตัวตนผ่าน OpenID Connect (OIDC) Identity Provider ได้อีกช่องทางหนึ่ง

บางท่านอาจสงสัย OIDC คืออะไร ซึ่งคำนี้ย่อมาจาก OpenID Connect เป็นโปรโตคอลที่ถูกออกแบบสำหรับงานด้านยืนยันตัวตน โดยอ้างอิงมาตรฐานจาก OAuth 2.0 สำหรับในส่วนของ OIDC นั้นจะต่อยอดจาก OAuth 2.0 อีกชั้น โดยเพิ่มข้อมูลเกี่ยวกับคนที่ล็อคอินเข้าระบบและประวัติของเขา โดย OIDC IDP นั้นมีนำมาใช้งานได้ทั้งรูปแบบเปิด (public) หรือที่ใช้ภายในองค์กรอยู่แล้ว (private) ก็ได้เช่นกัน

ในบทความนี้ เราจะใช้ Amazon Cognito เป็น OIDC identity provider โดย Cognito User Pool เป็น directory ของผู้ใช้ที่มีความปลอดภัย และติดตั้งได้ง่ายโดยไม่ต้องจัดการเซิร์ฟเวอร์เอง และในบทความนี้จะครอบคลุมในส่วนการสร้างผู้ใช้และกลุ่มของผู้ใข้ การนำ group key จาก ID token มาใช้ การเชื่อมต่อ OIDC IDP กับคลัสเตอร์ การกำหนดสิทธิสำหรับผู้ใช้งานผ่าน Kubernetes RBAC และ การตั้งค่าให้กับ CLI เพื่อให้ผู้ใช้งานสามารถเข้าถึงคลัสเตอร์ได้ อย่างไรก็ดีถ้าผู้อ่านมี OIDC IDP ที่ใช้งานอยู่แล้วได้ สามารถอ่านเพื่อทำความเข้าใจและนำไปประยุกต์ใช้กับ OIDC IDP ของตนได้

สิ่งที่ควรทำความเข้าใจก่อน

สำหรับบทความนี้ ผู้อ่านควรมีความเข้าใจพื้นฐานเกี่ยวกับโปรโตคอล OIDC และ OAuth2.0 ที่เกี่ยวข้องกับ JSON Web Token (JWT) นอกจากนี้ คุณจําเป็นต้องมีความเข้าใจพื้นฐานเกี่ยวกับ Amazon Cognito และ AWS CDK สำหรับบทความนี้ เราจะใช้ AWS CLI, AWS CDK และ jq ในการติดตั้งการเชื่อมต่อ OIDC กับ Amazon EKS ท้ายสุด ผู้อ่านต้องมีสิทธิ์ในการสร้างและจัดการคลัสเตอร์ Amazon EKS และ Amazon Cognito User Pool

ขั้นตอนที่ 1 สร้าง Cognito OIDC IDP โดยใช้ AWS CDK

สำหรับการติดตั้ง OIDC IDP เราจะใช้ AWS CDK ตามด้านล่างเพื่อสร้างและกําหนดค่า Cognito User Pool สำหรับการเริ่มต้นโปรเจ็ค AWS CDK ให้สร้างไดเร็กทอรี่แล้วรันคำสั่งเริ่มต้นของ AWS CDK ด้วยภาษา TypeScript ดังนี้

mkdir -p cognitouserpool && cd cognitouserpool && cdk init -l typescript

ติดตั้งแพ็คเกจ Amazon Cognito จาก AWS Construct Library โดยใช้คําสั่งดังนี้

npm install @aws-cdk/aws-cognito

เปิดไฟล์ ./lib/cognitouserpool-stack.ts แล้วแทนที่โค้ดที่ถูกสร้างขึ้นโดยอัตโนมัติด้วยโค้ดด้านล่างนี้

import * as cdk from '@aws-cdk/core';
import * as cognito from '@aws-cdk/aws-cognito';

export class CognitouserpoolStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const pool = new cognito.UserPool(this, 'myuserpool', {
      userPoolName: 'oidc-userpool',
      passwordPolicy: {
        minLength: 8,
        requireLowercase: false,
        requireUppercase: false,
        requireDigits: false,
        requireSymbols: false,
      },
      selfSignUpEnabled: true,
      signInAliases: {
        email: true,
      },
      autoVerify: {
        email: false,
      },
      accountRecovery: cognito.AccountRecovery.NONE,
      signInCaseSensitive: false,

    });

    const client = pool.addClient('oidc-client', {
      generateSecret: false,
      authFlows: {
        adminUserPassword: true,
        userPassword: true,
      },
      oAuth: {
        flows: {
          implicitCodeGrant: true,
        }
      },
    });

    pool.addDomain("CognitoDomain", {
      cognitoDomain: {
        domainPrefix: "oidc-userpool",
      },
    });

    const region = cdk.Stack.of(this).region
    const urlsuffix = cdk.Stack.of(this).urlSuffix
    const issuerUrl = `https://cognito-idp.${region}.${urlsuffix}/${pool.userPoolId}`;
    new cdk.CfnOutput(this, 'IssuerUrl', { value: issuerUrl })
    new cdk.CfnOutput(this, 'PoolId', { value: pool.userPoolId })
    new cdk.CfnOutput(this, 'ClientId', { value: client.userPoolClientId })
  }
}

const devEnv = {
  account: process.env.CDK_DEFAULT_ACCOUNT,
  region: process.env.CDK_DEFAULT_REGION,
}

const app = new cdk.App()

new CognitouserpoolStack(app, 'oidc-demo-dev', { env: devEnv})

app.synth();

รันคําสั่ง npm run build && cdk deploy สําหรับ AWS CDK เพื่อสร้าง Cognito User Pool และแสดงค่า IssuerUrl, PoolId และ ClientId ออกมา เราจะใช้ค่าเหล่านี้ในการกําหนดค่า EKS cluster ของเราสำหรับการเชื่อมต่อ OIDC IDP ในขั้นตอนที่ 3 ต่อไป

✅ CognitouserpoolStack

Outputs:
CognitouserpoolStack.ClientId = 702vqsrjicklgb7c5b7b50i1gc
CognitouserpoolStack.IssuerUrl = https://cognito-idp.us-west-2.amazonaws.com/us-west-2_re1u6bpRA
CognitouserpoolStack.PoolId = us-west-2_re1u6bpRA

เซฟค่าไว้ใน environment variable เพื่อใช้งานได้สะดวกขึ้นในคำสั่งถัดๆ ไป

CLIENT_ID=702vqsrjicklgb7c5b7b50i1gc && \
ISSUER_URL=https://cognito-idp.us-west-2.amazonaws.com/us-west-2_re1u6bpRA && \
POOL_ID=us-west-2_re1u6bpRA

นำค่า IssuerUrl, PoolId และ ClientId ที่สร้างขึ้นใน Cognito User Pool ให้เราสร้างกลุ่มชื่อsecret-reader และเพิ่มผู้ใช้ใหม่ด้วยอีเมล์ test@example.com เข้าไปในกลุ่ม ผู้อ่านสามารถเพิ่มผู้ใช้มากกว่าหนึ่งรายได้โดยทำตามขั้นตอนของการสร้างผู้ใช้ด้านล่าง สําหรับคำสั่งของการสร้างผู้ใช้และกลุ่ม และรหัสผ่านที่ใช้ในบทความนี้เป็นไปเพื่อการสาธิตเท่านั้น ถ้านำไปใช้งานจริง ควรตั้งค่าให้สอดคล้องกับนโยบายด้านความปลอดภัยขององค์กร

aws cognito-idp admin-create-user --user-pool-id $POOL_ID --username test@example.com --temporary-password password
 
aws cognito-idp admin-set-user-password --user-pool-id $POOL_ID --username test@example.com --password Blah123$ --permanent
 
aws cognito-idp create-group --group-name secret-reader --user-pool-id $POOL_ID 
 
aws cognito-idp admin-add-user-to-group --user-pool-id $POOL_ID --username test@example.com --group-name secret-reader

ขั้นตอนที่ 2: ทำความเข้าใจ ID token เพื่อนำมาใช้อ้างอิงกับฟิลด์ group claim

การกําหนดค่าตัวให้บริการระบุตัวตน OIDC ในขั้นตอนที่ 3 นั้น สิ่งสําคัญคือต้องเข้าใจ payload ของ ID token ที่ IDP ส่งกลับมาเมื่อมีการยืนยันตัวตนสําเร็จ ซึ่ง ID token เป็นโทเค็นประเภทหนึ่งที่มีข้อมูลอ้างอิงเกี่ยวกับการยืนยันตัวตนของผู้ใช้จาก IDP ซึ่งอาจมีข้อมูลอ้างอิงอื่นๆ ตามที่ร้องขอด้วย โดย ID token ถูกแสดงในรูปแบบ JWT และข้อมูลในบางฟิลด์สามารถนำมาใช้อ้างอิงกับ group claim ในขั้นตอนที่ 3 เพื่อให้คลัสเตอร์ Amazon EKS สามารถพิสูจน์ตัวตนกับกลุ่มของผู้ใช้ใน IDP ผ่าน ClusterRoleBinding แทนที่จะเป็นผู้ใช้แต่ละคน

สําหรับ Cognito User Pool ให้ใช้คําสั่งดังต่อไปนี้เพื่อตรวจสอบความถูกต้องของผู้ใช้กับ Cognito IDP เพื่อดึง ID token ที่เป็น JWT มา แล้วใช้คำสั่ง base64 ถอดรหัสจาก payload ของ token นั้น

aws cognito-idp admin-initiate-auth --auth-flow ADMIN_USER_PASSWORD_AUTH \
--client-id $CLIENT_ID \
--auth-parameters USERNAME=test@example.com,PASSWORD=Blah123$ \
--user-pool-id $POOL_ID --query 'AuthenticationResult.IdToken' \
--output text | cut -f 2 -d. | base64 --decode | awk '{print $1"}"}' | jq

โดยปกติ payload ของ Cognito ID token จะมีรายละเอียดตามด้านล่าง และฟิลด์จาก payload จะถูกใช้อ้างอิงกับฟิลด์ group claim ในขั้นตอนที่ 3 สําหรับ token ID ที่ออกโดย Amazon Cognito group key จะเป็น cognito:groups ดังแสดงด้านล่าง แต่ฟิลด์ที่ใช้อาจจะแตกต่างกันไปสําหรับ OIDC IDP อื่นๆ

{
  "sub": "86f7130a-5605-4c05-b402-c970b27633ce",
  "aud": "702vqsrjicklgb7c5b7b50i1gc",
  "cognito:groups": [ "secret-reader" ],
  "event_id": "aa0723aa-12f3-49f1-9a21-7a7d542129bd",
  "token_use": "id",
  "auth_time": 1612760751,
  "iss": "https://cognito-idp.us-west-2.amazonaws.com/us-west-2_re1u6bpRA",
  "cognito:username": "86f7130a-5605-4c05-b402-c970b27633ce",
  "exp": 1612764351,
  "iat": 1612760751,
  "email": "test@example.com"
}

ขั้นตอนที่ 3: เชื่อมต่อ OIDC IDP เข้ากับ Amazon EKS คลัสเตอร์

ขั้นตอนนี้ เราจะใช้ Amazon EKS Console ในการสร้าง cluster และต่อกับ OIDC identity provider สำหรับการสร้าง Amazon EKS ใหม่ให้ทำตามคู่มือนี้ เมื่อ cluster ถูกสร้างเสร็จแล้ว ให้คลิกที่ปุ่ม ‘Associate Identity Provider’ ภายใต้แท็บ Configuration > Authentication ใน console สําหรับการเชื่อมต่อ OIDC IDP กับคลัสเตอร์

หลังจากนั้น ใส่ค่า Identity Provider, Issuer URL, Client ID (อ้างถึงในชื่อ Audience หรือคีย์ aud ใน Step 2 ของ JWT ข้างต้น), username กับ groups claim และ groups prefixes ตามความเหมาะสม แล้วคลิก ‘Associate’ เพื่อต่อ IDP เข้ากับคลัสเตอร์ โดยจะใช้เวลาประมาณ 15-20 นาทีถึงจะเสร็จสมบูรณ์ ถ้าสังเกตที่ฟิลด์ ‘Groups claim’ จะมีการอ้างอิง cognito:groups ที่เราได้ในขั้นตอนที่ 2 จาก Cognito User Pool ID token

ค่าต่างๆของ OIDC Identity Provider ควรมีดังต่อไปนี้ หลังจากที่สเตตัส Active

ขั้นตอนที่ 4: กําหนดสิทธิ์การใช้งานผ่าน Kubernetes RBAC

ตอนนี้คลัสเตอร์ EKS ได้ต่อกับ OIDC identity provider แล้ว ให้เรามากําหนดสิทธิของผู้ใช้และ client เพื่อทําการยืนยันตัวตนกับคลัสเตอร์ อย่างไรก็ตาม ก่อนที่จะทดสอบการยืนยันตัวตน เราต้องกำหนดสิทธิ์ให้ผู้ใช้สามารถเข้าถึง k8s resources ในคลัสเตอร์ หลังจากผู้ใช้ยืนยันตัวตนผ่านแล้ว โดยเราสามารถนำ RoleBinding และ ClusterRoleBinding มาใช้ในการกำหนดว่าผู้ใช้หรือกลุ่มของผู้ใช้ได้รับสิทธิ์อะไรบ้าง สำหรับวิธีการเข้าถึงคลัสเตอร์ จะใช้คำสั่ง kubectl แต่ก่อนอื่นให้อัปเดตไฟล์ ~/.kube/config โดยรันคําสั่งตามด้านล่าง หากผู้อ่านใช้ IAM เดียวกับที่สร้างคลัสเตอร์ จะได้สิทธิ์เป็น admin ของคลัสเตอร์ตั้งแต่ตั้งต้น

aws eks update-kubeconfig --name oidc-test

kubectl get svc

ถ้าเข้าถึงคลัสเตอร์ได้สำเร็จ ผลลัพธ์ที่ได้จะคล้ายกับเอาต์พุตด้านล่าง

NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
kubernetes   ClusterIP   172.20.0.1   <none>        443/TCP   9h

หลังจากนั้น ให้ทำการสร้างไฟล์ clusterrole-read-secrets.yaml ด้วยข้อมูลตามด้านล่าง เพื่อที่จะสร้าง ClusterRole ชื่อ read-secrets ที่มีสิทธิในการอ่าน secrets

kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: read-secrets
rules:
- apiGroups:
  - ""
  resources:
  - secrets 
  verbs:
  - 'get'
  - 'watch'
  - 'list'

รันคำสั่งดังต่อไปนี้ เพื่อสร้าง ClusterRole

kubectl create –f clusterrole-read-secrets.yaml

ต่อมาทำการสร้าง ClusterRoleBinding เพื่อผูก ClusterRole กับกลุ่มผู้ใช้ที่ขึ้นต้นด้วย gid: ที่เราระบุไว้ในขั้นตอนที่ 3

kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: read-secrets-role-binding
  namespace: default
subjects:
- kind: Group
  name: "gid:secret-reader"
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: read-secrets
  apiGroup: rbac.authorization.k8s.io

ท้ายสุด สร้างไฟล์ clusterrolebinding-read-secrets.yaml และใส่ข้อมูลตามด้านบน แล้วเซฟ
จากนั้นรันคำสั่งด้านล่างเพื่อสร้าง ClusterRoleBinding

kubectl create -f clusterrolebinding-read-secrets.yaml

ขั้นตอนที่ 5: ทดสอบการเข้าถึงคลัสเตอร์

หลังจากที่ทำขั้นตอนที่ 3 และขั้นตอนที่ 4 เสร็จเรียบร้อย เรามาเริ่มทดสอบการเข้าถึงคลัสเตอร์ด้วยผู้ใช้จาก OIDC IDP โดยเริ่มต้นให้ทำการตั้งค่าการเข้าใช้งานคลัสเตอร์กับ kubectl โดยเราสามารถใช้ OIDC authenticator ซึ่งจะนำค่า id_token มาใช้เป็น bearer token โดยเราต้องระบุค่าใน kubeconfig เอง ซึ่งได้แก่ id_token, refresh_token, client_id และ client_secret ที่ได้จากการล็อกอินผ่าน OIDC IDP

โดยค่า id_token และ refresh_token ได้มาจาก OIDC IDP ส่งกลับมาให้ โดยรันคำสั่งการยืนยันตัวตนกับ Cognito ก่อน ตามด้านล่าง

aws cognito-idp admin-initiate-auth --auth-flow ADMIN_USER_PASSWORD_AUTH \
--client-id $CLIENT_ID \
--auth-parameters USERNAME=test@example.com,PASSWORD=Blah123$ \
--user-pool-id $POOL_ID \
--query 'AuthenticationResult.[RefreshToken, IdToken]'

หลังจากนั้น ให้รันคําสั่งด้านล่างเพื่อตั้งค่า OIDC authenticator แทนค่า <refresh_token> และ <id_token> ด้วยค่าที่ได้จากการรันคําสั่งข้างบน

kubectl config set-credentials cognito-user \
--auth-provider=oidc \
--auth-provider-arg=idp-issuer-url=$ISSUER_URL \
--auth-provider-arg=client-id=$CLIENT_ID \
--auth-provider-arg=refresh-token=<refresh_token> \
--auth-provider-arg=id-token=<id_token>

เพิ่ม context ใน kubeconfig เพื่อใช้งานได้สะดวกขึ้น และสลับไปใช้ context นี้

kubectl config set-context oidc-secret-reader —cluster arn:aws:eks:us-west-2:<account_id>:cluster/oidc-test --user cognito-user && kubectl config use-context oidc-secret-reader

ทำการทดสอบว่าเข้าใช้งานได้หรือไม่ โดยรันคําสั่ง kubectl get secrets และ kubectl get nodes ผลลัพธ์ที่ได้ควรเป็นตามนี้

$ kubectl get secrets                      
NAME                  TYPE                                  DATA   AGE
default-token-cwpl9   kubernetes.io/service-account-token   3      2d21h
 
$ kubectl get nodes                        
Error from server (Forbidden): nodes is forbidden: User "test@example.com" cannot list resource "nodes" in API group "" at the cluster scope

จากผลลัพธ์ข้างต้นแสดงให้เห็นว่า ผู้ใช้จาก OIDC ที่ถูกสร้างไว้ใน Cognito User Pool ได้ผ่านการยืนยันตัวตน โดยใช้ OIDC cluster authentication ของคลัสเตอร์ที่ติดตั้งไว้ในขั้นตอนที่ 3 และผู้ใช้สามารถเข้าถึง secrets ในคลัสเตอร์ได้ตาม k8s RBAC ที่กําหนดไว้ในขั้นตอนที่ 4

ถ้าผู้อ่านเลือกที่จะไม่อัปเดต kubeconfig เพื่อที่จะเข้าถึงคลัสเตอร์ สามารถใช้ flag --token แทน และระบุ ID token เป็นพารามิเตอร์ตอนรันคําสั่ง kubectl ตามด้านล่าง

kubectl --token=<IDTOKEN> get secrets

นอกจากวิธีที่กล่าวมาข้างต้นนั้น ผู้อ่านสามารถเลือกใช้แอปพลิเคชันช่วยเหลืออื่นไม่ว่าจะเป็นรูปแบบของเว็บหรือ CLI เพื่อใช้ในการยืนยันตัวตนกับ Cognito IDP ได้ โดยตัวอย่างจากชุมชนโอเพนซอร์สมีดังนี้

  1. Gangway
  2. Kubelogin
  3. k8s-oidc-helper (for Google IDP)
  4. k8s-auth-client

ระวังสับสน OIDC provider URL กับ OIDC IDP ในแท็บ authentication

ใน AWS คอนโซล ผู้อ่านอาจสังเกตเห็นรายละเอียด OIDC อีกอันหนึ่ง ซึ่งเป็นของ OpenID Connect provider URL ตามที่แสดงในหน้าจอคอนโซลด้านล่าง

สำหรับการใช้งาน OIDC authentication ที่กล่าวมาตั้งแต่ต้นบทความนั้น มีไว้ใช้ในการยืนยันตัวตนผ่าน JWT ที่ออกโดย OIDC IDP เพื่อเข้าถึง k8s API server ในขณะที่ Open ID Connect provider URL ไว้ทำ federation ของ k8s service account tokens ที่ออกโดย k8s API server กับ AWS IAM

OpenID Connect provider URL ถูกใช้โดย AWS IAM ในการสร้างความเชื่อถือระหว่าง OIDC IDP ซึ่งในกรณีนี้คือ k8s API server สําหรับ service account และ AWS account โดย k8s API server จะส่ง token ที่ออกโดย OpenID Connect provider URL ไปยัง AWS STS และรับ IAM temporary role credentials จาก AWS STS มา เพื่อให้ pod สามารถนำไปใช้ในการคุยกับ AWS service ที่ถูกอนุญาตไว้ใน IAM role ได้ ผู้อ่านสามารถอ่านรายละเอียดเพิ่มเติมได้จากลิงค์นี้

ขั้นตอนการลบ resources ที่ใช้ในบทความนี้

1. ลบ ClusterRole และ ClusterRoleBinding

kubectl delete clusterrole read-secrets && kubectl delete clusterrolebinding read-secrets-role-binding

2. ลบการเชื่อมต่อ OIDC กับคลัสเตอร์โดยใช้ AWS CLI

aws eks disassociate-identity-provider-config --cluster-name oidc-test --identity-provider-config '{"name": "oidc-config", "type": "oidc"}'

3. ลบ Cognito User Pool โดยใช้คำสั่ง AWS CDK

cdk destroy

4. ลบไดเร็คทอรี่ของโปรเจ็ค AWS CDK

cd .. && rm -rf cognitouserpool

กล่าวโดยสรุป

ในบทความนี้ ได้อธิบายวิธีการเชื่อมต่อ OIDC IDP เข้ากับ Amazon EKS ไว้ใช้สำหรับทำการยืนยันตัวตน ซึ่งช่วยให้ผู้ดูแลระบบสามารถจัดการผู้ใช้ด้วยวิธีเดิมที่คุ้นเคยได้ โดยที่ไม่ต้องมาจัดการผู้ใช้ผ่าน AWS IAM สำหรับผู้อ่านท่านไหนต้องการศึกษาข้อมูลเพิ่มเติม สามารถศึกษาเพิ่มเติมได้ตามหัวข้อด้านล่าง

  1. การยืนยันตัวตนของ Kubernetes
  2. ข้อกําหนดของ OpenID Connect
  3. ภาพรวมของ JSON Web Token
  4. การจัดการสิทธิ์และการเข้าถึง Amazon EKS
  5. การยืนยันตัวตน OIDC ของ Amazon EKS