病みつきエンジニアブログ

機械学習、Python、Scala、JavaScript、などなど

AWS IAMによる権限設定のハマりどころと、効率的なデバッグ方法

Amazon Web ServiceのIAM(Identity and Access Management)は、AWSの各種サービスに対してのアクセス制御を(結構細かく)設定するためのシステムです。

ただ、いくつか掛けられる制約にも制限があり、いろいろハマるところがあったので、メモを。

シナリオ

Jenkins経由で、特定のAMIからのみ、EC2インスタンスを一時的に立ち上げたり(run-instances)、消したりしたい(terminate-instances)。
停止(stop-instances)したり再開(start-instances)したり、というライフサイクルではない。
誤って全然関係ないインスタンスを消せないように制約をつけたいし、関係ないAMIから立ちあげられないように制限したい。
ついでにインスタンスタイプにしぼりたい。
また、安全のため特定のIPからのみアクセスできるようにしたい。

このようなシナリオでも、一応それっぽい権限で設定することができます。

IAMポリシーファイルの説明

IAMのポリシーファイルは、下記のような感じです。

{
  "Version": "2012-10-17", // バージョンは2012-10-17で固定
  "Statement": [
    {
      "Effect": "Allow",  // 許可するのか拒否するのか。デフォルトは全て"Deny"なので、"Allow"を記載していく感じ
      "Action": [ "ec2:TerminateInstances" ],  // 何のアクションを許可|拒否するのか。
      "Resource": [ "arn:aws:ec2:ap-northeast-1:1234567890:instance/*" ],  // そのアクションはどのリソース(インスタンスとかセキュリティーグループとか)へアクセス可能か
      "Condition": {  // どのような条件下でのみ、このStatementが有効か
        "StringEquals": { "ec2:InstanceType": "t2.micro" }
      }
    }
  ]
}

Statementが配列なので、いっぱい増やしていく感じ。

リソース条件はアスタリスク( "*" とか "arn:aws:ec2:ap-northeast-1:1234567890:instance/*" )で指定することもできるが、リソースを絞り込めば「そのリソースにしかアクセスできない」という状態を担保できる( "arn:aws:ec2:ap-northeast-1:1234567890:instance/id-hogehoge" )。

リソース指定できるアクションに制約がある

リソース指定によってIAMに制約をかけられる・・・と思いきや、全てのアクションがリソース指定できるわけではありません

特に、DescribeInstancesなどのGET系はだいたいダメで、他にもCreateTagsもダメです。その一覧はこちら

下記のようなresource指定してしまうと、そのStatementは無効になり、その操作は許可されなくなってしまいます。 リソースに指定できるのは "*" だけです。

{
  "Effect": "Allow",
  "Action": "ec2:DescribeInstances",
  "Resource": [ "arn:aws:ec2:ap-northeast-1:1234567890:instance/*" ], // NGな例
}

アクションとリソースと条件の組み合わせに制約がある

ここに書いてある通りですが、アクションごとに指定できるリソースに種類があり、リソースごとに指定できる条件キーが決まっています。

例えば、TerminateInstancesのアクションは、インスタンスIDに関するリソースのみ指定できます。 RunInstancesはイメージ、インスタンス、キーペア、etc。。。が指定できます。逆に、指定しないとそのリソースを使うことはできません。例えば、既存のキーペアを使ってインスタンスを作成したいなら、キーペアに対するリソース指定が必要になります。

RunInstancesのように複数のリソース指定をする場合に注意が必要なのが、複数のリソースで設定できる条件キーが異なることです。 例えば、イメージには「InstanceType」を指定できますが、キーペアには「Region」しか指定できません。 そのため、次のような設定をすると、「このキーペアにアクセスできないよ><」って言われます。

{
  "Effect": "Allow",
  "Action": "ec2:RunInstances",
  "Resource": [
    "arn:aws:ec2:ap-northeast-1:1234567890:instance/*",
    "arn:aws:ec2:ap-northeast-1:1234567890:key-pair/test-key-pair"
  ],
  "Condition": {
    "StringEquals": {
      "ec2:InstanceType": "t2.micro"
    }
  }
}

この場合は、おとなしく2つのStatementに分割します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "ec2:RunInstances",
      "Resource": [
        "arn:aws:ec2:ap-northeast-1:1234567890:instance/*"
      ],
      "Condition": {
        "StringEquals": {
          "ec2:InstanceType": "t2.micro"
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": "ec2:RunInstances",
      "Resource": [
        "arn:aws:ec2:ap-northeast-1:1234567890:key-pair/test-key-pair",
        // その他のリソース許可の指定
      ]
    }
  ]
}

