bitA Tech Blog
🧟

ゾンビバケットから学ぶARNの話

に公開

🎄この記事は bitA Tech Blog Advent Calendar の23日目の記事です。

はじめに

先日、AWS環境上のS3バケットをゾンビ化させてしまいました。
おかげで、ゾンビ化の原因となったARN(Amazonリソースネーム)について少し詳しくなりました。

本記事では、バケットをゾンビ化させてしまった一件と、それを通して学んだARNの知見について紹介します。
ゾンビというクリスマスにあまり似つかわしくない話題で恐縮ですが、お付き合いいただけますと幸いです。

ゾンビバケットとは

バケットポリシーの設定を誤った結果、誰もアクセスできなくなり、削除すらできなくなったバケットのことです。
記事の最後にも記載している通り、ルートユーザーを使えば復活させられるのですが、通常のユーザーだと、ゾンビ化させてしまったが最後、もう救いようがありません。

作りたかったバケットの要件とポリシー

今回新たに作ったS3バケットは、バケットにアクセスできるユーザーをなるべく絞りたかったため、バケットポリシーを設定することにしました。

自分の意図としては
「対象のS3バケットとそのオブジェクトについて、指定したIAMロールと、そのロールにスイッチしたセッション以外からの、あらゆる操作を拒否する」
といったポリシーを実現したく、以下のようなバケットポリシーを作成しました。(バケット名やARNはダミーです)

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "sid1",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::hogehoge",
        "arn:aws:s3:::hogehoge/*"
      ],
      "Condition": {
        "StringNotEquals": {
          "aws:username": [
              "arn:aws:iam::999999999999:user/rolename1",
              "arn:aws:iam::999999999999:user/rolename1/username2"
          ]
        }
      }
    }
  ]
}

…これで無事にゾンビバケットが爆誕します。

何がダメだったか

Conditionの記述が間違っていました。
具体的には、

  • ARNの形式が誤っている
  • StringNotEqualsに指定する値が誤っている

という2点が問題でした。

正しく記述できていれば、「特定のARN以外からのすべてのアクションを拒否する」というポリシーになるはずでした。しかし、ARNの形式が誤っていたことにより、「指定の文字列に合致するARN」がこの世に存在せず、結果として実質的にあらゆるリソースからのあらゆるアクションが拒否される、という状態になってしまったのです。

大前提、ARNの形式とは

ここで一度、ARNについて整理します。
ARNとは、AWS上の個々のリソースを一意に識別するための文字列です。

基本的には以下のような形式で表記されます。
arn:partition:service:region:account:resource-type/resource-id

最初はarn:から始まり、

要素 説明
partition リソースが置かれているパーティション(大抵の場合はaws)
service サービス名
region リソースのリージョン
account AWSアカウントID(ハイフン無し数字12桁)
resource-type リソースタイプ
resource-id リソース識別子

を順に指定していく構成となっています。
https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/reference-arns.html

この形式はあくまで基本形で、サービスやリソースのタイプによって少し変わることもあります。

IAMのARN形式について

今回取り上げるIAMのARNだと
arn:aws:iam::123456789012:user/john(例)
という風に、serviceにはiamが入り、IAMの場合にはregionが省略されます。

さらに、resource-typeの部分は、IAMユーザーを指定する場合はuserを、IAMロールの場合はroleを、IAMポリシーの場合はpolicyを入れなければなりません。

加えて、特定のIAMロールにスイッチした状態のセッションを指定したい際はresource-typeをassumed-roleとした上で、serviceの部分もiamではなくsts(AWS Security Token Serviceのこと)とする必要があります。
https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/reference_identifiers.html

上記を踏まえて、ゾンビ化の原因を見てみる

さて、ここでもう一度、上記のバケットポリシーで指定していたARNを見てみましょう。

ARNの部分を抜粋
"arn:aws:iam::999999999999:user/rolename1",
"arn:aws:iam::999999999999:user/rolename1/username2"

もうお分かりかと思いますが、今回 ①IAMロールと ②そのロールにスイッチした状態のセッションのARNを指定したかったにもかかわらず、ARNのresource-typeがいずれもuserになってしまっています。
さらに、スイッチロール後のユーザーのセッションはserviceもiamのままになっています。

この場合、正しくは

ARNの部分を抜粋
- "arn:aws:iam::999999999999:user/rolename1",
+ "arn:aws:iam::999999999999:role/rolename1",
- "arn:aws:iam::999999999999:user/rolename1/username2"
+ "arn:aws:sts::999999999999:assumed-role/rolename1/username2"

