· 8 分鐘閱讀

別再用 AWS Access Key 了

# 開發筆記

這篇文章會說明為什麼 AWS Access Key 是一個你應該儘早淘汰的做法,以及如何用 OIDC + IAM Role 取代它。

範圍涵蓋 GitHub Actions 的 CI/CD、EC2/ECS 上的服務,以及本地開發環境。

Access Key 有什麼問題

AWS 的 Access Key 是一組長期有效的 credentials,由 Access Key ID 和 Secret Access Key 組成。

建立之後除非手動刪除或停用,否則永遠有效。

用 AWS 後台建立 Access Key 時,也會發現 AWS 會盡可能阻止你建立 Access Key。

Access Key 一旦洩漏,攻擊者可以在你發現之前就用這組憑證做任何事情。像是開 EC2 挖礦、下載 S3 裡的客戶資料、刪除 RDS snapshot。

AWS 自己在 IAM Best Practices 裡就直接寫了:

Where possible, we recommend relying on temporary credentials instead of creating long-term credentials such as access keys.

洩漏比你想像中容易

Access Key 的洩漏途徑很多:

  • Git 歷史紀錄:不小心 commit 進去的 .env 或設定檔,就算之後刪掉,git log 裡還是看得到
  • CI/CD log:debug 時 echo 出環境變數,或是某個 library 在 error stack trace 裡印出了完整的環境變數
  • 員工離職:如果 key 是綁在某個 IAM User 上,離職時忘記 revoke 就是一個風險
  • 共用帳號:團隊共用同一組 key,出事的時候連是誰做的都查不到

GitHub 有 secret scanning 功能,AWS 也有 aws:SecretAccessKey 的自動偵測。但這些都是事後補救,並非根本解法。

管理成本

就算沒有洩漏,維護 Access Key 本身就有成本:

  • 定期輪替:AWS 建議至少 90 天輪替一次。每次輪替要更新所有用到這組 key 的地方,包括 GitHub Secrets、其他 CI 系統、本地開發環境
  • 權限稽核:要定期檢查這組 key 綁定的 IAM Policy 是否過度授權
  • 追蹤使用狀況:誰在用?哪些 workflow 在用?還有沒有在用?key 建了半年沒人動過,要不要刪掉?

這些事情在三五人的團隊看起來不痛不癢,但隨著團隊成長、專案變多,Access Key 的管理會逐漸變成持續的負擔。「反正沒那麼容易發生啦」的心態,會在某天反咬你一口。

用 OIDC + IAM Role 取代 Access Key

GitHub Actions 透過 OIDC(OpenID Connect)向 AWS 證明自己的身份。AWS 驗證後發放一組幾分鐘就過期的臨時憑證。整個過程不需要存放任何 secret。

本文以 GitHub Actions 為例,GitLab CI、CircleCI 等平台也有類似的 OIDC 整合機制。

OIDC 的運作方式

OIDC 是一個基於 JWT 的身份驗證協議。在 GitHub Actions 使用流程如下:

GitHub Actions Runner

    │ 1. 向 GitHub 的 OIDC Provider 請求 JWT token
    │    (token 裡包含 repo 名稱、branch、workflow 等資訊)


AWS STS (Security Token Service,負責發放臨時憑證的服務)

    │ 2. 驗證 JWT token 的簽章(透過 GitHub 的公開金鑰)
    │ 3. 檢查 token 的 claims 是否符合 IAM Role 的信任條件
    │ 4. 回傳臨時憑證(Access Key + Secret Key + Session Token)


GitHub Actions Runner

    │ 5. 用臨時憑證執行 AWS 操作
    │    (憑證在指定時間後自動過期)


  完成,憑證失效

每次 workflow 執行都會拿到一組新的 token,時效到了就自動失效。不需要輪替,也不可能從 Git 歷史裡挖出來重複利用。

另一個重要差異在於信任模型。

Access Key 的安全性完全依賴 secret 本身不被洩漏,一旦洩漏就無法區分合法與非法使用。

