
ต่อ GitHub Actions เข้า AWS ด้วย OIDC: จำกัดสิทธิ์ถึง repo และ branch

หากต้องการ deploy จาก GitHub Actions ไป AWS โดยไม่เก็บ access key ระยะยาวใน GitHub Secrets ให้เพิ่ม GitHub เป็น OIDC identity provider ใน IAM แล้วสร้าง role ที่ตรวจค่า audience และ subject ของโทเค็นก่อนออกข้อมูลรับรองชั่วคราว คู่มือ OIDC ของ GitHub แสดงวิธีจำกัด subject ให้ตรงกับ repository และ branch ที่อนุญาต
ก่อนเขียน trust policy ให้ดูว่า job ใช้ GitHub environment หรือไม่ เพราะ job ที่ใช้ environment จะมีชื่อ environment ใน subject แทนส่วนอ้างอิง branch หากต้องจำกัด branch ในกรณีนี้ ให้กำหนด deployment branches ของ environment ควบคู่กับ trust policy ที่ระบุชื่อ environment อย่างเจาะจง
เพิ่ม GitHub เป็น identity provider ใน IAM
ในบัญชี AWS ปลายทาง ให้สร้าง Identity provider ประเภท OpenID Connect ใน IAM โดยใช้ Provider URL เป็น https://token.actions.githubusercontent.com และ Audience เป็น sts.amazonaws.com จากนั้นสร้าง IAM role ประเภท Web identity แล้วเลือก provider นี้ ค่า audience ใน IAM ต้องตรงกับค่า aud ที่ action ขอจาก GitHub; สำหรับการตั้งค่าปกติของ action ค่านั้นคือ sts.amazonaws.com
การมี provider เพียงอย่างเดียวยังไม่ได้กำหนดว่า workflow ใด deploy ได้ Role มี policy สองส่วนที่ทำหน้าที่ต่างกัน: trust policy ตัดสินว่าใครสวม role ได้ ส่วน permissions policy ตัดสินว่าเมื่อสวม role แล้วทำอะไรกับทรัพยากร AWS ได้ ตามคำแนะนำการสร้าง role ของ AWS ช่อง repository และ branch ในหน้าตั้งค่า GitHub เป็นตัวเลือกที่หากปล่อยว่างจะใช้ wildcard และ IAM ตรวจว่า trust policy สำหรับ GitHub มีเงื่อนไข sub ซึ่งไม่ใช่ wildcard เพียงอย่างเดียว
หลังสร้าง role ให้เปิดดู JSON ของ trust policy อีกครั้ง โดยเฉพาะเมื่อสร้างผ่านหน้าจอ IAM เพราะการเลือก organization อย่างเดียวอาจยังเปิดให้ repository หรือ branch อื่นในขอบเขตนั้นสวม role ได้ ระบุชื่อ repository และรูปแบบ subject ของ job ให้ตรงก่อนนำ ARN ของ role ไปใส่ใน workflow
กำหนด trust policy ให้ตรง owner, repository และ branch

สมมติว่า owner ชื่อ OWNER, repository ชื่อ REPO และต้องการให้เฉพาะ branch main สวม role ให้ใช้ StringEquals ตรวจทั้ง aud และ sub ค่า sub สำหรับรูปแบบที่มี ID ถาวรคือ repo:OWNER@OWNER_ID/REPO@REPO_ID:ref:refs/heads/main ส่วน repository ที่ยังใช้รูปแบบชื่ออย่างเดียวจะส่ง repo:OWNER/REPO:ref:refs/heads/main ต้องเลือกค่าเดียวที่ตรงกับโทเค็นของ repository จริง
ตัวอย่าง trust policy ต่อไปนี้ใช้รูปแบบที่มี ID ถาวร ให้แทน ACCOUNT_ID, OWNER, OWNER_ID, REPO และ REPO_ID ด้วยค่าจริงก่อนบันทึก: {"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Federated":"arn:aws:iam::ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"},"Action":"sts:AssumeRoleWithWebIdentity","Condition":{"StringEquals":{"token.actions.githubusercontent.com:aud":"sts.amazonaws.com","token.actions.githubusercontent.com:sub":"repo:OWNER@OWNER_ID/REPO@REPO_ID:ref:refs/heads/main"}}}]}
หาก repository ส่ง subject แบบไม่มี @ID ให้ตัดส่วน @OWNER_ID และ @REPO_ID ออกจากค่า sub ใน policy เท่านั้น GitHub Actions จะต้องส่งค่าที่ตรงทั้ง owner, repository และ branch จึงสวม role นี้ได้ การตั้ง sub เป็น repo:OWNER/REPO:* กว้างกว่าตัวอย่าง เพราะ wildcard ครอบคลุมบริบทการรันอื่นใน repository ด้วย รวมถึง branch และ environment ที่ไม่ได้ตั้งใจให้ deploy
รูปแบบ subject อาจเปลี่ยนตามการตั้งค่าของ repository จึงควรตรวจค่าที่ออกจริงหากได้รับข้อผิดพลาดจาก STS อย่าเดารูปแบบจากชื่อ repository เพียงอย่างเดียว และอย่าเปลี่ยน StringEquals เป็น wildcard เพื่อให้การขอ role ผ่านชั่วคราว เพราะจะเปลี่ยนขอบเขตผู้ที่สวม role ได้ไปพร้อมกัน
ถ้าใช้ environment ให้จำกัด branch อีกชั้น

