AWSのマルチアカウント管理におけるIAMマネジメントで試行錯誤した話

Takuya Onda
Eureka Engineering
Published in
20 min readDec 10, 2019

--

この記事は 10日目の記事です。

この記事は eureka Advent Calendar 2019 10日目の記事です。
9日目は 弊社iOSエンジニアのAymenによる

でした。

こんにちは。エウレカでSREをしている恩田と申します。

好きなAWSサービスはVPCとRDS(Aurora)、好きな仕事は既存リソースのTerraform Importによるバージョン管理下移行作業 (社内通称IT土木) とTerraformのアップグレードです。(この前ようやくv0.12対応が終わり歓喜)。家庭では生後5ヶ月になった娘と戯れたりしながら日々仕事に精を出す毎日を送っています。

この記事では、エウレカで2019年度に実施したAWSのマルチアカウント環境におけるIAMマネジメントの最適化の試行錯誤の様子をお伝えします。

解決したかった課題

弊社が運営するオンラインデーティングサービス「Pairs」アプリケーションは主にAWS上でホスティングされています。

ログ基盤や一部マイクロサービスなどはGCPで構築していますが、メインで利用しているのはAWSです。エウレカではAWS上でPairsを始めとした多種多様なアプリケーションやサービスを運用しています。

そんなこんなで弊社でAWSを使いはじめて早8年。弊社の管理する複数のAWSアカウント上には月日と共に用途や処遇もわからないIAMユーザが無秩序に作成され続けてきました。

特に、開発者への権限委譲という名(言い訳)の元、マネジメントコンソールへログインしたいメンバーへ一貫性のないポリシーと共に作成され続けてきたIAM User(MFA等最低限はやっていたが)、および各開発者からさらに無秩序に作成されたIAM User、そして各IAM Userから払い出されたクレデンシャル群、、 。まさに腐臭の漂う状態でした。

目指すべきゴール

あらためて課題を整理すると、IAM Userの無秩序な生成が問題の根源だという事がわかりました。セキュリティリスクは勿論のこと、入退社対応の煩雑や「これどうすればいいですかね?」的な無駄なコミュニケーションの発生、およびそれに伴う脳みそコストも馬鹿にできません。

というわけで今年の春頃から、以下を具体的なゴールイメージとしてコツコツ改善をはじめました。

  • アイデンティティプロバイダ(IdP)とAWSアカウントを連携し、フェデレーションログインによるSSOでのみコンソールログイン可能にする
  • フェデレーションログインで用いるプリセットのIAM Role群とポリシー、および外部SaaS等用で利用する最低限のIAM UserをTerraformでバージョン管理。また、入退社対応や所属Roleの変更はIdP上で一元管理する
  • コンソールアクセス可能なIAM Userの完全撤廃。また上記プリセットRoleの権限変更や新規IAM User作成時は特定人によるコードレビューが必須となる仕組みを構築する。
  • 開発者が自分自身で自身に割り当てられたRoleに応じたユニークなAWS STS(一時的な認証情報)を払い出せるようにする。
  • これらをエウレカが保有する全AWSアカウントで実現する & 今後作成するアカウントでも同様の仕組みを省コストで実現できるコードベースをTerraformで整備する。

IdP(Okta)とAWSの連携

弊社ではユーザIDを管理するアイデンティティープロバイダー(IdP)としてOktaを昨年度より元々採用していました。そこで、AWSマネジメントコンソールへのログインをOkta経由でのフェデレーションログインに統一しました。

エウレカではTerraformで各種AWSリソースを管理してるので、以下Terraformのレシピでお送りします。

まず、Okta側でAWSのIAM Role一覧を取得するためのAWS Access / Secret Keyを払い出すためのIAM Userを作成します。

resource "aws_iam_user" "okta" {
name = "okta"
path = "/"
}
resource "aws_iam_policy" "okta" {
name = "OktaMasterAccountPolicy"
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"iam:ListRoles",
"iam:ListAccountAliases"
],
"Resource": "*"
}
]
}
EOF
}
resource "aws_iam_user_policy_attachment" "okta" {
depends_on = [
aws_iam_user.okta,
aws_iam_policy.okta,
]
user = aws_iam_user.okta.name
policy_arn = aws_iam_policy.okta.arn
}