このシナリオを「リソースタグ」でクリアするのは厳しい

AWSのブログにて言及されていますが、リソースタグを活用すれば「特定のAMIから、特定のタグを設定したインスタンスを作成し、特定のタグのインスタンスのみ削除できる」ということができそうです(まぁできないんですけど)。 作るStatementとしては、

  • 特定のAMIからのみ run-instances できるStatement
  • 特定のResourceTagを持つインスタンスのみ terminate-instances できるStatement

の2つを作る、という感じ。

しかし、 run-instances する際にResourceTagは設定できず(例えばインスタンス名とか)、 create-tags しないといけないのですが、CreateTagsアクションにはリソース条件などが設定できないのです(=全許可しかできない)。ブログでもコメント欄で突っ込まれています。

create / terminate のライフサイクルではなく、start / stop のライフサイクルであれば、この方式は良さそうです。ただ今回の場合は、インスタンスは消し去りたかったので、この方式は取れませんでした。

IP制御もできるが、IPはグローバルIP

ec2に限らず、「どこのIPからリクエストがあったか?」を条件とできる、 aws:SourceIp という条件キーもあります。指定する場合は、一番最初にDenyしておくと良いです。

{
  "Effect": "Deny",
  "Action": "*",
  "Resource": "*",
  "Condition": {
      "NotIpAddress": {
          "aws:SourceIp": "123.45.67.89"
      }
  }
}

一点注意点なのが、IPはグローバルIPとして展開されます。なので、EC2インスタンス上からのみ「俺をTerminateしてくれ」という処理をできるように制限したい場合、グローバルIPが事前にわかっていないといけません。

IAMPolicy Simulatorが使いづらい

IAM Policy Simulatorは正直使いづらいです。条件に合致しないときには、「何も一致しませんでした」としか言われません。何がダメだったのかもわからないです。

そこで、AWS CLIから実際にdry-runすると良いです。

aws ec2 run-instances --dry-run --image-id=ami-abcdefg --count=1 --instance-type t2.micro

こうすると、権限が不足してれば、エンコードされたエラーメッセージが出てきます。エラーメッセージのデコードは Management Consoleの権限不足エラーをデコードする | Developers.IO を参考に、resourceという項目を見ると良いです。このresourceに対して、適切な条件が設定されていない、ということがわかります。

zshの場合は、こんな感じで関数作るとデバッグが楽です(bashは知らん)。

function sts() {
  aws sts decode-authorization-message --encoded-message $1 | jq -r ".DecodedMessage" | jq -c ".context.resource"
}

sts "エンコードされたエラーメッセージ"

最終的に取った手段

結局、うまくTerminateInstancesの条件を指定できないため、TerminateInstancesの権限を与えるのはやめました。 代わりに、次の手段を取りました。

  • EC2の作成時、shutdown時の動作を「terminate(削除)」になるようにした
  • AWS CLI経由で terminate-instances するのをやめ、EC2インスタンス内で shutdown -h now した

shutdown時の動作をterminateにするには、

aws ec2 run-instances --image-id=ami-abcdefg --count=1 --instance-type t2.micro --instance-initiated-shutdown-behavior=terminate

というように、「 --instance-initiated-shutdown-behavior=terminate 」をつけます。こうすれば、terminate-instances しなくても、shutdownするだけで同じことができます。

最終的なポリシーファイルは、下記のような感じ。思い出しながら書いてるからいろいろ間違っているかもしれんけど。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "NotIpAddress": {
          "aws:SourceIp": "123.45.67.89"
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": "ec2:RunInstances",
      "Resource": "arn:aws:ec2:ap-northeast-1:1234567890:instance/*",
      "Condition": {
        "StringEquals": {
          "ec2:InstanceType": "t2.micro"
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": "ec2:RunInstances",
      "Resource": [
        "arn:aws:ec2:ap-northeast-1::image/ami-abcdefg",
        "arn:aws:ec2:ap-northeast-1:1234567890:security-group/*",
        "arn:aws:ec2:ap-northeast-1:1234567890:network-interface/*",
        "arn:aws:ec2:ap-northeast-1:1234567890:subnet/*",
        "arn:aws:ec2:ap-northeast-1:1234567890:volume/*",
        "arn:aws:ec2:ap-northeast-1:1234567890:key-pair/test-key-pair"
      ]
    }
  ]
}