เมื่อ job ระบุ environment เช่น production ค่า sub ตามรูปแบบชื่ออย่างเดียวจะเป็น repo:OWNER/REPO:environment:production และหาก repository ใช้ ID ถาวรก็ต้องเติม @OWNER_ID และ @REPO_ID ในส่วนต้นของค่าเดียวกัน ส่วน :ref:refs/heads/main จะไม่อยู่ใน subject รูปแบบ environment จึงนำสตริงของ branch ไปต่อท้ายค่า environment เพื่อบังคับให้ตรงทั้งคู่ไม่ได้
ให้เปลี่ยนเงื่อนไข sub ของ role เป็นค่า environment ที่ job ส่งจริง แล้วตั้ง deployment branch rule ของ GitHub environment production ให้รับเฉพาะ main วิธีนี้แบ่งหน้าที่ชัดเจน: AWS ตรวจว่าโทเค็นมาจาก repository และ environment ที่กำหนด ส่วน GitHub ตรวจว่า branch ใดเข้า environment นั้นได้ หากต้องการให้ IAM ตรวจ branch จาก sub โดยตรง ให้ใช้ job ที่ไม่ผูก environment และกำหนด :ref:refs/heads/main ใน trust policy
ตัวกรอง on: push และ branches: [main] ใน workflow จำกัดว่า push แบบใดเริ่ม job ได้ แต่ไม่ได้แทนเงื่อนไขของ role หากมี workflow อื่นใน repository ที่ขอโทเค็นสำหรับ environment เดียวกัน กฎของ environment และ trust policy ยังเป็นตัวกำหนดขอบเขตการเข้าถึง AWS ควรแยกชื่อ environment และ role ของ staging กับ production ให้ตรงกันทุกจุด
ให้ role ทำได้เฉพาะงาน deploy ที่จำเป็น
เมื่อกำหนดผู้ที่สวม role แล้ว ให้แนบ permissions policy ตามคำสั่ง deploy จริง ตัวอย่างแบบสมมติที่อัปโหลดไฟล์เดียวไป Amazon S3 ใช้ Action เป็น s3:PutObject และ Resource เป็น arn:aws:s3:::example-deploy-bucket/releases/index.html โดยแทนชื่อ bucket และ key ด้วยค่าของระบบตน การระบุ ARN ของ object ไฟล์เดียวช่วยจำกัดผลของข้อมูลรับรองที่ job ได้รับ
JSON สำหรับตัวอย่างนี้คือ {"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:PutObject","Resource":"arn:aws:s3:::example-deploy-bucket/releases/index.html"}]} Policy นี้ไม่ได้ให้สิทธิ์อ่าน ลบ หรือแสดงรายการ object หากงานจริงอัปโหลดหลายไฟล์จึงค่อยขยาย Resource ไปยัง prefix ที่จำเป็น และหากปลายทางกำหนดเงื่อนไขเพิ่มเติม เช่น การเข้ารหัสด้วย KMS ก็ต้องพิจารณาสิทธิ์ที่เงื่อนไขนั้นต้องใช้แยกต่างหาก
อย่าสับสนการแก้ permissions policy กับการแก้ trust policy หาก role สวมได้แต่ S3 ปฏิเสธ PutObject การเพิ่ม repository หรือ branch ใน trust ไม่ช่วยให้เขียน object ได้ ในทางกลับกัน การให้ s3:PutObject กว้างขึ้นก็ไม่ช่วย job ที่มีค่า sub ไม่ตรงกับเงื่อนไขการสวม role
ตั้ง workflow เพื่อขอข้อมูลรับรองชั่วคราว
ให้ job มี permissions เป็น id-token: write เพื่อขอโทเค็น OIDC และเพิ่ม contents: read หากใช้ actions/checkout จากนั้นเรียก aws-actions/[email protected] โดยส่ง role-to-assume เป็น ARN ของ role และ aws-region เป็น Region ที่ใช้งาน แล้ววางคำสั่ง AWS CLI ไว้หลังขั้นตอนนี้ การให้ id-token: write เปิดสิทธิ์ขอโทเค็น ไม่ได้ให้สิทธิ์แก้ทรัพยากร AWS ด้วยตัวเอง
- กำหนด on: push สำหรับ branches: [main] และตั้ง permissions ของ job ให้มี id-token: write รวมถึง contents: read เมื่อจะ checkout ไฟล์จาก repository
- เพิ่มขั้นตอน actions/checkout หากต้องใช้ไฟล์ใน repository แล้วเรียก action รับข้อมูลรับรองด้วย role-to-assume และ aws-region โดยไม่ส่ง aws-access-key-id หรือ aws-secret-access-key
- เพิ่มขั้นตอน run: aws sts get-caller-identity หลังรับข้อมูลรับรองและก่อนคำสั่ง deploy เพื่อดูว่ากำลังใช้บัญชีและ role ที่ตั้งใจไว้
- สำหรับตัวอย่าง S3 แบบสมมติ ให้เรียก aws s3api put-object --bucket example-deploy-bucket --key releases/index.html --body ./index.html หลังตรวจตัวตนสำเร็จ
หาก job ใช้ environment ให้ระบุ environment: production ที่ระดับ job และใช้ค่า sub รูปแบบ environment ใน trust policy อย่านำ policy สำหรับ branch อย่างเดียวมาใช้โดยไม่ปรับ เพราะ job จะส่ง subject คนละรูปแบบ ARN ของ role เป็นค่ากำหนดการเชื่อมต่อได้โดยไม่ต้องเก็บ access key ระยะยาวใน GitHub Secrets
แยกสาเหตุของ AccessDenied ด้วยผล STS
เริ่มจากผล aws sts get-caller-identity ใน job ที่ได้รับอนุญาต ค่า Account ควรเป็นบัญชี AWS เป้าหมาย และ Arn ควรระบุ assumed role ที่สร้างไว้ ผลนี้บอกว่า job ได้ข้อมูลรับรองของ role ที่คาดหมายแล้ว แต่ยังไม่ได้พิสูจน์ว่า role มีสิทธิ์เรียกทุกคำสั่งในขั้นตอน deploy
หากขั้นตอนรับข้อมูลรับรองล้มเหลวด้วย Not authorized to perform sts:AssumeRoleWithWebIdentity ให้เทียบ provider ในบัญชีปลายทาง, ค่า aud, ค่า sub ที่โทเค็นส่งจริง และ ARN ของ role ใน workflow จุดที่ควรดูเป็นพิเศษคือ policy ใช้รูปแบบไม่มี @ID แต่โทเค็นมี ID ถาวร หรือ job ใช้ environment แล้ว policy ยังคาด :ref:refs/heads/main แก้เฉพาะค่าที่ไม่ตรงและคงเงื่อนไข StringEquals ที่เจาะจงไว้
หาก get-caller-identity ผ่านแต่คำสั่งอัปโหลดถูกปฏิเสธ ให้ตรวจ Action และ Resource ใน permissions policy เทียบกับ bucket, key และเงื่อนไขของปลายทาง สำหรับตัวอย่างไฟล์เดียว ชื่อ object ใน ARN ต้องตรงกับ key ที่คำสั่ง put-object ใช้ การตรวจกรณี branch ที่ไม่อนุญาตต้องมี workflow ที่เริ่มจาก branch นั้นได้ก่อน; เมื่อส่งโทเค็นที่มี sub ต่างจาก policy ขั้นตอนสวม role ควรถูกปฏิเสธก่อนถึงคำสั่ง deploy
อ่านเพิ่มเติม:
บทความที่เกี่ยวข้อง


GitHub Actions หรือ GitLab CI: นาทีถูกกว่าอาจสร้างเสร็จช้ากว่า

กัน API key หลุดบน GitHub: Push Protection ข้ามไฟล์เกิน 50 MB

tensorlake 0.5.144 ขโมยคีย์ตอนติดตั้ง—อย่าเพิ่งหมุนโทเค็นผิดลำดับ

Cloudflare R2 หรือ Amazon S3: egress ฟรีแลกกับอัปโหลดที่ช้ากว่า

Supabase หรือ Neon: ฐานข้อมูลที่หลับได้อาจถูกกว่า จนทราฟฟิกทำงานทั้งวัน
สมัครรับจดหมายข่าวของเรา
รับข่าวสารล่าสุดเกี่ยวกับ Web3, AI และคริปโตส่งตรงถึงกล่องจดหมายของคุณ