最小権限のIAM Policy作成にCloudFormationのコマンドが役立つ

2021.10.31

最小権限のIAM Policyを作成するのって地味に面倒ですよね。以前私は、Route53ホストゾーンにDNSレコード作成するのに必要な最小権限のPolicyを作るため、権限ゼロの状態から始めて、権限不足エラーが出るたびに権限を足していくという力技でPolicyを作ったことがあります。

もうちょっとスマートなやり方が、CloudFormation(CFn)のコマンドを使うとできる場合があることを学んだのでレポートします。

aws cloudformation describe-type

そのコマンドが、 aws cloudformation describe-typeです。--typeオプションでRESOURCEを指定して、 --type-nameでCFnのリソースタイプネーム(AWS::EC2::VPCみたいなやつのことです)を指定すると、そのリソースタイプの詳細情報を返してくれます。

aws cloudformation describe-type --type RESOURCE --type-name AWS::EC2::VPC

{
    "Arn": "arn:aws:cloudformation:ap-northeast-1::type/resource/AWS-EC2-VPC",
    "Type": "RESOURCE",
    "TypeName": "AWS::EC2::VPC",
    "IsDefaultVersion": true,
    "Description": "Resource Type definition for AWS::EC2::VPC",
    "Schema": "{\n  \"typeName\": \"AWS::EC2::VPC\",\n  \"description\": \"Resource Type definition for AWS::EC2::VPC\",\n  \"additionalProperties\": false,\n  \"properties\": {\n    \"Id\": {\n      \"type\": \"string\",\n      \"description\": \"The Id for the model.\"\n    },\n    \"CidrBlock\": {\n      \"type\": \"string\",\n      \"description\": \"The primary IPv4 CIDR block for the VPC.\"\n    },\n    \"CidrBlockAssociations\": {\n      \"type\": \"array\",\n      \"description\": \"A list of IPv4 CIDR block association IDs for the VPC.\",\n      \"uniqueItems\": false,\n      \"insertionOrder\": false,\n      \"items\": {\n        \"type\": \"string\"\n      }\n    },\n    \"DefaultNetworkAcl\": {\n      \"type\": \"string\",\n      \"insertionOrder\": false,\n      \"description\": \"The default network ACL ID that is associated with the VPC.\"\n    },\n    \"DefaultSecurityGroup\": {\n      \"type\": \"string\",\n      \"insertionOrder\": false,\n      \"description\": \"The default security group ID that is associated with the VPC.\"\n    },\n    \"Ipv6CidrBlocks\": {\n      \"type\": \"array\",\n      \"description\": \"A list of IPv6 CIDR blocks that are associated with the VPC.\",\n      \"uniqueItems\": false,\n      \"insertionOrder\": false,\n      \"items\": {\n        \"type\": \"string\"\n      }\n    },\n    \"EnableDnsHostnames\": {\n      \"type\": \"boolean\",\n      \"description\": \"Indicates whether the instances launched in the VPC get DNS hostnames. If enabled, instances in the VPC get DNS hostnames; otherwise, they do not. Disabled by default for nondefault VPCs.\"\n    },\n    \"EnableDnsSupport\": {\n      \"type\": \"boolean\",\n      \"description\": \"Indicates whether the DNS resolution is supported for the VPC. If enabled, queries to the Amazon provided DNS server at the 169.254.169.253 IP address, or the reserved IP address at the base of the VPC network range \\\"plus two\\\" succeed. If disabled, the Amazon provided DNS service in the VPC that resolves public DNS hostnames to IP addresses is not enabled. Enabled by default.\"\n    },\n    \"InstanceTenancy\": {\n      \"type\": \"string\",\n      \"description\": \"The allowed tenancy of instances launched into the VPC.\\n\\n\\\"default\\\": An instance launched into the VPC runs on shared hardware by default, unless you explicitly specify a different tenancy during instance launch.\\n\\n\\\"dedicated\\\": An instance launched into the VPC is a Dedicated Instance by default, unless you explicitly specify a tenancy of host during instance launch. You cannot specify a tenancy of default during instance launch.\\n\\nUpdating InstanceTenancy requires no replacement only if you are updating its value from \\\"dedicated\\\" to \\\"default\\\". Updating InstanceTenancy from \\\"default\\\" to \\\"dedicated\\\" requires replacement.\"\n    },\n    \"Tags\": {\n      \"type\": \"array\",\n      \"description\": \"The tags for the VPC.\",\n      \"uniqueItems\": false,\n      \"insertionOrder\": false,\n      \"items\": {\n        \"$ref\": \"#/definitions/Tag\"\n      }\n    }\n  },\n  \"definitions\": {\n    \"Tag\": {\n      \"type\": \"object\",\n      \"additionalProperties\": false,\n      \"properties\": {\n        \"Key\": {\n          \"type\": \"string\"\n        },\n        \"Value\": {\n          \"type\": \"string\"\n        }\n      },\n      \"required\": [\n        \"Value\",\n        \"Key\"\n      ]\n    }\n  },\n  \"taggable\": true,\n  \"required\": [\n    \"CidrBlock\"\n  ],\n  \"createOnlyProperties\": [\n    \"/properties/CidrBlock\"\n  ],\n  \"conditionalCreateOnlyProperties\": [\n    \"/properties/InstanceTenancy\"\n  ],\n  \"readOnlyProperties\": [\n    \"/properties/Id\",\n    \"/properties/DefaultSecurityGroup\",\n    \"/properties/CidrBlockAssociations\",\n    \"/properties/DefaultNetworkAcl\",\n    \"/properties/Ipv6CidrBlocks\"\n  ],\n  \"primaryIdentifier\": [\n    \"/properties/Id\"\n  ],\n  \"handlers\": {\n    \"create\": {\n      \"permissions\": [\n        \"ec2:CreateVpc\",\n        \"ec2:CreateTags\",\n        \"ec2:ModifyVpcAttribute\",\n        \"ec2:DescribeVpcs\",\n        \"ec2:DescribeSecurityGroups\",\n        \"ec2:DescribeNetworkAcls\",\n        \"ec2:DescribeVpcAttribute\"\n      ]\n    },\n    \"read\": {\n      \"permissions\": [\n        \"ec2:DescribeVpcs\",\n        \"ec2:DescribeSecurityGroups\",\n        \"ec2:DescribeNetworkAcls\",\n        \"ec2:DescribeVpcAttribute\"\n      ]\n    },\n    \"update\": {\n      \"permissions\": [\n        \"ec2:CreateTags\",\n        \"ec2:ModifyVpcAttribute\",\n        \"ec2:DescribeVpcs\",\n        \"ec2:DeleteTags\",\n        \"ec2:ModifyVpcTenancy\",\n        \"ec2:DescribeSecurityGroups\",\n        \"ec2:DescribeNetworkAcls\",\n        \"ec2:DescribeVpcAttribute\"\n      ]\n    },\n    \"delete\": {\n      \"permissions\": [\n        \"ec2:DeleteVpc\",\n        \"ec2:DescribeVpcs\",\n        \"ec2:DeleteTags\"\n      ]\n    },\n    \"list\": {\n      \"permissions\": [\n        \"ec2:DescribeVpcs\"\n      ]\n    }\n  }\n}\n",
    "ProvisioningType": "FULLY_MUTABLE",
    "DeprecatedStatus": "LIVE",
    "Visibility": "PUBLIC",
    "TimeCreated": "2021-08-20T20:25:24.577000+00:00"
}