次に、フェデレーションログイン時に開発者に選択させるRoleの定義を作成します。イメージとして、API Developer、Security Developer、View Billingといった具合に職責に応じたIAM Roleとそれぞれに必要なパーミッションをPolicyとして定義します。

今回は経理担当者向けのRoleであるView BillingのRoleを例をイメージしながら説明します。

resource "aws_iam_role" "view_billing" {
name = "view_billing"
max_session_duration = 28800 # 8 hours
assume_role_policy =
data.aws_iam_policy_document.view_billing.json
}
data "aws_iam_policy_document" "view_billing" { statement {
sid = "AllowRoleAssumptionWithSAML"
effect = "Allow"
actions = ["sts:AssumeRoleWithSAML"]
principals {
type = "Federated"
identifiers =["arn:aws:iam::${data.aws_caller_identity.current.account_id}:saml-provider/Okta"]
}
}
# STSを取り出すための仕組用(後述)
statement {
sid = "AllowRoleAssumptionWithEC2"
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["ec2.amazonaws.com"]
}
}
}resource "aws_iam_policy" "view_billing" {
name = "view_billing"
path = "/"
description = "view_billing"
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Stmt1429754026000",
"Effect": "Allow",
"Action": [
"aws-portal:ViewAccount",
"aws-portal:ViewBilling",
"aws-portal:ViewPaymentMethods",
"aws-portal:ViewUsage"
],
"Resource": [
"*"
]
}
]
}
EOF
}
resource "aws_iam_role_policy_attachment" "view_billing" {
depends_on = [
aws_iam_role.view_billing,
aws_iam_policy.view_billing,
]

role = aws_iam_role.view_billing.name
policy_arn = aws_iam_policy.view_billing.arn
}

さらに、IAM Userの乱立を防ぐために各RoleからはIAM Userの操作ができないようにします。IAM User関連の操作をdisableするpolicyを作成し、それをRoleにアタッチします。

resource "aws_iam_policy" "iam_restriction" {
name = "iam_restriction"
path = "/"
description = "iam_restriction"
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "IAMPermissionManagementRestriction",
"Effect": "Deny",
"Action": [
"iam:DeactivateMFADevice",
"iam:CreateServiceSpecificCredential",
"iam:DeleteAccessKey",
"iam:UpdateOpenIDConnectProviderThumbprint",
"iam:ResetServiceSpecificCredential",
"iam:DeleteSSHPublicKey",
"iam:CreateVirtualMFADevice",
"iam:CreateSAMLProvider",
"iam:CreateUser",
"iam:CreateAccessKey",
"iam:CreateLoginProfile",
"iam:EnableMFADevice",
"iam:ResyncMFADevice",
"iam:UploadSSHPublicKey",
"iam:DetachUserPolicy",
"iam:DeleteOpenIDConnectProvider",
"iam:UpdateSAMLProvider",
"iam:DeleteLoginProfile",
"iam:ChangePassword",
"iam:UpdateLoginProfile",
"iam:UpdateServiceSpecificCredential",
"iam:UploadSigningCertificate",
"iam:RemoveClientIDFromOpenIDConnectProvider",
"iam:UpdateUser",
"iam:PutUserPermissionsBoundary",
"iam:DeleteUserPolicy",
"iam:AttachUserPolicy",
"iam:UpdateAccessKey",
"iam:DeleteUser",
"iam:DeleteUserPermissionsBoundary",
"iam:UpdateSSHPublicKey",
"iam:CreateOpenIDConnectProvider",
"iam:DeleteSigningCertificate",
"iam:DeleteVirtualMFADevice",
"iam:PutUserPolicy",
"iam:UpdateSigningCertificate",
"iam:AddClientIDToOpenIDConnectProvider",
"iam:DeleteServiceSpecificCredential",
"iam:DeleteSAMLProvider"
],
"Resource": [
"arn:aws:iam::*:saml-provider/*",
"arn:aws:iam::*:oidc-provider/*",
"arn:aws:iam::*:user/*",
"arn:aws:iam::*:sms-mfa/*",
"arn:aws:iam::*:mfa/*/*"
]
},
{
"Sid": "IAMWriteRestriction",
"Effect": "Deny",
"Action": [
"iam:SetSecurityTokenServicePreferences",
"iam:DeleteAccountPasswordPolicy",
"iam:UpdateAccountPasswordPolicy",
"iam:CreateAccountAlias",
"iam:DeleteAccountAlias"
],
"Resource": "*"
}
]
}
EOF
}
resource "aws_iam_role_policy_attachment" "view_billing_iam_restriction" {
depends_on = [
aws_iam_role.view_billing,
aws_iam_policy.iam_restriction,
]

role = aws_iam_role.view_billing.name
policy_arn = aws_iam_policy.iam_restriction.arn
}
* 各RoleはそのRoleたらしめるアクセス権(view_bilingであれば決済情報の閲覧権限)を定義したPolicy、IAM User関連の操作を禁ずるPolicy、その他今回は紹介しませんが各種センシティブデータを保有するS3などのデータアクセスを禁ずるPolicyなどが複数アタッチされた状態となります。