OIDC 用 JWT 簽章驗證身份,AWS 可以透過 GitHub 的 JWKS endpoint 獨立驗證 token 的真實性,不需要共享任何 secret。

跟 Access Key 的差異

Access KeyOIDC + IAM Role
憑證壽命永久有效幾分鐘到幾小時
洩漏風險洩漏後可無限使用過期即失效
Secret 管理要存在 GitHub Secrets不需要任何 secret
輪替手動,至少 90 天一次自動,每次執行都是新的
稽核只能追蹤到 IAM User可以追蹤到具體的 repo 和 workflow
離職處理要記得 revoke不需要,因為根本沒有長期憑證

在資安風險和管理成本上,IAM Role 都是明顯更好的選擇。AWS 的介面當中,也有提示不同的狀況應該如何處理:

  • CLI:使用 AWS CLI v2 或使用 CloudShell。建議大家使用 aws sso login,避免存放永久憑證在本地端
  • Local Code:使用 IDE 整合 AWS Toolkit,通過現有的控制台憑證或 IAM 身份中心進行身份驗證
  • 在 AWS 上的運算資源:使用 IAM Role
  • 第三方服務:使用臨時憑證取代長期憑證
  • 非跑在 AWS 的應用:使用 IAM Role Anywhere 建立臨時憑證
    • IAM Role Anywhere 需要搭配認證機構(AWS Private CA 等),對小團隊來說設定和維護 PKI 的成本不低,未必比管理 Access Key 輕鬆

設定步驟

1. 在 AWS 註冊 OIDC Provider

到 IAM Console > Identity providers > Add provider:

  • Provider type: OpenID Connect
  • Provider URL: https://token.actions.githubusercontent.com
  • Audience: sts.amazonaws.com

或是用 AWS CLI:

aws iam create-open-id-connect-provider \
  --url https://token.actions.githubusercontent.com \
  --client-id-list sts.amazonaws.com

一個 AWS 帳號只需要註冊一次,不管有多少個 repo 要用都是同一個 provider。

2. 建立 IAM Role

建立一個 IAM Role,Trust Policy 設定為信任 GitHub OIDC Provider:

{
  "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"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:YOUR_ORG/YOUR_REPO:*"
        }
      }
    }
  ]
}

