IAMが昨日よりもちょっと好きになる

こんにちは。エンジニアの山下です。

AWSを利用するうえでとても重要なサービス「IAM」ですが、AssumeRole,STS周りがしっくり来ていないので、Terraformを使って今日から理解を深めます。

なおIAMの基本的な仕様を実際に確認して理解を深めるといった内容になっているので、IAMを使ったアクセス制御アーキテクチャなどには触れません。

前提知識

  • Terraformでpublic subnetにEC2を構築できるくらいの知識

IAMって何

「Identity and Access Management」を略して「IAM」(アイアム)と呼びます。

AWSには約240ものサービス(2023/11/16時点)があり、そのそれぞれに役割があり出来る事が違います。IAMはそれらサービスやそのリソースに対して権限を付与したり、リソースに誰がアクセス出来るのかを管理します。

IAM自体も240あるサービスの内の1つにあたるので、例えば「誰がどのIAMの機能にアクセスできるのか」という設定はIAMを使って行います。

準備

これからIAMユーザをはじめとしたAWSのリソースの作成を行いますが、その前にTerraformやDockerの準備を行います。

ディレクトリ

  • workdir/
    • aws/
      • config // aws-cliで使う~/.aws/config
      • credentials // aws-cliで使う~/.aws/credentials
    • account_a
      • main.tf
      • .env // Terraformの実行IAMユーザ情報
    • docker-compose.yaml

workdir/account_a/.env

#Terraformを実行するIAMユーザ
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=

workdir/aws/credentials

[default]
#AdministratorAccessを持つIAMユーザ
aws_access_key_id=
aws_secret_access_key=

workdir/aws/config

[default]
region=ap-northeast-1
output=json

docker

docker-compose.yaml

services: 

  Terraform-a:
    image: hashicorp/Terraform:1.4
    entrypoint: sh
    env_file:
      - ./account_a/.env
    volumes:
      - ./account_a:/account_a
    working_dir: /account_a
    tty: true

  aws:
    image: amazon/aws-cli:latest
    entrypoint: sh
    volumes:
      - ./aws:/root/.aws
    tty: true

Terraform

main.tf

Terraform {
  required_version = "~> 1.4.5"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.0"
    }
  }

  backend "s3" {
    bucket = "hogehoge_bucket"
    key    = "iam-demo.tfstate"
    region = "ap-northeast-1"
  }
}

provider "aws" {
  default_tags {
    tags = {
      project = local.project_name
    }
  }
}

locals {
  project_name = "iam-demo"
}

IAMユーザ

まずはIAMユーザを作成してIAMポリシーとの関係を確認します。

AWSにアカウントを作成すると自動的にroot userが作成されますが、その権限の広さは果てしないです。通常はroot userとは別にIAMユーザを作成して、root user認証が必要なタスクのみroot userを使うようにすることがAWSでは推奨されています。

なのでとりあえずIAMユーザを作成します。(TerraformのためのIAMユーザが既にあると思いますがこの先分かりやすいのでまっさらなIAMユーザを作成しています)

account_a/iam.tf

resource "aws_iam_user" "yamada" {
  name = "yamada"
}

アクセスキーIDとシークレットアクセスキーも作成します。Terraformでアクセスキー付きのIAMユーザを作成しないのは、手間に感じたためです。

$ aws iam create-access-key --user-name yamada
{
    "AccessKey": {
        "UserName": "yamada",
        "AccessKeyId": "hogehogehoge",
        "Status": "Active",
        "SecretAccessKey": "varvarvarvar",
        "CreateDate": "2023-04-17T15:21:45+00:00"
    }
}

workdir/aws/credentials にアクセスキーを追記

~~~~~~~~~~~~~
[yamada]
aws_access_key_id=hogehogehoge
aws_secret_access_key=varvarvarvar

workdir/aws/config に追記

~~~~~~~~~~~~~
[profile yamada]
region=ap-northeast-1
output=json

アクセス許可を確認

yamadaには何もIAMポリシーがついていないので、何もできない状態です。

試しに作成したyamadaでAMIの検索を行うと、「AMIを検索する権限がない」と表示されます。

$ aws ec2 describe-images --owners amazon --profile yamada

