GitHub ActionsのCI実行時に各種AWSサービスにアクセスする為に使用していたIAM Userのクレデンシャル情報を廃止して、OpenID Connect経由でIAM Roleを使用する方式で永続的なクレデンシャル情報を渡さないように変更しました。
今回は、変更手順をTerraformのコードサンプルを交えてまとめてみます。
変更前
これまでは、GitHub ActionsのCI実行環境から各種AWSサービスを利用する際は、非推奨な方法ではあるもののCI実行用に作成したIAM Userのクレデンシャル情報を使用してアクセスしていました。
AWSサービスのアクセス制御はIAMロールの一時認証情報を利用する方法が推奨されていますが、AWS環境外からアクセスする場合は実行環境にIAMロールを参照する為のインスタンスプロファイルが設定できない為です。
【変更前: GitHub Actionsから各種AWSサービスにアクセス】
何が課題だったか
この方式でもGitHub ActionsのCI実行環境から各種AWSサービスへのアクセスは可能でしたが、主に以下二つの問題を抱えている状態でした。
1. クレデンシャル情報漏洩時のセキュリティリスクが大きい
IAM Userのクレデンシャル情報はアクセスキーとシークレットアクセスキーの二つの文字列である事に加えて、開発者が破棄して新しいクレデンシャル情報にローテーションしない限り、文字列は有効であり続けます。
これはIAM Userの性質上仕方のない事ではありますが、万が一クレデンシャル情報の文字列が外部に漏洩してしまった場合のリスクが非常に高い為、AWSでも非推奨とされています。
2. キーローテーション時のオペレーションコストがかかる
上記リスク軽減の為に数ヶ月毎の頻度でクレデンシャル情報のキーローテを行う事になりますが、この作業は手動運用でしか対応できず毎回対応するのは面倒です。
IAM Userそれぞれに対して、
- クレデンシャル情報の新規発行
- 発行したクレデンシャル情報で各種CI環境などに登録されている情報を更新
- 更新後の情報で正しくアクセスできるかの動作確認
- 不要になった古いクレデンシャル情報を削除
をユーザ数分対応する必要がある為、結構なオペレーションコストがかかります。
どう解消したか
仕方ないとは分かりつつも何とかならないかなぁと思っていたところ、IAM Userのクレデンシャル情報を使わないでもGitHub ActionsからAWSサービスにアクセスできるようになった!という嬉しいニュースが昨年秋に飛び込んできました。
この時点ではまだ非公式な状態でしたが、それから約一ヶ月後にGitHub公式から正式にサポートがアナウンスされました。
今回サポートされたOpenID Connectの認証方法は、
- 信頼関係を登録したGitHubリポジトリ用のIAM Roleを用意
- OIDCプロバイダで認証トークンを発行してAWSサービスへのアクセスを許可する一時アクセストークンを要求
- 信頼関係を登録済のリポジトリから認証トークン付きでリクエストがあった場合に、AWSサービスへのアクセスを許可する一時トークンを発行
- 一時トークンを使用して許可されたサービスにアクセス
という流れです。
【OpenID Connectを利用した認証(公式から引用)】
クラウドプロバイダ側で信頼元を登録して一時トークンを発行する形に変更されたことでIAM Userが不要になり、クレデンシャル情報の漏洩リスクやキーローテーションを行うオペレーションコストの課題が解決されていますね。
変更手順
という事で、新しい認証方法へ変更していきます。
0. 変更前の状態を再現
変更の流れを書いていく上での、変更前の状態を再現する下準備です。
対象リポジトリとアクセスしたいAWSリソースが既に存在する場合は、この手順はSKIPしてください。
- 対象GitHubリポジトリ: MasakiMisawa/ci-cd-test
- アクセス先AWSサービス: S3/masakimisawa-ci-cd-test
- GitHub Actionsで実行するワークフロー: test-workflow
今回の流れ説明では、ci-cd-testというリポジトリを使用して進めます。
このリポジトリのGitHub Secretsに、別途用意するAWSリソースへのアクセスが許可されたIAM UserのAWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEYの二つが登録されています。
CI環境からアクセスする先のAWSリソースは、S3のmasakimisawa-ci-cd-testというバケットを用意しました。
もちろん、プライベートバケットなのでアクセスが許可されたIAMポリシーがアタッチされた状態でないとアクセスが拒否されます。
CI実行用という事で、今回はdevelopブランチへのPull Request作成時にトリガーされるtest-workflowというワークフローを実行させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
name: test-workflow on: pull_request: branches: - develop jobs: TestS3Upload: name: S3 upload runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 - name: "Configure AWS credentials" uses: aws-actions/configure-aws-credentials@v1 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: ap-northeast-1 - name: upload run: aws s3 cp ./README.md s3://masakimisawa-ci-cd-test/${{ github.head_ref }}/ |
実開発時はCI環境のビルド結果成果物をS3にアップロードするなどもう少し長い処理になると思いますが、今回は作成したS3バケットへのアクセスが確認できれば十分だったので、リポジトリルートのREADME.mdファイルをPR作成元ブランチのパスにアップロードするだけの簡単な処理にしています。
今回は記事の説明用ということで分かり易くする目的でS3バケット名を通常文字列にしていますが、実開発用リポジトリなどではGitHub Secretsの環境変数に置き換えてください。
CI環境の環境変数でaws-access-key-idとaws-secret-access-keyを使用する為に、GitHub SecretsにIAM Userのクレデンシャル情報を登録していますね。
この情報を削除した上でAWSサービスにアクセス可能にするのが今回の目的です。
1. OpenID Connect Providerの作成
準備ができたところで、OpenID Connect Providerから作成していきます。
1 2 3 4 5 |
resource "aws_iam_openid_connect_provider" "github_oidc" { url = "https://token.actions.githubusercontent.com" client_id_list = ["sts.amazonaws.com"] thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"] } |
OpenID Connect Providerのリソース作成はこれで終わりです。
サムプリントは、2022年1月にGitHub ActionsのID プロパイダのルート証明書が変更された影響で、それ以前に使用されていたサムプリントを使用しているとAWSの認証が通らなくなっているようなのでご注意ください。
サムプリントに「a031c46782e6e6c662c2c87c76da9aa62ccabd8e」の更新前の値が記載された情報が多く出回っているので、AWSの認証が通らない場合は最新の値に更新してみてください。
また、2022年2月現在では記載した値がGitHub ActionsのID プロバイダのルート証明書の最新の値になっていますが、中間証明書の有効期限的に今後も定期的にサムプリントの値は更新されていく事が予想される為、認証が通らなくなった場合は公式ドキュメントを参考にサムプリントの値を取得して更新してください。
最新のサムプリントの値は動的に取得する事も可能なので、cronトリガーの日時実行処理などで最新の値を取得して更新していくと運用が楽になりそうですね。
1 2 |
openssl s_client -servername token.actions.githubusercontent.com -showcerts -connect token.actions.githubusercontent.com:443 < /dev/null 2>/dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' | sed "0,/-END CERTIFICATE-/d" > certificate.crt openssl x509 -in certificate.crt -fingerprint -noout | cut -f2 -d'=' | tr -d ':' | tr '[:upper:]' '[:lower:]' |
尚、今回は2022/02時点での最新の値を固定文字列で登録する形で進めます。
2. IAM Roleの作成
続いて、信頼関係を登録してAWSサービスへのアクセスの一時トークンを発行するIAM Roleを作成していきます。
作成するリソース量が少しだけ増えますが、IAM Roleを作成するコード例です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
data "aws_iam_policy_document" "principal_policy" { statement { effect = "Allow" actions = ["sts:AssumeRoleWithWebIdentity"] principals { type = "Federated" identifiers = [aws_iam_openid_connect_provider.github_oidc.arn] } condition { test = "StringLike" variable = "token.actions.githubusercontent.com:sub" values = ["repo:MasakiMisawa/ci-cd-test:*"] } } } resource "aws_iam_role" "oidc_test_iam_role" { name = "GitHubActionsOIDCTestRole" assume_role_policy = data.aws_iam_policy_document.principal_policy.json } data "aws_iam_policy_document" "allowed_action_policy" { statement { effect = "Allow" actions = [ "s3:ListBucket", "s3:GetObject", "s3:PutObject" ] resources = [ "arn:aws:s3:::masakimisawa-ci-cd-test", "arn:aws:s3:::masakimisawa-ci-cd-test/*" ] } } resource "aws_iam_policy" "oidc_test_iam_policy" { name = "GitHubActionsOIDCTestPolicy" policy = data.aws_iam_policy_document.allowed_action_policy.json } resource "aws_iam_role_policy_attachment" "oidc_test_iam_role_policy_attachment" { role = aws_iam_role.oidc_test_iam_role.name policy_arn = aws_iam_policy.oidc_test_iam_policy.arn } |
IAM Roleにアタッチするプリンシパルのポリシーで、conditionの条件で信頼関係を登録するリポジトリを絞った状態でGitHub ActionsのID プロバイダに対してSTS.AssumeRoleWithWebIdentityの実行用の一時トークン発行を許可します。
今回はci-cd-testリポジトリの全ブランチに一時トークンの発行を許可しましたが、デプロイ時はmainブランチのみを許可などリポジトリ名の後の「:*」の部分で条件を指定すれば、更に細かく設定することも可能です。
一時トークンの中身は、作成したS3のmasakimisawa-ci-cd-testバケットに対してcpコマンドを実行する為の最小権限を与えています。
3. GitHub Actions Workflowの修正
AWS側の準備が整ったので、最後にGitHub Actions側で実行するCIのワークフロー定義を変更します。
IAM Userのクレデンシャル情報を環境変数に使用していた部分を、IAM RoleのロールArnを指定する形に変更する作業です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
name: test-workflow on: pull_request: branches: - develop permissions: id-token: write contents: read jobs: TestS3Upload: name: S3 upload runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 - name: "Configure AWS credentials" uses: aws-actions/configure-aws-credentials@master with: role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/GitHubActionsOIDCTestRole aws-region: ap-northeast-1 - name: upload run: aws s3 cp ./README.md s3://masakimisawa-ci-cd-test/${{ github.head_ref }}/ |
secretsから取得しているAWS_ACCOUNT_IDの部分は、自身のAWSアカウントIDに置き換えてください。
変更点は以下になります。
- Configure AWS credentials欄からクレデンシャル情報を削除
- Configure AWS credentials欄にrole-to-assume情報を追加
- permissions欄を追加
- aws-actions/configure-aws-credentialsをv1からmasterに変更
今回やりたかった事で、AWS_ACCESS_LEY_IDとAWS_SECRET_ACCESS_KEYの二つを削除しています。
先程作成したGitHubActionsOIDCTestRoleのIAM Role ARNです。
アカウントIDだけSecretsに登録するも良し、ARN毎登録するも良しですね。
一時トークン引き受け用に、IDトークンの書き込み権限が必要になります。
また、permissionsを明示的に書くとデフォルトで有効な権限(今回はactionsのcheckoutに必要な contents: read)も無効になってしまうようなので、必要な権限を明示的に追記します。
自分はこれが抜けていて結構ハマってしまいました。。。
4. 動作確認
AWS側とGitHub Actions側両方の作業が完了した為、正常にアクセス可能になっているかを確認します。
OpenID Connectを信頼元に登録したIAM Roleの一時権限を使用してアクセスが無事に成功しています。
5. お片づけ
これでIAM Userのクレデンシャル情報は不要になったので、これまでの感謝の気持ちを込めつつお別れします。
まずは、GitHubリポジトリに登録してある二つのSecretsのキーを削除。
最後に、AWS側でIAM Userごと削除。
無事にクレデンシャル情報を消せましたという事で、今回は終わりです。
ここまで読んでいただきありがとうございました!