幾個要注意的地方:

  • ACCOUNT_ID 換成你的 AWS 帳號 ID
  • YOUR_ORG/YOUR_REPO 換成你的 GitHub repo 全名
  • sub 的值可以更精確地限制,例如只允許 main branch:repo:org/repo:ref:refs/heads/main
  • 如果要允許多個 repo,可以用 wildcard:repo:org/*:*,但這樣任何在該 org 下的 repo 都能 assume 這個 role

Permission Policy 就根據你的需求設定。以部署 ECS 為例,需要以下權限:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ecs:UpdateService",
        "ecs:DescribeServices",
        "ecs:DescribeTaskDefinition",
        "ecs:RegisterTaskDefinition",
        "ecr:GetAuthorizationToken",
        "ecr:BatchCheckLayerAvailability",
        "ecr:GetDownloadUrlForLayer",
        "ecr:BatchGetImage",
        "ecr:PutImage",
        "ecr:InitiateLayerUpload",
        "ecr:UploadLayerPart",
        "ecr:CompleteLayerUpload",
        "iam:PassRole"
      ],
      "Resource": "*"
    }
  ]
}

實務上 Resource 不該用 *,應該限制到具體的 ECS Service 和 ECR Repository ARN。上面只是為了展示需要哪些 Action。

3. 設定 GitHub Actions

在 workflow 裡加上兩個東西:permissionsaws-actions/configure-aws-credentials

name: Deploy

on:
  push:
    branches: [main]

permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-actions-deploy
          aws-region: ap-northeast-1
          role-duration-seconds: 900

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Build and push image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t $ECR_REGISTRY/my-app:$IMAGE_TAG .
          docker push $ECR_REGISTRY/my-app:$IMAGE_TAG

      - name: Deploy to ECS
        run: |
          aws ecs update-service \
            --cluster my-cluster \
            --service my-service \
            --force-new-deployment

幾個容易忘記的地方:

permissions 一定要設定。 id-token: write 讓 runner 能向 GitHub OIDC Provider 請求 token。沒有這行,runner 就拿不到 token,會直接失敗。而且一旦你設定了 permissions,沒列出來的權限會被設為 none,所以 contents: read 也要一起加上去,不然 actions/checkout 會沒有權限讀 repo。

role-duration-seconds 預設是 3600 秒(1 小時)。 如果你的 workflow 跑不了那麼久,設短一點。憑證的有效時間越短越安全。

role-to-assume 不需要存在 GitHub Secrets 裡。 這個值是 IAM Role 的 ARN,不是敏感資訊,直接寫在 workflow 檔案裡也可以。

不只 GitHub Actions:EC2 和 ECS 也一樣

同樣的原則適用於所有跑在 AWS 上的 workload。

EC2 上的應用程式不該用 Access Key,應該用 Instance Profile。Instance Profile 就是綁在 EC2 instance 上的 IAM Role,instance 裡的 AWS SDK 會自動透過 metadata service 取得臨時憑證。

ECS 的 Task 也不該用 Access Key,應該用 Task Role。在 Task Definition 裡指定 taskRoleArn,container 裡的 AWS SDK 就會自動拿到對應的臨時憑證。

{
  "family": "my-app",
  "taskRoleArn": "arn:aws:iam::ACCOUNT_ID:role/my-app-task-role",
  "executionRoleArn": "arn:aws:iam::ACCOUNT_ID:role/ecsTaskExecutionRole",
  "containerDefinitions": [...]
}

注意 taskRoleArnexecutionRoleArn 是不同的東西:

  • Task RoletaskRoleArn):你的應用程式在 runtime 存取 AWS 資源用的(例如讀 S3、寫 DynamoDB)
  • Execution RoleexecutionRoleArn):ECS agent 用來拉 container image、寫 CloudWatch Logs 的

兩者的權限應該分開設定,不要共用。

本地開發也別用 Access Key:aws sso login

很多人在 CI/CD 用了 OIDC,本地開發卻還是在 ~/.aws/credentials 裡放了一組 Access Key。

本地 key 的風險跟 CI 不太一樣,但也不能忽略:筆電遺失或被偷、磁碟未加密、多人共用開發機,都可能讓這組 key 落入他人手中。

AWS CLI 支援透過 IAM Identity Center(以前叫 AWS SSO)登入,流程跟用 Google OAuth 登入網站一樣:

aws configure sso

設定完之後,每次要用的時候:

aws sso login --profile my-profile

瀏覽器會跳出來讓你完成驗證,驗證完 CLI 就能用了。拿到的一樣是臨時憑證,過期就重新登入,不需要在本地存任何長期的 key。

~/.aws/config 大概長這樣:

[profile my-profile]
sso_session = my-sso
sso_account_id = 123456789012
sso_role_name = AdministratorAccess
region = ap-northeast-1

[sso-session my-sso]
sso_start_url = https://your-org.awsapps.com/start
sso_region = ap-northeast-1
sso_registration_scopes = sso:account:access

如果團隊還沒有啟用 IAM Identity Center,這需要在 AWS Organizations 層級開啟。設定的過程不複雜,但需要有 Organization 的管理權限。

一旦設定好,團隊成員就可以用公司的 IdP(Google Workspace、Okta、Azure AD)直接登入 AWS,不需要另外建 IAM User。

總結

不要用 Access Key。

不管是 CI/CD、EC2/ECS 上跑的服務、還是本地開發,都有對應的替代方案:

  • GitHub Actions → OIDC + IAM Role
  • EC2 → Instance Profile
  • ECS → Task Role
  • 本地開發aws sso login

初始設定完成後,日常維護的負擔遠低於管理 Access Key。Trust Policy 和 Permission Policy 仍然需要隨著專案演進調整,但至少不用再擔心憑證洩漏和輪替的問題。

如果你的 GitHub Actions 裡還有 AWS_ACCESS_KEY_ID,或是 ~/.aws/credentials 裡還躺著一組 key,現在是把它們清掉的好時機。