kakakakakku blog

Weekly Tech Blog: Keep on Learning!

Terraform で実装した IAM ポリシーを IAM Access Analyzer で検証できる tf-policy-validator コマンド

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.com

セットアップ ✔️

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 を参考にした.

docs.aws.amazon.com

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 を参考にした.

docs.aws.amazon.com

--config default.yaml に関して ✔️

IAM Policy Validator for Terraform を試すときに特にハマった(最初よくわからなかった)のは --config default.yaml と指定している default.yaml だった.以下に GitHub でサンプルとして紹介されている default.yaml をそのまま載せる.設定項目としては大きく3種類 arnServiceMapiamPolicyAttributesvalidatePolicyResourceType がある.

厳しかったのは 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_policyname パラメータは Optional なので Terraform コードで指定されない場合がある💡(ただ aws_s3_bucket_policybucket パラメータは Required なんだけども...)

arnServiceMap:
  aws_iam_policy: name?fakename
  aws_s3_bucket_policy: bucket?fakeBucket

2. iamPolicyAttributes

iamPolicyAttributes「Terraform リソースタイプ」「ポリシーを表すパラメータ」をマッピングする設定で,例えば aws_iam_policyaws_s3_bucket_policypolicy パラメータになる.

iamPolicyAttributes:
  aws_iam_policy: policy
  aws_s3_bucket_policy: policy

3. validatePolicyResourceType

validatePolicyResourceType には AWS::S3::BucketAWS::S3::AccessPoint など特定のリソースタイプを指定できる.boto3 のドキュメントを見ると書いてあるけど,AWS IAM Access Analyzer の ValidatePolicy API の仕様として,検証するポリシータイプ policyTypeIDENTITY_POLICY を指定すれば AWS IAM ポリシー関連を検証できて,RESOURCE_POLICY を指定すればリソースポリシーを検証できる仕組みになっている.

boto3.amazonaws.com

ただし AWS::S3::BucketAWS::S3::AccessPoint は AWS CloudFormation のリソースタイプの名称になっているため,Terraform のリソースタイプである aws_s3_bucket_policyaws_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」も紹介されていた👀

github.com

Prevent unintended access with AWS IAM Access Analyzer policy validation (SEC319) から引用

あと IAM Policy Validator for Terraform のサンプルを試したらうまく実行できなくてドキュメントを修正したプルリクエストも送っておいた✌️

github.com

また AWS IAM Access Analyzer で検証できる項目は他にも多くあって以下のドキュメントにまとまっている📝

docs.aws.amazon.com