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"
}
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 172.20.0.1 <none> 443/TCP 9h
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 ได้ โดยตัวอย่างจากชุมชนโอเพนซอร์สมีดังนี้
- Gangway
- Kubelogin
- k8s-oidc-helper (for Google IDP)
- 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 สำหรับผู้อ่านท่านไหนต้องการศึกษาข้อมูลเพิ่มเติม สามารถศึกษาเพิ่มเติมได้ตามหัวข้อด้านล่าง