このコマンドの出力値Schema、そのままだとパースされていないので読みにくのですが、jqを使ってパースしてみます。

aws cloudformation describe-type --type RESOURCE --type-name AWS::EC2::VPC --query Schema --output text | jq

{
  "typeName": "AWS::EC2::VPC",
  "description": "Resource Type definition for AWS::EC2::VPC",
  "additionalProperties": false,
  "properties": {
    "Id": {
      "type": "string",
      "description": "The Id for the model."
    },
    "CidrBlock": {
      "type": "string",
      "description": "The primary IPv4 CIDR block for the VPC."
    },
    "CidrBlockAssociations": {
      "type": "array",
      "description": "A list of IPv4 CIDR block association IDs for the VPC.",
      "uniqueItems": false,
      "insertionOrder": false,
      "items": {
        "type": "string"
      }
    },
    "DefaultNetworkAcl": {
      "type": "string",
      "insertionOrder": false,
      "description": "The default network ACL ID that is associated with the VPC."
    },
    "DefaultSecurityGroup": {
      "type": "string",
      "insertionOrder": false,
      "description": "The default security group ID that is associated with the VPC."
    },
    "Ipv6CidrBlocks": {
      "type": "array",
      "description": "A list of IPv6 CIDR blocks that are associated with the VPC.",
      "uniqueItems": false,
      "insertionOrder": false,
      "items": {
        "type": "string"
      }
    },
    "EnableDnsHostnames": {
      "type": "boolean",
      "description": "Indicates whether the instances launched in the VPC get DNS hostnames. If enabled, instances in the VPC get DNS hostnames; otherwise, they do not. Disabled by default for nondefault VPCs."
    },
    "EnableDnsSupport": {
      "type": "boolean",
      "description": "Indicates whether the DNS resolution is supported for the VPC. If enabled, queries to the Amazon provided DNS server at the 169.254.169.253 IP address, or the reserved IP address at the base of the VPC network range \"plus two\" succeed. If disabled, the Amazon provided DNS service in the VPC that resolves public DNS hostnames to IP addresses is not enabled. Enabled by default."
    },
    "InstanceTenancy": {
      "type": "string",
      "description": "The allowed tenancy of instances launched into the VPC.\n\n\"default\": An instance launched into the VPC runs on shared hardware by default, unless you explicitly specify a different tenancy during instance launch.\n\n\"dedicated\": An instance launched into the VPC is a Dedicated Instance by default, unless you explicitly specify a tenancy of host during instance launch. You cannot specify a tenancy of default during instance launch.\n\nUpdating InstanceTenancy requires no replacement only if you are updating its value from \"dedicated\" to \"default\". Updating InstanceTenancy from \"default\" to \"dedicated\" requires replacement."
    },
    "Tags": {
      "type": "array",
      "description": "The tags for the VPC.",
      "uniqueItems": false,
      "insertionOrder": false,
      "items": {
        "$ref": "#/definitions/Tag"
      }
    }
  },
  "definitions": {
    "Tag": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "Key": {
          "type": "string"
        },
        "Value": {
          "type": "string"
        }
      },
      "required": [
        "Value",
        "Key"
      ]
    }
  },
  "taggable": true,
  "required": [
    "CidrBlock"
  ],
  "createOnlyProperties": [
    "/properties/CidrBlock"
  ],
  "conditionalCreateOnlyProperties": [
    "/properties/InstanceTenancy"
  ],
  "readOnlyProperties": [
    "/properties/Id",
    "/properties/DefaultSecurityGroup",
    "/properties/CidrBlockAssociations",
    "/properties/DefaultNetworkAcl",
    "/properties/Ipv6CidrBlocks"
  ],
  "primaryIdentifier": [
    "/properties/Id"
  ],
  "handlers": {
    "create": {
      "permissions": [
        "ec2:CreateVpc",
        "ec2:CreateTags",
        "ec2:ModifyVpcAttribute",
        "ec2:DescribeVpcs",
        "ec2:DescribeSecurityGroups",
        "ec2:DescribeNetworkAcls",
        "ec2:DescribeVpcAttribute"
      ]
    },
    "read": {
      "permissions": [
        "ec2:DescribeVpcs",
        "ec2:DescribeSecurityGroups",
        "ec2:DescribeNetworkAcls",
        "ec2:DescribeVpcAttribute"
      ]
    },
    "update": {
      "permissions": [
        "ec2:CreateTags",
        "ec2:ModifyVpcAttribute",
        "ec2:DescribeVpcs",
        "ec2:DeleteTags",
        "ec2:ModifyVpcTenancy",
        "ec2:DescribeSecurityGroups",
        "ec2:DescribeNetworkAcls",
        "ec2:DescribeVpcAttribute"
      ]
    },
    "delete": {
      "permissions": [
        "ec2:DeleteVpc",
        "ec2:DescribeVpcs",
        "ec2:DeleteTags"
      ]
    },
    "list": {
      "permissions": [
        "ec2:DescribeVpcs"
      ]
    }
  }
}