An error occurred (UnauthorizedOperation) when calling the DescribeImages operation: You are not authorized to perform this operation.

AMIを検索するポリシーを付与してみます。AMIの検索には「ec2:DescribeImages」を許可するポリシーが必要なので、例えばAWSが用意してくれている「arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess」をyamadaに付与したり、自分でポリシーを作成して付与すれば良いです。

今回は自分でポリシーを作成して、yamadaに付与します。

account_a/iam.tf

resource "aws_iam_user" "yamada" {
  name = "yamada"
}

////////////////////
// ここから追加

resource "aws_iam_user_policy" "yamada_policy" {
  user = aws_iam_user.yamada.name
  policy = data.aws_iam_policy_document.yamada_policy.json
}

// ポリシー(インラインポリシー)
data "aws_iam_policy_document" "yamada_policy" {
  statement {
    // 許可する
    effect = "Allow"
    // ↓何を?
    actions = [
      "ec2:DescribeImages",
    ]
    // ↓何に対して?
    resources = [ "*" ] // 全てのイメージに
  }
}

yamadaにiamポリシーをアタッチする

これでyamadaはAMIの検索ができるようになっています。

$ aws ec2 describe-images --owners self amazon --profile yamada
{
    "Images": [
        {
            "Architecture": "x86_64",
            "CreationDate": "2021-07-07T06:50:20.000Z",
            "ImageId": "ami-0e02bd77bff2110f5",
            "ImageLocation": "amazon/amzn2-ami-kernel-5.10-hvm-2.0.20210701.0-x86_64-ebs",
            "ImageType": "machine",
            "Public": true
~~~~~~~~~~以下省略~~~~~~~~~~~~~~~~~~~~~~~~~~

もちろん許可していないことはできません。試しにEC2のインスタンスを検索してみます。

$ aws ec2 describe-instances --profile yamada

An error occurred (UnauthorizedOperation) when calling the DescribeInstances operation: You are not authorized to perform this operation.

たとえyamada自身に関するようなことでも、許可がなければデータにアクセスできません。例えばyamadaでyamadaのアクセスキー一覧の確認を試みてもそれはできません。

実際にやると、「yamadaにyamadaのアクセスキー一覧を表示する許可がない」と表示されます。

$ aws iam list-access-keys --user-name yamada --profile yamada

An error occurred (AccessDenied) when calling the ListAccessKeys operation: User: arn:aws:iam::hogehogehoge:user/yamada is not authorized to perform: iam:ListAccessKeys on resource: user yamada because no identity-based policy allows the iam:ListAccessKeys action

上述のように IAMユーザにIAMポリシーを付与する というのが基本となる使い方です。そしてIAMポリシーには 何に対して何が出来るのか を定義します。

IAMロール

IAMのもう一つの重要な概念がIAMロール(role)です。

IAMロールもIAMユーザと同じようにIAMポリシーを付与して使いますが、IAMユーザではできないクロスアカウントのアクセス制御が可能です。

社内にプロジェクトが3つある場合を想像してみてください。プロジェクト毎にアカウントを払い出し、それぞれに4つのIAMユーザ admin,developer,viewer,guestを作成すると合計で12のアクセスキーを管理する必要があります。

IAMロールを活用する事でアクセスキー管理を解決(軽減)することができます。

先ほど作成したyamadaをから別のアカウントのAWSリソースにアクセスしてみます。

以降、IAMユーザyamadaを作成したアカウントを「Account A(account_a)」、これからIAMロールを作成するアカウントを「Account B(account_b)」と呼ぶことにします。

準備

docker-composeにaccount_b用のTerraformを作成します。

version: '3'

services: 

  Terraform-a:
    image: hashicorp/Terraform:1.4
    entrypoint: sh
    env_file:
      - ./account_a/.env
    volumes:
      - ./account_a:/account_a
    working_dir: /account_a
    tty: true

#############↓↓追加##################
  Terraform-b:
    image: hashicorp/Terraform:1.4
    entrypoint: sh
    env_file:
      - ./account_b/.env
    volumes:
      - ./account_b:/account_b
    working_dir: /account_b
    tty: true
#############↑↑#####################

  aws:
    image: amazon/aws-cli:latest
    entrypoint: sh
    volumes:
      - ./aws:/root/.aws
    tty: true

ディレクトリにaccount_b用のコードを追加します。

  • workdir/
    • aws/
      • config
      • credentials
    • account_a
      • main.tf
      • iam.tf
      • .env
    • account_b // ←追加
      • main.tf
      • .env //
    • docker-compose.yaml

IAMロールを作成

準備ができたところでまずはIAMロールを作成してみます。

IAMロールにはポリシーを付与するだけでなく、「誰が自分自身(IAMロール)を使えるのか」をTrust Policyに定義します。(ここでいう「使う」は、AWS公式ではよく「引き受ける」と呼ばれています)

account_b/ima.tf

resource "aws_iam_role" "developer" {
  name               = "developer"
  assume_role_policy = data.aws_iam_policy_document.developer_trust_policy.json
}

// 誰が使えるのかを定義するTrust Policy
data "aws_iam_policy_document" "developer_trust_policy" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "AWS"
      // Account AのyamadaだけがこのIAMロールを使える
      identifiers = ["arn:aws:iam::account_aのID:user/yamada"]
    }
  }
}

