昨天最後留下一個問題:要讓 GitHub Actions 幫我們 push Image、通知 EC2 換版,GitHub 的機器要怎麼拿到 AWS 的權限?
今天就來設定 OIDC,讓 workflow 每次執行時才向 AWS 換一組短期的權限,GitHub 上完全不用存 access key ㄅ!
昨天有說到最直接的做法,是幫 GitHub Actions 建一個 IAM User,產生一組 access key 存進 GitHub 的 Secrets,workflow 執行時再拿出來用。這樣確實可行,但跟今天要用的 OIDC 比,有各自的優缺點:
| access key 存在 GitHub | OIDC | |
|---|---|---|
| GitHub 上存了什麼 | 可以直接操作 AWS 的 access key | 只有 Role 的 ARN,它不是密碼 |
| 有效多久 | 直到手動停用為止 | 每次執行才換一組,預設 1 小時後失效 |
| 外洩了會怎樣 | 拿到的人可以一直用,直到我們發現 | 沒有長期的 key 可以偷,換到的權限也很快就過期 |
| 誰能使用 | 任何拿到 key 的人 | 只有指定的 repo 和 branch |
| 要不要定期更換 | 要,而且很容易忘記 | 不用,每次都是新的 |
OIDC 的全名是 OpenID Connect,是一套用來證明「我是誰」的標準。套到我們的情境,就是由 GitHub 會開一張身分證明給 workflow,而 AWS 認得這張證明,就發短期的權限給它。
可以想成你要去某棟公司大樓參加研討會:大會的工作人員會給你參加證明,而你要拿著這張證明給警衛確認,就換給你一張當天有效的識別證,而能進到哪幾層樓和用哪些服務也都寫在上面。對照起來是這樣:
| 大會參加者 | OIDC |
|---|---|
| 參加證明 | GitHub 發的身分證明(ID token),寫著「我是哪個 repo、哪個 branch 的 workflow」 |
| 警衛 | AWS STS,專門發短期權限的服務 |
| 警衛認得的證明 | IAM 的身分提供者(Identity provider) |
| 大會參與者名單 | Role 的信任政策(trust policy) |
| 當天有效的識別證 | 短期的 access key,預設 1 小時後失效 |
| 參與者能進哪幾層樓 | Role 的許可政策(permissions policy) |
實際跑起來的流程是這樣:
gha-deploy-role 這個 Role。所以今天在 AWS 上要做兩件事:讓 AWS 認得 GitHub 發的身分證明,以及建一個 Role、寫好名單。至於這個 Role 能做什麼,下一篇寫 deploy.yml 時再來加。
如果專案還沒放上 GitHub,先在 GitHub 建一個新的 repo(建議選 Private,原因在最後的資安小提醒),再把 Day9 的 Next.js 專案 push 上去。
等一下會用到 GitHub 帳號名稱和 repo 名稱,這篇用 my-account 和 my-app 當範例。
先到 IAM 的 Console 新增一個 Identity providers:
https://token.actions.githubusercontent.com。sts.amazonaws.com。
sts.amazonaws.com。PS. 比較舊的教學會要你按 Get thumbprint 取得憑證指紋,現在已經不用了,AWS 會用自己信任的憑證機構清單去驗證 GitHub 的憑證。
一樣在 IAM 的 Console:
token.actions.githubusercontent.com,Audience 選 sts.amazonaws.com。
my-app,GitHub branch 填 main,按 Next。
gha-deploy-role,按 Create role(建立角色)。arn:aws:iam::123456789012:role/gha-deploy-role。*,代表這個帳號底下任何 repo、任何 branch 都能用這個 Role,所以這兩格一定要填。建好之後,點擊你剛剛建的 role 進到 Role 的 Trust relationships(信任關係) Tab,就能看到 Console 幫我們寫好的名單:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:my-account/my-app:ref:refs/heads/main"
}
}
}
]
}
| 欄位 | 意思 |
|---|---|
Federated |
只認在"讓 AWS 認得 GitHub"新增的 GitHub 身分提供者 |
Action |
允許拿身分證明來換這個 Role 的權限 |
aud |
這張證明是要給 AWS STS 的 |
sub |
哪個 repo、哪個 branch 的 workflow 才能使用,也就是名單本身 |
為什麼一定要有 sub?因為全世界的 GitHub repo 拿到的身分證明,都是同一個 GitHub 發的,aud 也都一樣。只看 aud 的話,任何人的 workflow 都能用我們的 Role。所以 IAM 規定,信任 GitHub 的 Role 一定要寫 sub 條件,而且不能只寫 *,不然連建都建不起來。
到 GitHub 上的 repo:
AWS_ROLE_ARN,Value 貼上剛剛複製的 ARN,按 Add variable。
為什麼放 Variables 而不是 Secrets?ARN 只是 Role 的名字,不是密碼,別人就算知道,也過不了名單那一關。放在 Variables 之後還看得到值,要改也方便;Secrets 存進去之後就看不到了。
在專案裡新增 .github/workflows/oidc-test.yml:
name: OIDC test
on:
workflow_dispatch:
permissions:
id-token: write
jobs:
whoami:
runs-on: ubuntu-latest
steps:
- name: Show OIDC subject
run: echo "repo:${GITHUB_REPOSITORY_OWNER}@${GITHUB_REPOSITORY_OWNER_ID}/${GITHUB_REPOSITORY#*/}@${GITHUB_REPOSITORY_ID}:ref:${GITHUB_REF}"
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
aws-region: ap-east-2
- name: Who am I
run: aws sts get-caller-identity
逐段來看:
on: workflow_dispatch:不會自動執行,要到 GitHub 上手動按按鈕才會跑,測試時比較好控制。permissions:id-token: write 是允許 workflow 跟 GitHub 要身分證明,用 OIDC 一定要有這行。只要加了 permissions 之後,沒列出來項目的權限都會變成沒有,因為這個 workflow 不用讀程式碼,所以只寫這一行就好。jobs:workflow 實際要做的工作,可以有好幾個 job,這裡只有一個;whoami 是我們自己取的名字,會顯示在 Actions 的執行畫面上。
aws 指令steps:在這台機器上依序執行的步驟,run 是直接執行指令,uses 是套用別人寫好的 action;接著再用每個步驟的 name 逐一說明
sub,等一下對答案用。${GITHUB_REPOSITORY#*/} 這行是把 my-account/my-app 前面的 my-account/ 去掉,只留下 repo 名稱。aws-region 填自己用的 region,這個系列是台北 ap-east-2。把這個檔案 commit、push 到 main 之後,就可以手動執行了:
main,再按綠色的 Run workflow。如果你的 repo 跟這篇練習一樣是新建的,可能會看到紅燈,Configure AWS credentials 這一步出現:
Could not assume role with OIDC: Not authorized to perform sts:AssumeRoleWithWebIdentity
前面還會有好幾行 Retry AssumeRole: attempt 1 of 12 failed,這個 action 失敗時會自動重試,所以要等一下才會停。Not authorized 的意思是:AWS 收到了身分證明,簽章也沒問題,但上面的 sub 不在這個 Role 的名單上。對照一下兩邊:
| 來源 | sub |
|---|---|
| Role 的名單(Console 產生的) | repo:my-account/my-app:ref:refs/heads/main |
| Show OIDC subject 印出來的 | repo:my-account@1234567/my-app@123456789:ref:refs/heads/main |
差別在帳號名稱和 repo 名稱後面,各多了 @ 加一串數字。
這是 GitHub 在 2026 年 7 月 15 日的改動:這天之後建立的 repo,
sub會在帳號和 repo 名稱後面,加上它們的數字 ID。原因是名稱可以改,舊名稱空出來之後,可能被別人註冊走;如果名單只認名稱,新主人就能用同一個名稱通過檢查。數字 ID 不會換人用,加上去就不怕被冒用。(如果你的 repo 是 7 月 15 日之前建的,第一次就會是綠燈,可以直接跳到下面看 Who am I 的結果。)
修正的方式是把名單改成新格式:
gha-deploy-role,切到 Trust relationships(信任關係) 分頁,按 Edit trust policy(編輯信任政策)。token.actions.githubusercontent.com:sub 那一行的值,換成 Show OIDC subject 印出來的那一串。回到 GitHub 剛剛失敗的那次執行,按 Re-run jobs,選 Re-run all jobs(如果還是失敗,等幾秒讓 IAM 的設定生效再試一次)。這次就會是綠燈了,Who am I 會印出:
{
"UserId": "AROAEXAMPLEROLEID1234:GitHubActions",
"Account": "123456789012",
"Arn": "arn:aws:sts::123456789012:assumed-role/gha-deploy-role/GitHubActions"
}
assumed-role/gha-deploy-role/GitHubActions 的意思是:現在是用 gha-deploy-role 這個 Role 的身分在操作,GitHubActions 是這次使用的名稱,configure-aws-credentials 預設就是這個。
這樣問題就來了:這個 Role 明明還沒有任何權限,為什麼問得出來?
因為 AWS 規定「問自己是誰」不需要任何權限。也就是說,GitHub 現在已經換得到身分了,只是還什麼都不能做。
名單上只寫了 main,那我們換成其他 branch 呢?可以試試看:
test-oidc,建立一個新的 branch。test-oidc。ref:refs/heads/test-oidc,跟名單對不上。test-oidc 刪掉。deploy.yml 也會用這個 Role,等於只有 merge 進 main 的程式碼才能部署。repo:<owner>/<repo>:*,也就是這個 repo 的任何 branch 都可以,後來回頭檢查時才收緊成只限 main。當時雖然沒出事,但「任何 branch」代表任何能 push 的人,開一個新 branch、改一下 workflow,就能拿到部署的權限。名單能寫多窄,就要寫多窄喔!照著做卡住的話,可以先對一下這張表:
| 看到的錯誤 | 原因 |
|---|---|
Not authorized to perform sts:AssumeRoleWithWebIdentity |
sub 對不上:新舊格式、branch、帳號或 repo 名稱打錯(大小寫也要一樣) |
No OpenIDConnect provider found in your account |
在"讓 AWS 認得 GitHub"的身分提供者沒建,或 Provider URL 打錯 |
Credentials could not be loaded |
漏了 id-token: write,或 AWS_ROLE_ARN 沒設定、名稱打錯 |
今天新增的身分提供者和 Role 都不用錢;GitHub Actions 在 private repo 每個月有免費的執行時間,這篇的 workflow 跑一次只用到一兩分鐘。
現在 GitHub Actions 已經換得到 AWS 的身分了,但這個 Role 還什麼都不能做。下一篇就來寫 deploy.yml:push 到 main 之後自動 build、推上 ECR、通知 EC2 換版,用到哪些權限,就幫這個 Role 加哪些ㄅ!
資安小提醒:repo 建議設成 Private,因為 Public repo 的 Actions logs 所有人都看得到。另外,能用 OIDC 的地方,就不要再把 access key 存進 GitHub 喔!