Terraform で AWS IAM ポリシーや Amazon S3 バケットポリシーを実装するときに terraform plan
は通るのに terraform apply
で失敗したり,terraform apply
は通るのにポリシー自体に誤りがあって期待通りに動かなかったり,無駄にハマってしまった経験ってないでしょうか💨少なくとも僕はたくさんあるー \( 'ω')/
そこで Terraform と AWS IAM Access Analyzer の「ポリシー検証」を組み合わせたらどうだろう❓と思って調べてみたところ,GitHub の awslabs/terraform-iam-policy-validator
リポジトリに Python で実装された「IAM Policy Validator for Terraform」というツールがあった❗️今回はこの IAM Policy Validator for Terraform を試した結果をまとめる.
セットアップ ✔️
GitHub に載っている通り pip install
で OK👌
$ pip install tf-policy-validator $ tf-policy-validator -h usage: tf-policy-validator [-h] --template-path TEMPLATE_PATH --region REGION [--profile PROFILE] [--config CONFIG [CONFIG ...]] [--enable-logging] [--ignore-finding FINDING_CODE,RESOURCE_NAME,RESOURCE_NAME.FINDING_CODE] [--treat-finding-type-as-blocking ERROR,SECURITY_WARNING] [--allow-external-principals ACCOUNT,ARN] options: -h, --help show this help message and exit --template-path TEMPLATE_PATH Terraform plan file (JSON) --region REGION The region the resources will be deployed to. --profile PROFILE The named profile to use for AWS API calls. --config CONFIG [CONFIG ...] Config file for running this script --enable-logging Enable detailed logging. --ignore-finding FINDING_CODE,RESOURCE_NAME,RESOURCE_NAME.FINDING_CODE Allow validation failures to be ignored. Specify as a comma separated list of findings to be ignored. Can be individual finding codes (e.g. "PASS_ROLE_WITH_STAR_IN_RESOURCE"), a specific resource name (e.g. "MyResource"), or a combination of both separated by a period.(e.g. "MyResource.PASS_ROLE_WITH_STAR_IN_RESOURCE"). --treat-finding-type-as-blocking ERROR,SECURITY_WARNING Specify which finding types should be treated as blocking. Other finding types are treated as non-blocking. Defaults to "ERROR" and "SECURITY_WARNING". Specify as a comma separated list of finding types that should be blocking. Possible values are "ERROR", "SECURITY_WARNING", "SUGGESTION", and "WARNING". Pass "NONE" to ignore all errors. --allow-external-principals ACCOUNT,ARN A comma separated list of external principals that should be ignored. Specify as a comma separated list of a 12 digit AWS account ID, a federated web identity user, a federated SAML user, or an ARN. Specify "*" to allow anonymous access. (e.g. 123456789123,arn:aws:iam::111111111111:role/MyOtherRole,graph.facebook.com)
仕組み ✔️
IAM Policy Validator for Terraform の仕組みをザッとまとめると以下のようになる.
IAM Policy Validator for Terraform を試す ✔️
1. AWS IAM ポリシーに間違ったアクション名を指定してしまった場合
Terraform の aws_iam_policy
リソースと aws_iam_policy_document
データソースを使って AWS IAM ポリシーを設定するときに sqs:SendMessage
ではなくsqs:SendMessages
という間違ったアクション名を指定してしまっているサンプルを準備した.
resource "aws_iam_policy" "example" { name = "example" policy = data.aws_iam_policy_document.example.json } data "aws_iam_policy_document" "example" { statement { effect = "Allow" actions = ["sqs:SendMessages"] resources = ["arn:aws:sqs:*:123456789012:MyCompanyQueue"] } }
terraform plan
コマンドと terraform show
コマンドを使って JSON 形式のプラン結果 tf.json
を取得する.そして tf-policy-validator
コマンドを実行すると AWS IAM Access Analyzer の検証結果が出て The action sqs:SendMessages does not exist.
と書いてある.ちゃんとアクション名の間違いを検出できている👏
ちなみに --config default.yaml
のように指定している YAML ファイルに関しては後ほど後述する💡
$ terraform plan -out tf.plan $ terraform show -json -no-color tf.plan > tf.json $ tf-policy-validator --region ap-northeast-1 --template-path tf.json --config default.yaml { "BlockingFindings": [ { "findingType": "ERROR", "code": "INVALID_ACTION", "message": "The action sqs:SendMessages does not exist.", "resourceName": "example", "policyName": "aws_iam_policy.example", "details": { "findingDetails": "The action sqs:SendMessages does not exist.", "findingType": "ERROR", "issueCode": "INVALID_ACTION", "learnMoreLink": "https://docs.aws.amazon.com/IAM/latest/UserGuide/access-analyzer-reference-policy-checks.html#access-analyzer-reference-policy-checks-error-invalid-action", "locations": [ { "path": [ { "value": "Statement" }, { "value": "Action" } ], "span": { "start": { "line": 5, "column": 18, "offset": 95 }, "end": { "line": 5, "column": 36, "offset": 113 } } } ] } } ], "NonBlockingFindings": [] }
サンプルとして使った AWS IAM ポリシーは以下のドキュメントの Example 1: Allow a user to create queues
を参考にした.
2. Amazon S3 バケットポリシーに間違った条件キーを指定してしまった場合
Terraform の aws_s3_bucket_policy
リソースと aws_iam_policy_document
データソースを使って Amazon S3 バケットポリシーを設定するときに s3:x-amz-server-side-encryption-aws-kms-key-id
ではなくs3:x-amz-server-side-encryption-aws-kms-key-ids
という間違ったアクション名を指定してしまっているサンプルを準備した.
resource "aws_s3_bucket_policy" "example" { bucket = "example" policy = data.aws_iam_policy_document.example.json } data "aws_iam_policy_document" "example" { statement { effect = "Deny" actions = ["s3:PutObject"] principals { type = "*" identifiers = ["*"] } resources = [ "arn:aws:s3:::example/*" ] condition { test = "Null" variable = "s3:x-amz-server-side-encryption-aws-kms-key-ids" values = ["true"] } } }
terraform plan
コマンドと terraform show
コマンドを使って JSON 形式のプラン結果 tf.json
を取得する.そして tf-policy-validator
コマンドを実行すると AWS IAM Access Analyzer の検証結果が出て The condition key s3:x-amz-server-side-encryption-aws-kms-key-ids does not exist in the service s3. Use a valid condition key.
と書いてある.ちゃんと条件キーの間違いを検出できている👏
ちなみに --config default.yaml
のように指定している YAML ファイルに関しては後ほど後述する💡
$ terraform plan -out tf.plan $ terraform show -json -no-color tf.plan > tf.json $ tf-policy-validator --region ap-northeast-1 --template-path tf.json --config default.yaml { "BlockingFindings": [ { "findingType": "ERROR", "code": "INVALID_SERVICE_CONDITION_KEY", "message": "The condition key s3:x-amz-server-side-encryption-aws-kms-key-ids does not exist in the service s3. Use a valid condition key.", "resourceName": "example", "policyName": "aws_s3_bucket_policy.example", "details": { "findingDetails": "The condition key s3:x-amz-server-side-encryption-aws-kms-key-ids does not exist in the service s3. Use a valid condition key.", "findingType": "ERROR", "issueCode": "INVALID_SERVICE_CONDITION_KEY", "learnMoreLink": "https://docs.aws.amazon.com/IAM/latest/UserGuide/access-analyzer-reference-policy-checks.html#access-analyzer-reference-policy-checks-error-invalid-service-condition-key", "locations": [ { "path": [ { "value": "Statement" }, { "value": "Condition" }, { "value": "Null" }, { "key": "s3:x-amz-server-side-encryption-aws-kms-key-ids" } ], "span": { "start": { "line": 8, "column": 16, "offset": 163 }, "end": { "line": 8, "column": 65, "offset": 212 } } } ] } } ], "NonBlockingFindings": [] }
サンプルとして使った Amazon S3 バケットポリシーは以下のドキュメントの Require SSE-KMS for all objects written to a bucket
を参考にした.
--config default.yaml
に関して ✔️
IAM Policy Validator for Terraform を試すときに特にハマった(最初よくわからなかった)のは --config default.yaml
と指定している default.yaml
だった.以下に GitHub でサンプルとして紹介されている default.yaml をそのまま載せる.設定項目としては大きく3種類 arnServiceMap
と iamPolicyAttributes
と validatePolicyResourceType
がある.
厳しかったのは GitHub に default.yaml
の説明がなく理解しにくいところで,最終的に Python の実装を読みながらデバッグをしてやっと理解できた💡(と思う)
# generate fake ARN # default can be specified using the following format: # {<key>?<default>} arnServiceMap: aws_iam_policy: name?fakename aws_iam_role: name?fakename aws_api_gateway_rest_api_policy: rest_api_id?fakeRestApiId aws_backup_vault_policy: backup_vault_name?fakeBackupVaultName aws_cloudwatch_event_bus_policy: event_bus_name?fakeEventBusName aws_cloudwatch_log_destination_policy: destination_name?fakeDestinationName aws_codeartifact_domain_permissions_policy: domain?fakeDomain aws_codeartifact_repository_permissions_policy: repository?fakeRepository aws_codebuild_resource_policy: fakename aws_ecr_registry_policy: fakename aws_ecr_repository_policy: repository?fakeRepositoryName aws_ecrpublic_repository_policy: repository_name?fakeRepositoryName aws_efs_file_system_policy: file_system_id?fakeFileSystemId aws_elasticsearch_domain: domain_name?fakeDomainName aws_elasticsearch_domain_policy: domain_name?fakeDomainName aws_glacier_vault: name?fakename aws_glacier_vault_lock: vault_name?fakeVaultName aws_glue_resource_policy: fakeName aws_iot_policy: name?fakename aws_kms_external_key: fakeName aws_kms_key: fakeName aws_kms_replica_external_key: fakeName aws_kms_replica_key: fakeName # aws_lambda_layer_version_permission: layer_name?fakeLayberName aws_media_store_container_policy: container_name?fakeContainerName aws_networkfirewall_resource_policy: resource_arn?fakeResourceArn aws_organizations_policy: name?fakename aws_s3_access_point: name?fakename aws_s3_bucket: bucket?fakeBucket aws_s3_bucket_policy: bucket?fakeBucket aws_s3control_access_point_policy: access_point_arn?fakeAccessPointArn aws_s3control_bucket_policy: bucket?fakeBucket aws_s3control_multi_region_access_point_policy: details.name?fakename aws_s3control_object_lambda_access_point_policy: name?fakename aws_ses_identity_policy: name?fakename aws_sns_topic: name?fakename aws_sns_topic_policy: arn?fakename aws_sqs_queue: name?fakename aws_sqs_queue_policy: fakeQueueUrl aws_ssoadmin_permission_set_inline_policy: instance_arn?fakeSSOInstanceArn aws_sagemaker_model_package_group_policy: model_package_group_name?fakeModelPackageGroupName aws_secretsmanager_secret: name?fakename aws_secretsmanager_secret_policy: secret_arn?fakeSecretArn aws_transfer_access: server_id?fakeServerId aws_transfer_user: user_name?fakeUserName aws_vpc_endpoint: fakeName # iamChecks: # - AccessAnalyzer # iamExceptions: # AccessAnalyzer: # - Arn: "arn:aws:iam::123456789012:policy/test_policy" iamPolicyAttributes: aws_iam_group_policy: policy aws_iam_policy: policy aws_iam_role: - assume_role_policy - inline_policy.policy aws_iam_role_policy: policy aws_iam_user_policy: policy aws_api_gateway_rest_api: policy #note aws_api_gateway_rest_api_policy: policy aws_backup_vault_policy: policy aws_cloudwatch_event_bus_policy: policy aws_cloudwatch_log_destination_policy: access_policy aws_cloudwatch_log_resource_policy: policy aws_codeartifact_domain_permissions_policy: policy_document aws_codeartifact_repository_permissions_policy: policy_document aws_codebuild_resource_policy: policy aws_ecr_registry_policy: policy aws_ecr_repository_policy: policy aws_ecrpublic_repository_policy: policy aws_efs_file_system_policy: policy aws_elasticsearch_domain: access_policies aws_elasticsearch_domain_policy: access_policies aws_glacier_vault: access_policy aws_glacier_vault_lock: access_policy aws_glue_resource_policy: policy aws_iot_policy: policy aws_kms_external_key: policy aws_kms_key: policy aws_kms_replica_external_key: policy aws_kms_replica_key: policy # aws_lambda_layer_version_permission: policy aws_media_store_container_policy: policy aws_networkfirewall_resource_policy: policy aws_organizations_policy: content aws_s3_access_point: policy aws_s3_bucket: policy aws_s3_bucket_policy: policy aws_s3control_access_point_policy: policy aws_s3control_bucket_policy: policy aws_s3control_multi_region_access_point_policy: details.policy aws_s3control_object_lambda_access_point_policy: policy aws_ses_identity_policy: policy aws_sns_topic: policy aws_sns_topic_policy: policy aws_sqs_queue: policy aws_sqs_queue_policy: policy aws_ssoadmin_permission_set_inline_policy: inline_policy aws_sagemaker_model_package_group_policy: resource_policy aws_secretsmanager_secret: policy aws_secretsmanager_secret_policy: policy aws_transfer_access: policy aws_transfer_user: policy aws_vpc_endpoint: policy validatePolicyResourceType: aws_s3_bucket: AWS::S3::Bucket aws_s3_bucket_policy: AWS::S3::Bucket aws_s3control_access_point_policy: AWS::S3::AccessPoint aws_s3control_multi_region_access_point_policy: AWS::S3::MultiRegionAccessPoint aws_s3control_object_lambda_access_point_policy: AWS::S3ObjectLambda::AccessPoint
1. arnServiceMap
arnServiceMap
は「Terraform リソースタイプ」と「名前を表すパラメータ」をマッピングする設定で,例えば aws_iam_policy
だと name
パラメータで,aws_s3_bucket_policy
だと bucket
パラメータになる.
- aws_iam_policy | Resources | hashicorp/aws | Terraform | Terraform Registry
- aws_s3_bucket_policy | Resources | hashicorp/aws | Terraform | Terraform Registry
ちなみに ?
を区切り文字にできて右側は名前を表すパラメータが存在しない場合のデフォルト値になる.例えば aws_iam_policy
の name
パラメータは Optional なので Terraform コードで指定されない場合がある💡(ただ aws_s3_bucket_policy
の bucket
パラメータは Required なんだけども...)
arnServiceMap: aws_iam_policy: name?fakename aws_s3_bucket_policy: bucket?fakeBucket
2. iamPolicyAttributes
iamPolicyAttributes
は「Terraform リソースタイプ」と「ポリシーを表すパラメータ」をマッピングする設定で,例えば aws_iam_policy
も aws_s3_bucket_policy
も policy
パラメータになる.
iamPolicyAttributes: aws_iam_policy: policy aws_s3_bucket_policy: policy
3. validatePolicyResourceType
validatePolicyResourceType
には AWS::S3::Bucket
や AWS::S3::AccessPoint
など特定のリソースタイプを指定できる.boto3 のドキュメントを見ると書いてあるけど,AWS IAM Access Analyzer の ValidatePolicy API の仕様として,検証するポリシータイプ policyType
に IDENTITY_POLICY
を指定すれば AWS IAM ポリシー関連を検証できて,RESOURCE_POLICY
を指定すればリソースポリシーを検証できる仕組みになっている.
ただし AWS::S3::Bucket
や AWS::S3::AccessPoint
は AWS CloudFormation のリソースタイプの名称になっているため,Terraform のリソースタイプである aws_s3_bucket_policy
や aws_s3control_access_point_policy
をマッピングする必要があって,それが validatePolicyResourceType
の役割となる.
default.yaml
をどうするべきか
結論としては GitHub の default.yaml
をそのままコピーして使ってしまって良いと思う👌とは言え不要な設定があるのが気になる場合や独自にカスタマイズをする場合などは書き換えることもできる.僕自身は今回必要最低限の以下の default.yaml
を作って動作確認をした❗️
arnServiceMap: aws_iam_policy: name?fakename aws_s3_bucket_policy: bucket?fakeBucket iamPolicyAttributes: aws_iam_policy: policy aws_s3_bucket_policy: policy validatePolicyResourceType: aws_s3_bucket_policy: AWS::S3::Bucket
IAM Policy Validator for Terraform x GitHub Actions ✔️
IAM Policy Validator for Terraform を GitHub Actions で実行して CI/CD パイプラインに組み込む場合は以下のようにワークフローを実装すれば OK👌
name: IAM Policy Validator for Terraform on: push: branches: - master pull_request: branches: - master permissions: id-token: write contents: read jobs: accessanalyzer: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 - name: Install dependencies run: pip install tf-policy-validator - name: Configure AWS Credentials uses: aws-actions/configure-aws-credentials@v2 with: role-to-assume: ${{ secrets.AWS_ROLE_ARN }} aws-region: ap-northeast-1 - uses: hashicorp/setup-terraform@v3 - name: Run IAM Policy Validator for Terraform run: | terraform init terraform plan -out tf.plan terraform show -json -no-color tf.plan > tf.json tf-policy-validator --region ap-northeast-1 --template-path tf.json --config default.yaml
注意点 ✔️
GitHub の Limitations
にも書いてあるけど,IAM Policy Validator for Terraform は Terraform のプラン結果を活用するため Computed
なパラメータ (known after apply)
を検出できないという制約がある.よって,例えば Amazon S3 バケットポリシーの場合は通常実装するであろう aws_s3_bucket
リソースを組み合わせるとプラン結果が異なるため「検出なし」と判断されてしまう💨
{ "BlockingFindings": [], "NonBlockingFindings": [] }
まとめ ✔️
Terraform と AWS IAM Access Analyzer の「ポリシー検証」を組み合わせて AWS IAM ポリシーや Amazon S3 バケットポリシーの間違いを検出できるツール「IAM Policy Validator for Terraform」を試してみた❗️注意点はあって完璧ではないけど GitHub Actions などの CI/CD パイプラインに組み込めて便利👏
関連情報
AWS re:Invent 2022 のセッション Prevent unintended access with AWS IAM Access Analyzer policy validation (SEC319) の資料を読んでいたら IAM Policy Validator for Terraform も紹介されていた❗️同じように AWS CloudFormation で検証する「IAM Policy Validator for AWS CloudFormation」も紹介されていた👀
あと IAM Policy Validator for Terraform のサンプルを試したらうまく実行できなくてドキュメントを修正したプルリクエストも送っておいた✌️
また AWS IAM Access Analyzer で検証できる項目は他にも多くあって以下のドキュメントにまとまっている📝