一番下のhandlersプロパティに注目です。そこだけ抜き出してみます。

aws cloudformation describe-type --type RESOURCE --type-name AWS::EC2::VPC --query Schema --output text | jq .handlers

{
  "create": {
    "permissions": [
      "ec2:CreateVpc",
      "ec2:CreateTags",
      "ec2:ModifyVpcAttribute",
      "ec2:DescribeVpcs",
      "ec2:DescribeSecurityGroups",
      "ec2:DescribeNetworkAcls",
      "ec2:DescribeVpcAttribute"
    ]
  },
  "read": {
    "permissions": [
      "ec2:DescribeVpcs",
      "ec2:DescribeSecurityGroups",
      "ec2:DescribeNetworkAcls",
      "ec2:DescribeVpcAttribute"
    ]
  },
  "update": {
    "permissions": [
      "ec2:CreateTags",
      "ec2:ModifyVpcAttribute",
      "ec2:DescribeVpcs",
      "ec2:DeleteTags",
      "ec2:ModifyVpcTenancy",
      "ec2:DescribeSecurityGroups",
      "ec2:DescribeNetworkAcls",
      "ec2:DescribeVpcAttribute"
    ]
  },
  "delete": {
    "permissions": [
      "ec2:DeleteVpc",
      "ec2:DescribeVpcs",
      "ec2:DeleteTags"
    ]
  },
  "list": {
    "permissions": [
      "ec2:DescribeVpcs"
    ]
  }
}

