เจอ Pod ตัวหนึ่งใน cluster เก่าที่ mount ~/.aws/credentials แบบ static key เข้าไปตรงๆ เมื่ออาทิตย์ก่อน — key นั้นสร้างมาตั้งแต่ปีก่อน ไม่เคย rotate เลยสักครั้ง แล้ว policy ที่แนบก็คือ AdministratorAccess ตรงๆ เพราะตอนนั้นมีคนรีบ deploy ให้ทันและไม่มีเวลานั่งไล่ permission ทีละตัว เจอแบบนี้แล้วสะอึกนิดหน่อย เพราะ Pod นั้นแค่ต้องอ่าน object จาก S3 bucket เดียว แต่ดันถือกุญแจทั้งบ้านไว้
เรื่องนี้เป็นปัญหาคลาสสิกของ IAM บน EKS ที่หลายทีมยังเจอ วันนี้เลยอยากพาดูสามเรื่องที่มักโดนมองข้ามพร้อมกัน: IRSA แทน static key, least privilege ที่ทำได้จริงไม่ใช่แค่ทฤษฎี และ audit logging ที่ตอบคำถามได้เวลามีคนถามว่า "ใครทำอะไรตอนไหน"
ทำไม static IAM key ถึงเป็นปัญหา
Key แบบ long-lived มีปัญหาซ้อนกันสามชั้น หนึ่งคือมันไม่หมดอายุเองถ้าไม่ตั้ง rotation policy เอาไว้ สองคือมันมักถูกแปะไว้ใน config file, environment variable หรือแย่กว่านั้นคือ commit เข้า repo โดยไม่ตั้งใจ (Gitleaks เจอเคสนี้บ่อยมาก) สามคือมันผูกกับ Pod ทุกตัวเท่ากันหมด ไม่ว่า Pod นั้นจะทำหน้าที่อะไร ถ้า Pod หนึ่งโดน compromise ก็เท่ากับ credential ทั้งชุดโดนไปด้วย
IRSA (IAM Roles for Service Accounts) แก้ปัญหานี้ตรงจุด คือให้แต่ละ Kubernetes ServiceAccount ผูกกับ IAM Role ผ่าน OIDC provider ของ EKS cluster เอง ไม่มี static key ไม่มีไฟล์ credential ต้องจัดการ Pod ขอ token ชั่วคราวผ่าน service account token ที่ EKS สร้างให้อัตโนมัติ แล้ว AWS SDK ฝั่ง Pod จะ assume role ผ่าน STS ให้เอง
ตั้ง IRSA แบบคร่าวๆ
ก่อนอื่นต้องเปิด OIDC provider ให้ cluster (ถ้ายังไม่เปิด):
eksctl utils associate-iam-oidc-provider \
--cluster my-cluster \
--approve
จากนั้นสร้าง IAM Role ที่มี trust policy ผูกกับ OIDC provider และ namespace/service account เจาะจง ไม่ใช่แบบเปิดกว้าง:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::<account-id>:oidc-provider/<oidc-provider-url>"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"<oidc-provider-url>:sub": "system:serviceaccount:my-namespace:my-app-sa",
"<oidc-provider-url>:aud": "sts.amazonaws.com"
}
}
}]
}
จุดสำคัญคือ sub ต้องระบุ namespace และ service account name ตรงตัว ไม่ใช่ wildcard เพราะถ้าเปิดกว้างเกินไป Pod อื่นใน cluster ที่ไม่เกี่ยวก็จะ assume role นี้ได้ด้วย เท่ากับย้อนกลับไปปัญหาเดิม
ฝั่ง Kubernetes ก็แค่ annotate ServiceAccount:
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-app-sa
namespace: my-namespace
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::<account-id>:role/my-app-s3-reader
แล้ว Deployment ก็ชี้ไปใช้ ServiceAccount นี้ตามปกติ ไม่ต้องแตะ SDK code เลยด้วยซ้ำ เพราะ AWS SDK เวอร์ชันใหม่ๆ รู้จัก AWS_WEB_IDENTITY_TOKEN_FILE ที่ EKS inject ให้อัตโนมัติอยู่แล้ว
Least privilege ที่ทำได้จริง ไม่ใช่แค่คำสวยๆ
ปัญหาของ least privilege คือทุกคนรู้ว่าดี แต่พอถึงเวลาจริงมักจะแนบ policy กว้างไว้ก่อนเพราะกลัวแอพพัง ตรงนี้มีวิธีที่พอช่วยได้บ้าง:
หนึ่ง เริ่มจาก policy แคบสุดเท่าที่รู้ว่าแอพต้องใช้ อย่างเคส S3 reader ด้านบน ควรจำกัด action ให้เหลือแค่ s3:GetObject และ s3:ListBucket บน ARN ของ bucket ที่ใช้จริงเท่านั้น ไม่ใช่ s3:* บน *
สอง ใช้ IAM Access Analyzer ช่วยดู policy ที่ generate จาก CloudTrail log ย้อนหลัง มันจะบอกว่า role นี้เรียก action อะไรบ้างจริงๆ ในช่วงที่ผ่านมา เอาผลนั้นมาเทียบกับ policy ที่แนบอยู่ ถ้าเจอ action ที่ไม่เคยถูกเรียกเลยใน 90 วัน ก็เป็นตัวเลือกแรกๆ ที่ควรตัดออก
สาม แยก Role ตามหน้าที่ อย่าใช้ Role เดียวกันสำหรับหลายๆ microservice เพราะแม้แต่ละ service จะ "ดูคล้ายกัน" แต่ blast radius ตอนมีปัญหาจะต่างกันมาก ถ้า Role เดียวใช้ร่วมกันสิบ service แล้ว service หนึ่งโดนเจาะ อีกเก้าก็เสี่ยงไปด้วย
Secrets ที่ไม่ใช่ IAM
Secrets management เป็นอีกเรื่องที่มักปนกับ IAM แต่จริงๆ ควรแยก IAM คือควบคุมว่า "ใคร/อะไรทำอะไรได้" ส่วน secrets คือ "ข้อมูลลับที่แอพต้องใช้" เช่น database password, API key ของ third-party ที่ไม่ใช่ AWS
แนวทางที่ใช้ได้จริงคือเก็บใน AWS Secrets Manager หรือ Parameter Store (SecureString) แล้วดึงเข้า Kubernetes ผ่าน External Secrets Operator แทนการฝัง secret ไว้ใน manifest หรือ Helm values ตรงๆ
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
namespace: my-namespace
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: SecretStore
target:
name: db-credentials-k8s
data:
- secretKey: password
remoteRef:
key: prod/my-app/db-password
ตรงนี้ IRSA ก็เข้ามาช่วยอีกชั้น เพราะ External Secrets Operator เองก็ต้องมี permission อ่าน Secrets Manager ซึ่งควรผูกผ่าน IRSA เช่นกัน ไม่ใช่ static key เหมือนกัน วนกลับมาที่หลักการเดิม
Audit logging — ตอบคำถามได้เวลาต้องสืบ
Least privilege ช่วยลดความเสียหายถ้ามีอะไรผิดพลาด แต่ audit log คือสิ่งที่ตอบได้ว่า "เกิดอะไรขึ้นจริง" เปิด CloudTrail ให้ log ทุก management event เป็นพื้นฐานที่ควรมีอยู่แล้ว แต่ที่มักถูกลืมคือ data event ของ S3 และ Lambda ซึ่งไม่เปิดมาให้ default ต้องเปิดเพิ่มเอง ถ้า bucket เก็บข้อมูลสำคัญแต่ไม่ได้เปิด data event ไว้ ตอนเกิดเหตุจะสืบย้อนไม่ได้ว่ามีใคร GetObject ไฟล์ไหนไปบ้าง
อีกจุดคือ EKS control plane logging เอง แนะนำให้เปิดอย่างน้อย audit และ authenticator log type ส่งเข้า CloudWatch Logs เพราะ log พวกนี้จะโชว์ว่า service account ไหน call Kubernetes API อะไร ซึ่งเวลาต้อง correlate กับ CloudTrail (ฝั่ง AWS API) จะได้ภาพครบทั้งสองชั้น ทั้ง container orchestration และ cloud resource
สุดท้ายคือให้ log พวกนี้ไป SIEM หรืออย่างน้อยตั้ง alert บน pattern ที่ผิดปกติ เช่น AssumeRole จาก IP แปลกๆ หรือ IAM policy ถูกแก้ตอนดึกนอกเวลางาน มี log เก็บไว้เฉยๆ โดยไม่มีคนดูก็แทบไม่ต่างจากไม่มี log
สรุปสั้นๆ
ทั้งสามเรื่องนี้เกี่ยวโยงกันเป็นชั้นๆ IRSA ตัดปัญหา static credential ที่จุดตั้งต้น least privilege จำกัดความเสียหายถ้า Role หลุด และ audit logging ทำให้สืบย้อนได้เมื่อมีเหตุ ทำแค่อย่างเดียวไม่พอ ต้องทำควบคู่กันถึงจะปิดช่องโหว่ได้จริง เคส Pod ที่ถือ Admin key ที่เล่าไปตอนต้น สุดท้ายก็แก้ด้วยการย้ายไป IRSA พร้อม policy เฉพาะ S3 bucket เดียว ใช้เวลาไม่ถึงบ่ายวันนั้นเอง แค่ต้องมีคนมานั่งไล่ก่อนว่า Pod นี้ต้องใช้ permission อะไรจริงๆ

Top comments (0)