次にIdP側の設定です。設定方法は公式のドキュメントを紹介して今回の記事では割愛します。一通りアプリの設定後、Integration設定の中で先程作成したAWSのIAM Role一覧を取得するためのAWS Access / Secret Keyをセットし、API連携がうまくいけば、後はOkta上で誰(グループ)をどのRoleに所属させるかを設定します。(雑な説明でスミマセン)

最後にAWS側のIDプロバイダー設定を作成、Oktaのメタデータファイルを設定して完了です。ここまでセットできたら、IdP経由でのSSOを試してみます。無事成功すれば、マネジメントコンソールに遷移する前に自身に割り当てられたRoleを選択する画面が表示されます。(画像は管理者の私がログインした画面なので、

無事にマネジメントコンソールへたどり着いたら、右上のログイン情報をおもむろに見てみます。フェデレーションログインが成功していることが確認できました。

コンソールアクセス可能なIAM Userの完全撤廃

フェデレーションログインによるマネジメントコンソールへの道が確保できたら、AWS上に存在するコンソールログイン可能なIAM User全てのコンソールアクセスを無効化します。この段階ではIAM Userから払い出されたクレデンシャルは無効化されないのでババっと一気に無効化します。

(IAMユーザ一覧 -> 列の管理 -> コンソールへのアクセスにチェックを入れて、IAM User一覧から探すのが一番ラク)

次に、アクセスキーを払い出してるIAM Userを一つずつ確認し、一つずつ地道に無効化していきます。最後に使用された日時をベースに直近利用されてないやつはエイヤで無効化。残りは地道に調べたり社内に聞き周りながら利用有無や用途を確認しながら潰していきます(ここが一番泥臭い、、)。

最後に、コンソールログインをDisableした全IAM Userを削除します。この時点でIAM Userに紐づくクレデンシャルは全て無効化されているので、ここもエイヤで消してしまいます。(テキストで書くとあっという間ですが、このIAM User撤廃の工程が一番重い、、)

プリセットRoleの権限変更や新規IAM User作成時は特定人によるコードレビューを必須化する

次に、IAM Userの乱立を防ぐための体制を整えます。フェデレーションログインの手段を確立したので開発者用のIAM Userはもう必要ありません。

また、先程の手順でフェデレーションログイン用のIAM Role群は全てIAM User関連のリソース操作を禁じていますので、マネジメントコンソール経由でのIAM Userの新規作成や変更はできません。

とはいえ、例えば監視SaaSやCIなど外部ツールとの連携やマルチクラウド環境においてAWSクレデンシャルが必要なケースはまだまだあります。AWSリソース間であればIAM RoleやInstance Profileなどを利用すれば事足りますが、IAM Userが必要なシナリオは実際問題存在します。

そこで、今回の改修では、以下のようなTerrafor Moduleの構成にしました。各アカウント毎に作成したディレクトリからmoduleを呼び出し、フェデレーションログイン用のIAM UserやプリセットRoleとPolicy、Saas連携等で必要なIAM Userのプリセットをまとめて作成するようにします。

├── Account_A
│ ├── aws_account_bootstrap
│ │ ├── main.tf <= global_modulesを呼びプリセットRole等を一括作成
│ │ ├── provider.tf -> ../provider.tf
│ │ ├── terraform.tfstate
│ │ ├── variables.tf
│ │ └── versions.tf
│ ├── provider.tf
│ └── application
│ ├── main.tf
│ ├── provider.tf -> ../provider.tf
│ ├── terraform.tfstate
│ ├── variables.tf
│ └── versions.tf

├── Account_B
│ ├── aws_account_bootstrap
│ │ ├── main.tf <= global_modulesを呼びプリセットIAM群を一括作成
│ │ ├── provider.tf -> ../provider.tf
│ │ ├── terraform.tfstate
│ │ ├── variables.tf
│ │ └── versions.tf
│ ├── provider.tf
│ └── application
│ ├── main.tf
│ ├── provider.tf -> ../provider.tf
│ ├── terraform.tfstate
│ ├── variables.tf
│ └── versions.tf

├── global_modules
│ └── aws_account_bootstrap
│ ├── iam_user_okta.tf
│ ├── iam_role_A.tf <= フェデレーションログイン用プリセットRole(職責単位)
│ ├── iam_role_B.tf <= フェデレーションログイン用プリセットRole(職責単位)
│ ├── iam_role_C.tf <= フェデレーションログイン用プリセットRole(職責単位)
│ ├── iam_user_A.tf <= 外部Saas用プリセットIAM User (SaaS単位)
│ ├── iam_user_B.tf <= 外部Saas用プリセットIAM User (SaaS単位)

次に、このModule内のディレクトリ全体をGithubのCODEOWNERファイルの設定で特定の管理者Teamをレビュワーに指定し、かつこのレシピを管理するレポジトリのブランチ保護設定画面(Github上)からCODEOWNERによるレビューを強制化します。

実はこのタイミングで合わせて、社用レポジトリのブランチ保護設定を開発者が自身で無効化できないよう、Organizatinoレベル管理者権限の整理や職務分掌体制の構築、Github MembershipやTeam、所属の定義やレポジトリに対する権限設定などのTerraformによるバージョン管理化への移行などもやったりしました。が、長くなるので今回のブログでは割愛、、

開発者が自分自身で自身に割り当てられたRoleに応じたユニークなAWS STS(一時的な認証情報)を払い出せるようにする。

最後に、開発者が自身に割り当てられたRoleに応じたユニークなAWS STS(一時的な認証情報)を払い出せる仕組みを構築しました。

マネジメントコンソールへのログインが可能とはいえ、やはり開発者としてはCLIで色々hogehogeしたりしたいもの。そこで、期限付き認証情報をローカルからでも開発者毎にユニークな形で取り出せる仕組みが欲しいねという事で用意したのが背景です。

これを実現するために、アカウント毎に作成したフェデレーションログイン用IAM RoleをAssume RoleしたEC2を用意し、開発者が公開鍵認証による多段SSH経由でSTSを取り出せる仕組みを構築しました。この辺もセキュアかつトレーサビリティを担保するための仕組みを整えたのですが、執筆時間の関係で今回は割愛させていただきます:dogeza

* きっと弊社SRE’sの誰か書いてくれるはず、、

終わりに

この記事では、2019年度に実施したAWSのマルチアカウント環境におけるIAMマネジメントの最適化の試行錯誤の様子をお伝えしました。

現時点での私の結論としては、いかに「IAM Userが不要な体制を構築するか」がAWS IAMマネジメントのキモなのかなと考えています。が、アカウント管理は奥が深いです。

--

--