対象リソース(この例だとVPC)に対する各種操作(create/read/update/delete/list)に必要な権限の一覧が出力されています。 CFnを実行するIAMエンティティの最小ポリシーを作りたいのであれば、テンプレート内のリソースタイプを一つずつ--type-nameに指定してこのコマンドを実行すれば、最小権限がすぐわかりますね。それ以外の場合でも、やりたいこととCFnリソースタイプの関係さえ掴めればこのコマンドを使って最小権限を調べることができます。

全リソースに対応しているわけではない

残念ながらこのhandlerプロパティは全てのリソースタイプで出力してくれるわけではありません。例えば最初に挙げたRoute53ホストゾーンのDNSレコード、これは対応していませんでした。

aws cloudformation describe-type --type RESOURCE --type-name AWS::Route53::RecordSet --query Schema --output text | jq

{
  "typeName": "AWS::Route53::RecordSet",
  "description": "Resource Type definition for AWS::Route53::RecordSet",
  "additionalProperties": false,
  "properties": {
    "Id": {
      "type": "string"
    },
    "AliasTarget": {
      "$ref": "#/definitions/AliasTarget"
    },
    "Comment": {
      "type": "string"
    },
    "Failover": {
      "type": "string"
    },
    "GeoLocation": {
      "$ref": "#/definitions/GeoLocation"
    },
    "HealthCheckId": {
      "type": "string"
    },
    "HostedZoneId": {
      "type": "string"
    },
    "HostedZoneName": {
      "type": "string"
    },
    "MultiValueAnswer": {
      "type": "boolean"
    },
    "Name": {
      "type": "string"
    },
    "Region": {
      "type": "string"
    },
    "ResourceRecords": {
      "type": "array",
      "uniqueItems": false,
      "items": {
        "type": "string"
      }
    },
    "SetIdentifier": {
      "type": "string"
    },
    "TTL": {
      "type": "string"
    },
    "Type": {
      "type": "string"
    },
    "Weight": {
      "type": "integer"
    }
  },
  "definitions": {
    "AliasTarget": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "DNSName": {
          "type": "string"
        },
        "EvaluateTargetHealth": {
          "type": "boolean"
        },
        "HostedZoneId": {
          "type": "string"
        }
      },
      "required": [
        "DNSName",
        "HostedZoneId"
      ]
    },
    "GeoLocation": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "ContinentCode": {
          "type": "string"
        },
        "CountryCode": {
          "type": "string"
        },
        "SubdivisionCode": {
          "type": "string"
        }
      }
    }
  },
  "required": [
    "Type",
    "Name"
  ],
  "createOnlyProperties": [
    "/properties/HostedZoneName",
    "/properties/Name",
    "/properties/HostedZoneId"
  ],
  "primaryIdentifier": [
    "/properties/Id"
  ],
  "readOnlyProperties": [
    "/properties/Id"
  ]
}

どうやら aws cloudformation describe-typeの返り値 ProvisioningTypeの値がNON_PROVISIONABLEの物はhandlerプロパティを出力しないようです。(ProvisioningTypeの値はNON_PROVISIONABLEIMMUTABLEFULLY_MUTABLEの3パターンあります。)

最小権限のIAM Policyを作成する別の方法

IAM Access Analyzerを使えば、実際の操作履歴からPolicyを作ることができます。一度広めの権限を与えてから絞っていくようなアプローチですね。