· 10分で読了

Access Keyはもう使うな

# 開発ノート
この記事は中国語から自動翻訳されたものです。翻訳によりニュアンスが失われている場合があります。

この記事では、なぜAWS Access Keyができるだけ早く廃止すべき方法なのか、そしてOIDC + IAM Roleでそれをどう置き換えるかを説明する。

対象範囲は、GitHub ActionsのCI/CD、EC2/ECS上のサービス、そしてローカル開発環境だ。

Access Keyには何の問題があるのか

AWSのAccess Keyは、Access Key IDとSecret Access Keyからなる長期有効なcredentialsだ。

作成したら、手動で削除または無効化しない限り、ずっと有効だ。

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: デバッグ中に環境変数を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を削除すべきか、判断しなければならない

こうしたことは3〜5人のチームでは大した負担に見えないかもしれないが、チームが成長し、プロジェクトが増えるにつれて、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を使い、既存のconsole credentialsやIAM Identity Centerで認証する
  • 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アカウントごとに登録は1回だけでよい。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がこのroleをassumeできる

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の2つだ。

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であり、secretではない。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に書き込んだりするためのもの

この2つの権限は分けて設定し、共用してはいけない。

ローカル開発でもAccess Keyは使わない: aws sso login

CI/CDではOIDCを使っていても、ローカル開発ではまだ~/.aws/credentialsにAccess Keyを置いている人が多い。

ローカルのkeyのリスクはCIとは少し違うが、無視できない。ノートPCの紛失や盗難、ディスクの暗号化漏れ、複数人での開発機共有などで、この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が眠っているなら、今こそ消すべきときだ。