// IAMロールに「IAMポリシー=出来る事」を付与する
resource "aws_iam_role_policy_attachment" "ec2_full" {
  role       = aws_iam_role.developer.name
  // EC2に対するすべてのことが出来る権限(VPCやサブネットの作成の可能)
  policy_arn = "arn:aws:iam::aws:policy/AmazonEC2FullAccess"
}

両アカウントの状態図

VPCを作成する

Account BにAccount Aのyamadaからのアクセスを許可するIAMロールが作成できたので、yamadaを使ってAccount BにVPCを作成します。

手順は以下の通りです。

  1. IAMロールdeveloperのarnを控える
  2. yamadaを使ってAccount Bのdeveloperを使うための認証情報を取得
  3. 「1.」で取得した認証情報をつかってAccount BにVPCを作成する

1. IAMロールdeveloperのarnを控える

developerのarnを控えておきます。

$ aws iam list-roles --query "Roles[?RoleName == 'developer']" --profile {AccountBのIAMユーザ}
または
$ (docker compose exec Terraform-b sh -c ")Terraform state show aws_iam_role.developer(")

2. yamadaを使ってAccount Bのdeveloperを使うための認証情報を取得

developerの認証情報を取得するためにはSTS(Security Token Service)に「私はAccount Aのyamadaです。Account Bのdeveloperの認証情報を下さい。」というやりとりを経て、トークンを発行してもらうことが必要です。

このやりとりを「AssumeRole」と呼びます。

さて実際にAssumeRoleしてみますが、yamadaにはAssumeRoleする権限がないと表示されます。いまのyamadaには「ec2:DescribeImages」以外のことが行えないIAMポリシーを設定しているためです。

$ aws sts assume-role --role-arn "{控えておいたAccountBのdeveloperのarn}" --role-session-name yamada-developer --profile yamada

An error occurred (AccessDenied) when calling the AssumeRole operation: User: arn:aws:iam::varvarvar:user/yamada is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::hogehgoe:role/developer

yamadaがAssumeRoleが出来るようにポリシーを編集します。

account_a/iam.tf

data "aws_iam_policy_document" "yamada_policy" {
  statement {
    effect = "Allow"
    actions = [
      "ec2:DescribeImages",
      // ↓追加
      "sts:AssumeRole"
    ]
    resources = [ "*" ]
  }
}

yamadaのiamポリシーにassumeroleをアタッチ

もう一度AssumeRoleすると今度は上手く認証情報が取得できます。

$ aws sts assume-role --role-arn "{控えておいたAccountBのdeveloperのarn}" --role-session-name yamada-developer --profile yamada
{
    "Credentials": {
        "AccessKeyId": "varvarvar",
        "SecretAccessKey": "foofoofoo",
        "SessionToken": "hogehogehogehogehogehoge",
        "Expiration": "2023-04-23T10:15:14+00:00"
    },
    "AssumedRoleUser": {
        "AssumedRoleId": "hogefoo:yamada-developer",
        "Arn": "arn:aws:sts::hogehoge:assumed-role/developer/yamada-developer"
    }
}

