Terraform用のAWSアクセスキーの権限管理を、スイッチロール形式で最適化する方法
はじめに
西藤です。
皆さんはTerraformを使ったAWSインフラの実装をする際のアクセスキーの取り扱いはどのようにしていますか?
- IAMユーザーを発行して、そのユーザーに紐づくアクセスキーを生成し、それを使ってTerraformのコマンドを実行する
という形が多いのではないでしょうか。
もちろん業務ユースであれば、IAMに付与している権限は必要最低限のものに絞って、アクセスキー自体も関係者のみに配布するでしょうし、配布したアクセスキーも一定期間経過したら、アクセスキー自体のローテーションを運用に入れることで、不正使用のリスクをヘッジできます。
・・・それが、例えば個人使用の時はどうですか?
業務利用じゃないし、Terraformで新しいリソースを作るたびに権限不足に引っ掛かるのがめんどくさいので、Full権限を与えたアクセスキーでじゃんじゃん検証・・・してませんか?
もちろん個人の検証を機動的に行いたいときに、細やかな権限設定に苦労していては本末転倒です。
しかし、アクセスキーが何らかの方法で流出して好き勝手にAWSを使われるのも怖い。そういった不安も解消しておきたいもの。
そこで、今回はTerraformのコマンド実行をスイッチロールを用いた形に差し替えることで、少しでも安心を得る方法を紹介します。
目指す体制としましては、
- アクセスキー自体にはスイッチロール(Assume Role)するためだけの最低限の権限のみ。(アクセスキー単体ではAssume Role以外は何もできない)
- Terraformのインフラ実装自体はスイッチロール後のIAMロールが持つ権限で行う
- スイッチロール自体の実行条件を絞り込む
という形にしていきます。
前提
改善に向けてまずは、変更前のTerraform実行構成から確認していきます。
コードリポジトリの中が
.
├── .env
├── Dockerfile
├── resources.tf
├── docker-compose.yml
└── main.tf
のような構成だとします。(.gitignore
ファイルや .terraform/
ディレクトリなどプロジェクトを動かす上で増えてくるファイルの一部は割愛)
Terraformの実行バージョンや、AWS providerの設定を main.tf
にて
provider "aws" {
region = "ap-northeast-1"
}
terraform {
required_version = "0.14.5"
backend "s3" {
bucket = "<tfstateファイルをおくS3バケット名>"
key = "<tfstateファイル名>"
region = "ap-northeast-1"
}
required_providers {
aws = {
source = "hashicorp/aws"
version = "3.25.0"
}
}
}
のように記述。resources.tf
ファイルに各種AWSリソースの設定を加えていく形です。
.env ファイルには使用するアクセスキーの情報を
AWS_ACCESS_KEY_ID=XXXXXXXXXXXXXXXXXXX
AWS_SECRET_ACCESS_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
AWS_DEFAULT_REGION=ap-northeast-1
のように記載し、docker-compose.ymlは
version: "3"
services:
terraform:
build: .
volumes:
- .:/app/
env_file: .env
と指定することで、
docker-compose run terraform apply
のようにコマンド実行できます。
このようにして、実行するPC内のAWSのprofileを汚さずに、アクセスキー情報もプロジェクトのコードリポジトリ内に集約されるので取り扱いはしやすい構成としておりました。
しかし、.env
ファイルの中身はアクセスキー情報そのものです。当然、自分しか触れることができないようにするはずですが、この情報が不正に取得されたとき、アクセスキーの権限を使って自由にAWSを利用されてしまいます。
今回はこの envファイルの中身が仮に不正に取得されても、即座にAWSの不正利用はされないようにする改善を加えていきます。
スイッチ用ロールの作成
改善のためにはまず、アクセスキーに紐づいていたIAMユーザーからスイッチロールする「スイッチ先のロール」(assume roleされるロール)を作ります。
IAMロールの作成画面で「別のAWSアカウント」を選択して、自身のAWSアカウントのIDを入力します。
(自分のアカウントを指定する場合も画面は「別のAWSアカウント」…というのが少しややこしいですが。。。)
入力したら、ウィザードに沿って権限を付与する画面です。
いかなる構成でも強すぎる権限付与は望ましいものではなく、最小権限から徐々に拡大していくのが鉄則ですが、今回の改善では、まずはスイッチロールがちゃんと作用することがわかるまでは、従来アクセスキーに紐づいていたIAMユーザーについていた権限を付与するといいでしょう。
タグの設定や、作成するロール名も指定して作成完了です。
今回は、PowerUserAccess
の権限を持ったTerraformRole
というIAMロールを作ります。
スイッチ用ロールの「信頼関係」の設定
作成されたTerraformRole
を見てみましょう。
現時点のままでは、自分のAWSアカウントからスイッチロールができるIAMロールができただけです。
以下の図の「信頼されたエンティティ」の記載を見ると、自分のAWSアカウントのIDが表示されています。
「信頼関係の編集」を開くと分かるのですが具体的には
arn:aws:iam::<自分のAWSアカウントID>:root
からのsts:AssumeRole
を許可する構成になっております。
それを今回の用途に合わせて厳密に設定しなおします。
「信頼関係の編集」を開いてポリシードキュメントを次のようにします。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::<自分のAWSアカウントID>:user/<スイッチロール元のIAMユーザー名>"
},
"Action": "sts:AssumeRole"
}
]
}
Princial
部分での指定しているIAMユーザーは、従来より使っていたアクセスキーのIAMユーザーです。このように指定することで、このロールにスイッチロール(AssumeRole)が行えるようになります。
これにより、指定のユーザー名のIAMユーザーのみがこのロールにスイッチできるように制限されます。
さらに
"Condition": {
"IpAddress": {
"aws:SourceIp": "xx.xx.xx.xx"
}
}
というようなIpAddress
を使ったCondition節を加えれば、アクセス元のIPアドレスも制限でき、最終的に「指定のIAMユーザーかつ、指定のIPアドレスからのアクセスの時のみ、スイッチロールできる」という設定になります。
IAMユーザーの権限設定
スイッチ先のロールの設定は終わりましたので、従来より使っていたアクセスキーのIAMユーザーの設定を調整しましょう。
従来はこのIAMユーザーにTerraformで各種リソースを作成するための権限がついていたと思いますが、ついていた権限は全て外しましょう。
そして、次のようなポリシーのみを適用します。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "1",
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::<自分のAWSアカウントID>:role/<スイッチロール先のIAMロール名>"
}
]
}
次のような具合です。
アクセスキーのIAMユーザーにはAssumeRoleを行う権限しかついてないことがわかります。
以上でIAMの設定は完了です。
Terraform側の設定
使用するIAMユーザー、IAMロールの設定ができたら最後は、Terraformでそれを使用する方法です。
具体的には
- AWSの“Provider”設定でロールを指定
- Terraformの”Backend”設定でロールを指定
の2つです。
再掲となりますが、
provider "aws" {
region = "ap-northeast-1"
}
terraform {
required_version = "0.14.5"
backend "s3" {
bucket = "<tfstateファイルをおくS3バケット名>"
key = "<tfstateファイル名>"
region = "ap-northeast-1"
}
required_providers {
aws = {
source = "hashicorp/aws"
version = "3.25.0"
}
}
}
のように指定していましたが、これに変更を加えて。
provider "aws" {
region = "ap-northeast-1"
# assume_roleブロックを追加
assume_role {
role_arn = "arn:aws:iam::<自分のAWSアカウントID>:role/<スイッチロール先のIAMロール名>"
}
}
terraform {
required_version = "0.14.5"
backend "s3" {
bucket = "<tfstateファイルをおくS3バケット名>"
key = "<tfstateファイル名>"
region = "ap-northeast-1"
# role_arn オプションを追加
role_arn = "arn:aws:iam::<自分のAWSアカウントID>:role/<スイッチロール先のIAMロール名>"
}
required_providers {
aws = {
source = "hashicorp/aws"
version = "3.25.0"
}
}
}
のように
provider "aws"
のブロックの中でassume_role
のブロックを作ってロールを指定terraform
のブロックの中で、backend
のブロック内でrole_arn
の指定
を行います。
これによって、Terraformのコマンド実行の際にはスイッチロールした上でコマンド実行されるようになります。
(この辺りは設定の指定方法の誤りがあっても、terraform実行時のエラーでは誤り箇所を具体的には指摘してくれず結構つまづきました。。。指定の記述方法にはご注意ください。)
実行結果
以上が問題なく設定ができていれば、terraform plan
コマンドをしても、以前と変わらず
No changes. Infrastructure is up-to-date.
This means that Terraform did not detect any differences between your
configuration and real physical resources that exist. As a result, no
actions need to be performed.
のように表示されるはずです。これで今後はスイッチロールの方式で、terraformが実行できることがわかります。
もし、上記のIAMユーザーとIAMロールの「信頼関係」設定と一致しない条件で実行した場合、
Error: error configuring S3 Backend: IAM Role (arn:aws:iam::111111111111:role/TerraformRole) cannot be assumed.
There are a number of possible causes of this - the most common are:
* The credentials used in order to assume the role are invalid
* The credentials do not have appropriate permission to assume the role
* The role ARN is not valid
Error: NoCredentialProviders: no valid providers in chain. Deprecated.
For verbose messaging see aws.Config.CredentialsChainVerboseErrors
のように表示され、スイッチロール(Assume Role)ができなかった旨表示されることがわかります。
まとめ
以上がTerraform実行においてスイッチロールを用いた形に変える方法です。
これにより、本件での.env
ファイル内に記載されていたAWSアクセスキーは、それ単体だけではスイッチロールする権限しか持たず、AWSリソースへの操作を何も行えないようになります。
また、スイッチロールもスイッチ先のAWSアカウントID、ロール名が特定できないと実行できず、さらには今回はIPアドレスでも制限をつけたので、自分が普段検証を行う環境のIPアドレスだけを指定すれば、スイッチ先を特定されてもブロックされます。
※AWSアクセスキー自体を定期的に無効化して、そもそも使えなくする仕組みについては、私の以前の記事をご参照ください。
AWSアクセスキーを自動的に無効化する方法
もし、個人の検証ではなく関係者の人数や拠点数が多い場合、IAMの各設定は手間になってしまう可能性もありますが、本記事によりTerraform実行体制を少しでもよりセキュアにするためのアイディアの一つとして、参考となれば幸いです。