という風に指定しなければなりません。

さらに、StringNotEqualsの値も誤っていた

間違いはそれだけではありません。
再度バケットポリシーのConditionの部分を見てみましょう。

Conditionの部分を抜粋
"Condition": {
  "StringNotEquals": {
    "aws:username": [
        "arn:aws:iam::999999999999:user/rolename1",
        "arn:aws:iam::999999999999:user/rolename1/username2"
    ]
  }
}

Conditionとは、ポリシーが適用される条件を指定するための、オプションで使用できる要素です。
ここでは文字列条件演算子であるStringNotEqualsを記述し、「特定の要素以外」という条件を指定しています。
https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/reference_policies_elements_condition.html

StringNotEqualsでaws:usernameを指定する場合はユーザー名を指定する必要がありますが、問題の記述では誤ってARNを指定しています。

今回のConditionの指定で表されるのは「配列内の要素の文字列を含むユーザー名以外からのすべてのアクションを拒否する」という条件ですが、指定の文字列(=ARN)を含むユーザー名は存在しないので、結果的にこのバケットは誰からも操作できなくなります。
これがゾンビ化の原因です。

正しく指定するには、下記のようにキーをaws:PrincipalArn(ARNを指すプロパティ)としなければなりません。

Conditionの部分を抜粋
"Condition": {
  "StringNotEquals": {
-    "aws:username": [
+    "aws:PrincipalArn": [
        "arn:aws:iam::999999999999:role/rolename1",
        "arn:aws:sts::999999999999:assumed-role/rolename1/username2"
    ]
  }
}

正解はこちら

最終的に、下記のように指定することで、
「対象のS3バケットとそのオブジェクトについて、指定したIAMロールと、そのロールにスイッチしたセッション以外からの、あらゆる操作を拒否する」
という、狙い通りのポリシーが設定できました。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "sid1",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::hogehoge",
        "arn:aws:s3:::hogehoge/*"
      ],
      "Condition": {
        "StringNotEquals": {
-          "aws:username": [
+          "aws:PrincipalArn": [
-              "arn:aws:iam::999999999999:user/rolename1",
+              "arn:aws:iam::999999999999:role/rolename1",
-              "arn:aws:iam::999999999999:user/rolename1/username2"
+              "arn:aws:sts::999999999999:assumed-role/rolename1/username2"
          ]
        }
      }
    }
  ]
}

なぜこんな間違いをしたか

結論、いわゆる 「だろう運転」の結果です。

バケットポリシーを自分で一から設定するのは初めてだったので、色々な記事の情報やAIの回答を継ぎはぎして作成していました。

その際に、参考情報ではConditionをユーザー名で指定していたところを自分でいい感じにARN(これもちゃんと調べずに手で書き換えてしまった)に書き換えて、まさかゾンビを生み出してしまうとも思わず「まあこれでいけるだろう」と無邪気に設定してしまったのが原因です。

ゾンビを撃退する方法

一度操作不能になったバケットを復活させるには、ルートユーザーを使う必要があります。
ルートユーザーでサインインした後、バケットのポリシーを削除あるいは編集することで復旧できます。
なお、バケットポリシーを先に削除しない限り、たとえルートユーザーであってもバケットの操作ができないため注意が必要です。

最後に

今回の事象で学んだことをまとめました。

  • 権限周りの設定はなんとなくでやらない(当然)
  • AI出力や有志記事などの二次情報だけでなく、公式ドキュメントもきちんと参照する
    • 今回の場合、必要な情報が公式ドキュメント内でも様々なページに散らばっており、途中から憶測で突っ走ってしまったのが良くなかった
    • 途中で諦めずに公式ドキュメントをきちんと隅々まで読むべき
  • 実際に設定する前に有識者にレビューしてもらう
  • 個人開発等でレビューできる人が周りに居ない場合は、先にポリシーを絞って検証を行う
    • 例)アクションの指定をs3:*(全てのアクション)ではなく、最悪操作できなくても困らないアクションのみに絞って設定してみる、など

反省点は多い一方で、これまでなんとなくで使っていたARNについて詳細に調べる良い機会になりました。

この記事が、ポリシー設定につまずいている方やARNについて詳しく知りたい方のお役に立てれば幸いです。
そして何より、読者の皆様がゾンビを生み出すマッドサイエンティストになってしまわないことを心より願っています。

bitA Tech Blog
bitA Tech Blog

Discussion