取得した認証情報をaws-cliで使用する設定をします。

aws/credentials

[yamada_developer]
aws_access_key_id=
aws_secret_access_key=
aws_session_token=

aws/config

[profile yamada_developer]
region=ap-northeast-1
output=json

yamada-developerでVPCの作成を試みると無事作成できました。

$ aws ec2 create-vpc --cidr-block "10.0.0.0/16" \
 --tag-specifications "ResourceType=vpc,Tags=[{Key=Name,Value=yamada_ec2}]" \
 --profile yamada_developer
{
    "Vpc": {
        "CidrBlock": "10.0.0.0/16",
        "DhcpOptionsId": "dopt-055161ee9dd0edabe",
        "State": "pending",
        "VpcId": "vpc-0ea369921e71a5a98",
        "OwnerId": Account BのID,
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

確認がとれたのでvpcを削除しつつ、yamadaでAccount Bのリソースにアクセス出来ないことを確認します。

$ sh-4.2# aws ec2 delete-vpc --vpc-id vpc-0ea369921e71a5a98 --profile yamada

An error occurred (UnauthorizedOperation) when calling the DeleteVpc operation: You are not authorized to perform this operation. Encoded authorizati...

sh-4.2# aws ec2 delete-vpc --vpc-id vpc-0ea369921e71a5a98 --profile yamada_developer
(エラーなく終了)

PassRole

IAMユーザ、IAMロールを作成してIAMユーザを作ることなく別アカウントのリソースにアクセス出来るようになりましたが、もう一つだけ確認しておくべき基本事項があります。PassRoleです。

なぜPassRoleについて知る必要があるのかを確認するために、yamada_developerでS3にアクセスするLambdaを作成してみます。

IAMロールに権限を追加

yamada_developerでS3にアクセスするLambdaを作成したいので、Account BのIAMロールであるdeveloperに「IAMロールを作成・削除する権限」と「IAMポリシーを付与する権限」、「Lambdaを作成・削除する権限」を付与します。

account_b/iam.tf

resource "aws_iam_role_policy_attachment" "create_lambda" {
  role       = aws_iam_role.developer.name
  policy_arn = aws_iam_policy.create_lambda.arn
}
// ↓のポリシーを↑で紐づけ
resource "aws_iam_policy" "create_lambda" {
  name   = "create-lambda"
  policy = data.aws_iam_policy_document.create_lambda.json
}

data "aws_iam_policy_document" "create_lambda" {
  statement {
    effect = "Allow"
    actions = [
      // IAMロールを作成・削除する権限
      "iam:CreateRole",
      "iam:DeleteRole",
      // IAMロールにポリシーを付与する権限
      "iam:AttachRolePolicy",
      // lambdaを作成・削除する権限
      "lambda:CreateFunction",
      "lambda:DeleteFunction",
    ]
    resources = ["*"]
  }
}

Lambdaを作成

IAMロールに権限が付与されたのでyamada_developerでLambdaを作成します。

Lambdaが使用するIAMロールをyamada_developerで作成します。

$ aws iam create-role \
--role-name lambda-role \
--assume-role-policy-document '{"Version": "2012-10-17","Statement": [{ "Effect": "Allow", "Principal": {"Service": "lambda.amazonaws.com"}, "Action": "sts:AssumeRole"}]}' --profile yamada_developer

{
    "Role": {
        "Path": "/",
        "RoleName": "lambda-role",
        "RoleId": "AROA47RKXOVODWLVTA2NR",
        "Arn": "arn:aws:iam::Account BのID:role/lambda-role",
        "CreateDate": "2023-04-23T13:37:45+00:00",
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Principal": {
                        // lambdaから使用されることを許可する
                        "Service": "lambda.amazonaws.com"
                    },
                    "Action": "sts:AssumeRole"
                }
            ]
        }
    }
}

作成したロールにS3FullAccessを付与します。

$ aws iam attach-role-policy --role-name lambda-role \
 --policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess \
 --profile yamada_developer

yamada-developerでlambda-roleを作成する画像

次にLambdaで動かすコードを用意します。(S3にアクセスするLambdaを作成する体ですが、コードはなんでも良いです)

exports.handler = async function(event, context) {
  return {statusCode: 200, body: JSON.stringify('hello')}
}

