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 Key | OIDC + 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の値はもっと厳密に制限できる。たとえばmainbranchだけ許可するなら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に追加するのはpermissionsとaws-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": [...]
}
taskRoleArnとexecutionRoleArnは別物だという点に注意が必要だ。
- Task Role(
taskRoleArn): アプリケーションがruntimeでAWSリソースにアクセスするためのもの(たとえばS3の読み取り、DynamoDBへの書き込み) - Execution Role(
executionRoleArn): 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が眠っているなら、今こそ消すべきときだ。