OIDC Trust Policy 設定指南
OIDC 聯合讓 CI/CD pipeline 用短期 token 取代長期 access key 存取雲端資源。設定本身不複雜,但 trust policy 的 claim 條件寫錯一個字就會變成「任何 repo 都能假扮這個 role」或「完全無法 assume」。本篇是 GitHub Actions 與 AWS 之間的 OIDC 聯合的完整設定步驟,從建立 provider 到 trust policy 設計到測試驗證。其他 CI 平台(GitLab CI、CircleCI)的原理相同,差別只在 issuer URL 和 claim 結構:
| 平台 | Issuer URL | sub claim 格式範例 |
|---|---|---|
| GitHub Actions | token.actions.githubusercontent.com | repo:{org}/{repo}:ref:refs/heads/{branch} |
| GitLab CI | gitlab.com | project_path:{group}/{project}:ref_type:branch:ref:main |
| CircleCI | oidc.circleci.com/org/{org-id} | org/{org-id}/project/{project-id}/user/{user-id} |
本篇以 GitHub Actions 為主,其他平台替換 issuer URL 和 sub condition 即可。
建立 OIDC Provider
OIDC provider 是 AWS 帳號裡的一個資源,聲明「我信任這個外部 identity provider 簽發的 token」。GitHub Actions 的 OIDC issuer URL 是固定的,每個 AWS 帳號只需要建一個 provider。
1resource "aws_iam_openid_connect_provider" "github" {
2 url = "https://token.actions.githubusercontent.com"
3 client_id_list = ["sts.amazonaws.com"]
4 thumbprint_list = ["ffffffffffffffffffffffffffffffffffffffff"]
5}client_id_list 設為 sts.amazonaws.com 是 GitHub 官方建議的 audience 值。thumbprint_list 在 2023 年之後 AWS 不再用它驗證 GitHub 的憑證鏈(改用 AWS 自己維護的根憑證清單),但欄位仍然是必填,填 40 個 f 作為佔位值即可。
這個 provider 建一次就好。多個 role 可以共用同一個 provider,差別在各自的 trust policy 怎麼寫。
Trust Policy 設計:claim 收斂
Trust policy 決定「誰能假扮這個 role」。OIDC token 裡帶有多個 claim(描述「這是哪個 repo、哪個 branch、哪個 workflow 在跑」),trust policy 用 condition 比對這些 claim,全部命中才允許 assume。
最小可行的 trust policy
1data "aws_iam_policy_document" "ci_trust" {
2 statement {
3 actions = ["sts:AssumeRoleWithWebIdentity"]
4
5 principals {
6 type = "Federated"
7 identifiers = [aws_iam_openid_connect_provider.github.arn]
8 }
9
10 condition {
11 test = "StringEquals"
12 variable = "token.actions.githubusercontent.com:aud"
13 values = ["sts.amazonaws.com"]
14 }
15
16 condition {
17 test = "StringLike"
18 variable = "token.actions.githubusercontent.com:sub"
19 values = ["repo:my-org/my-app:ref:refs/heads/main"]
20 }
21 }
22}兩個 condition 各守一個邊界。aud 驗證 audience 對不對(防止其他用途的 token 被拿來 assume)。sub 驗證請求來自哪個 repo 和 branch——這是最關鍵的收斂點。
sub claim 的結構
GitHub Actions 的 sub claim 格式是 repo:{owner}/{repo}:{context},其中 context 隨觸發方式不同:
| 觸發方式 | sub claim 值 |
|---|---|
| push to branch | repo:my-org/my-app:ref:refs/heads/main |
| pull request | repo:my-org/my-app:pull_request |
| environment deploy | repo:my-org/my-app:environment:production |
| tag push | repo:my-org/my-app:ref:refs/tags/v1.0.0 |
| manual dispatch | repo:my-org/my-app:ref:refs/heads/main |
Trust policy 的 sub condition 要根據實際需要選擇收斂到哪個層級。只允許 main branch 的 push 就寫 repo:my-org/my-app:ref:refs/heads/main;只允許 production environment 的 deploy 就寫 repo:my-org/my-app:environment:production。
environment-based 收斂(推薦)
GitHub Actions 的 environment 功能讓 sub claim 帶上 environment 名稱。搭配 environment protection rules(required reviewers、wait timer),可以在 trust policy 層和 GitHub 層各設一道 gate:
1condition {
2 test = "StringEquals"
3 variable = "token.actions.githubusercontent.com:sub"
4 values = ["repo:my-org/my-app:environment:production"]
5}Workflow 裡對應的設定:
1jobs:
2 apply:
3 environment: production
4 permissions:
5 id-token: write
6 contents: read只有 workflow 宣告了 environment: production 且通過 environment 的 protection rules 後,runner 拿到的 token 才會帶上 environment:production 的 sub claim,才能 assume 這個 role。
Plan Role 與 Apply Role 分離
把 plan 和 apply 拆成兩個 role,各自給最小權限。plan 只需要 read 權限(讀 state、讀雲端現況),apply 需要 write 權限(建立/修改/刪除資源)。分離的好處是 PR 階段的 plan 即使被攻破,攻擊者也只能讀不能改。
1resource "aws_iam_role" "infra_plan" {
2 name = "infra-plan"
3 assume_role_policy = data.aws_iam_policy_document.plan_trust.json
4}
5
6resource "aws_iam_role" "infra_apply" {
7 name = "infra-apply"
8 assume_role_policy = data.aws_iam_policy_document.apply_trust.json
9}
10
11resource "aws_iam_role_policy_attachment" "plan_readonly" {
12 role = aws_iam_role.infra_plan.name
13 policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
14}Trust policy 的差異:plan role 允許任何 branch 的 PR 觸發(repo:my-org/my-app:pull_request);apply role 只允許 main branch 或 production environment(repo:my-org/my-app:environment:production)。
1jobs:
2 plan:
3 if: github.event_name == 'pull_request'
4 permissions:
5 id-token: write
6 contents: read
7 pull-requests: write
8 steps:
9 - uses: aws-actions/configure-aws-credentials@v4
10 with:
11 role-to-assume: arn:aws:iam::123456789012:role/infra-plan
12 aws-region: ap-northeast-1
13 - run: terraform plan -out=plan.tfplan
14
15 apply:
16 if: github.ref == 'refs/heads/main'
17 environment: production
18 permissions:
19 id-token: write
20 contents: read
21 steps:
22 - uses: aws-actions/configure-aws-credentials@v4
23 with:
24 role-to-assume: arn:aws:iam::123456789012:role/infra-apply
25 aws-region: ap-northeast-1
26 - run: terraform apply -auto-approve常見設定錯誤
audience 不匹配
1Error: Not authorized to perform sts:AssumeRoleWithWebIdentity最常見的原因是 trust policy 的 aud condition 值跟 OIDC provider 的 client_id_list 不一致。兩者都要是 sts.amazonaws.com。如果用了舊版的 configure-aws-credentials action(v1),它預設用 sigstore 作為 audience,跟 sts.amazonaws.com 對不上。確認 action 版本是 v4+。
sub condition 太寬
1condition {
2 test = "StringLike"
3 variable = "token.actions.githubusercontent.com:sub"
4 values = ["repo:my-org/*"]
5}這允許 my-org 底下任何 repo 的任何 branch assume 這個 role。如果組織裡有公開 repo 或 fork 權限寬鬆的 repo,攻擊者可以在那些 repo 裡觸發 workflow 來 assume 生產環境的 role。至少收斂到 repo 層級(repo:my-org/my-app:*),生產環境收斂到 branch 或 environment。
sub condition 太緊
1condition {
2 test = "StringEquals"
3 variable = "token.actions.githubusercontent.com:sub"
4 values = ["repo:my-org/my-app:ref:refs/heads/main"]
5}這只允許 push to main 觸發的 workflow。PR 觸發的 workflow 拿到的 sub 是 repo:my-org/my-app:pull_request,跟這個 condition 不匹配,plan 階段會失敗。如果 plan 需要在 PR 階段跑,plan role 的 trust policy 要加 PR 的 sub pattern。
忘記設 permissions
1jobs:
2 deploy:
3 # 缺少 permissions 區塊
4 steps:
5 - uses: aws-actions/configure-aws-credentials@v4GitHub Actions 的 OIDC token 只有在 workflow 宣告 permissions: { id-token: write } 時才會簽發。缺了這一行,configure-aws-credentials 拿不到 token,報「OIDC token not available」。這個錯誤訊息不直觀——它說的是 token 不存在,不是權限不夠。
多帳號時忘記指定 provider
如果組織有多個 AWS 帳號,每個帳號都要各自建 OIDC provider。trust policy 的 Federated principal 要指向本帳號的 provider ARN,不能跨帳號引用。跨帳號部署時,workflow 用不同的 role-to-assume 切換帳號,每個帳號的 role 各自信任同一個 GitHub OIDC issuer 但是各自獨立的 provider 資源。
測試與驗證
設定完成後的驗證步驟:
- 手動觸發 workflow:push 一個無害的 commit 到 main、開一個 test PR,觀察
configure-aws-credentials步驟是否成功 - 檢查 CloudTrail:搜尋
AssumeRoleWithWebIdentity事件,確認 source identity 和 assumed role 正確 - 反向驗證:從一個不在 trust policy 允許範圍的 repo 或 branch 觸發 workflow,確認 assume 被拒絕
- 權限範圍驗證:在 plan job 裡嘗試一個 write 操作(如
aws s3 rm),確認被拒絕——驗證 plan role 的 read-only 限制確實生效
1# 在 CloudTrail 搜尋 OIDC assume 事件
2aws cloudtrail lookup-events \
3 --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity \
4 --max-items 5驗證通過後,這套 OIDC 設定就取代了所有存放在 CI 環境變數裡的 access key。原有的 key 可以排程停用和刪除,排程的節奏見access key 輪替。trust policy 的持續維護重點是:新增 repo 時 sub condition 要同步更新、組織改名時 issuer 的 repo 路徑要全面修正。
時程參考:OIDC provider 建立 + trust policy 設計 + workflow 驗證約需 1-2 小時。OIDC provider 與 IAM role 本身不產生額外費用。
跨分類引用
- → 身分與憑證地基:OIDC 的概念基礎與權限邊界設計
- → infra 走 PR 流程:plan/apply 的 CI pipeline 怎麼用這裡設定好的 role
- → 跨帳號策略:多帳號環境下的 OIDC provider 配置