zip圧縮

$ zip function.zip index.js

Lambdaへのデプロイとその際に作成したIAMロールlambda-roleを付与します。するとエラーが発生します。

$ aws lambda create-function \
 --function-name role-test \
 --zip-file fileb://function.zip \
 --handler index.handler --runtime nodejs18.x \
 --role arn:aws:iam::Account BのID:role/lambda-role \
 --profile yamada_developer

An error occurred (AccessDeniedException) when calling the CreateFunction operation: User: arn:aws:sts::Account BのID:assumed-role/developer/yamada-developer is not authorized to perform: iam:PassRole on resource: arn:aws:iam::Account BのID:role/lambda-role because no identity-based policy allows the iam:PassRole action

これがPassRoleを知る必要がある理由です。PassRoleを理解していないとこのエラーに対処することが出来ないうえに、よく分からないからという理由でIAMユーザを無駄に作成してしまうことにもなりえます。(極論)

PassRoleとは

PassRoleとは読んで字の如くロールをパスすることです。今の例でいうとIAMロールのdeveloperがLambdaにlambda-roleを付与することです。

PassRoleが必要な場面は他にもあり、特にEC2の作成時にはこのエラーをよく目にするかと思います。EC2(のinstance profile)にIAMロールをアタッチして起動する際に、作業をしているユーザにPassRoleの権限がないとEC2が作成できません。

pass-role権限がない図

このエラーを解決するためには、developerへPassRoleが出来る権限を付与します。

account_b/iam.tf

resource "aws_iam_role_policy_attachment" "pass_role" {
  role       = aws_iam_role.developer.name
  policy_arn = aws_iam_policy.pass_role.arn
}

resource "aws_iam_policy" "pass_role" {
  name   = "pass-role"
  policy = data.aws_iam_policy_document.pass_role.json
}

data "aws_iam_policy_document" "pass_role" {
  statement {

    effect = "Allow"

    // 何を許可する? → iam:PassRoleを許可する
    actions = [
      "iam:PassRole",
    ]

    // 何に対して? → Lambdaに対して
    resources = ["*"]
    condition {
      test = "StringEquals"
      variable = "iam:PassedToService"

      values = [
        "lambda.amazonaws.com"
      ]
    }
  }
}

もう一度試してみると上手くいきました。

$ aws lambda create-function \
--function-name role-test \
--zip-file fileb://function.zip \
--handler index.handler \
--runtime nodejs18.x \
--role arn:aws:iam::Account BのID:role/lambda-role \
--profile yamada_developer

{
    "FunctionName": "role-test",
    "FunctionArn": "arn:aws:lambda:ap-northeast-1:Account BのID:function:role-test",
    "Runtime": "nodejs18.x",
    "Role": "arn:aws:iam::Account BのID:role/lambda-role",
    "Handler": "index.handler",
    "CodeSize": 268,
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

アカウントの管理は慎重に

これまででIAMの基本を確認しました。これでクロスアカウントにIAMユーザを作成しないことで認証情報を増やさないような運用ができると思います。今回TerraformではIAMユーザをつかってリソースの作成を行いましたが、これをIAMロールを使用したリソースの作成をするように運用したりなど、色々出来る事の幅が広がります。

また、記事中では「IAMロールを作成・付与できるポリシー」を付与したIAMロールを作成しました。PassRole権限がなくともそのロールを使用すればなんでも出来てしまうので実運用では注意が必要です。

また記事では触れていませんが、IAMユーザとIAMロールには出来る事の上限を定義する事ができたり、AssumeRoleが出来るIAMユーザの条件にMFA認証を必須とすることや、フェデレーションといってAWS外部(例:Github Actionsなど)から認証を行うことができます。

IAMユーザは何かと便利ですが、IAMロールで事足りる場合は積極的に使っていきたいと思います。

Webサイト・システムの
お悩みがある方は
お気軽にご相談ください

お問い合わせ 03-6380-6022(平日09:30~18:30)

出張またはWeb会議にて、貴社Webサイトの改善すべき点や
ご相談事項に無料で回答いたします。

無料相談・サイト診断 を詳しく見る

多くのお客様が気になる情報をまとめました、
こちらもご覧ください。