🔐

AWSでリソースのタグ付け強制もウルトラCだった件 - タグポリシー、IAMポリシー、AWS Config required-tags

2024/05/16に公開

現在ゲートキーパー型で運用しているAWSリソース管理の権限移譲に本気で取り組んでいます。そのための中核になる技術の1つ、ABAC(Attribute-based access control)と呼ばれる、タグ(Attribute)に基づいて権限を定義する認可戦略を検証しています。ABAC権限モデル実現のためには、規定タグがリソースに付与されていることを担保する必要があります。この”タグ付け強制”がMFAの強制に続き、またまたウルトラCだったので、検証結果と併せてご紹介していきます。

Problem

冒頭で記載したようにABAC(Attribute-based access control)権限モデル実現のため、AWSリソースに規定のタグが付与されていることを担保したいです。また、私の所属する企業では1つのAWSアカウントに複数プロダクト(予算管理が別)が混在しており、コスト配分の観点からも規定タグの強制は重要なファクターです。

そもそもABAC権限モデルとは?

以下はEC2インスタンスに付与された”ManagedBy"タグの値が、IAMプリシンバル(ユーザー、ロール)の”ManagedBy"タグの値と一致する場合にのみ、インスタンスの起動、停止を許可するIAMポリシーです。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["ec2:StartInstances", "ec2:StopInstances"],
      "Resource": "arn:aws:ec2:*:*:instance/*",
      "Condition": {
        "StringEquals": {
          "aws:ResourceTag/ManagedBy": "${aws:PrincipalTag/ManagedBy}"
        }
      }
    }
  ]
}

このようにリソースに付与されたタグの基づいて動的な権限管理ができる権限モデルです。

達成したい要件を詳細化します。

  • 必須タグ: 全てのリソースには必ず以下キーのタグが付与されている必要がある。
    • CostCenter: コスト配分
    • ManagedBy: 管理主体(アクセスコントロール)
  • 必須タグは大文字、小文字を区別したい(タグポリシー使用のベストプラクティス)。
  • 必須タグ以外は任意のタグを設定して良い。

Solution

結果完全な解決には至らなかったのですが、現AWSの実装状況で有力なソリューションを紹介します。

機械的に全てのリソースにタグの強制は不可能だったため、運用でのカバーを盛り込んで、結果的にタグルールを厳守するソリューションになります。

IAMポリシーで必須タグを機械的に強制

リソースの管理を実施するIAMプリシンバルに以下の様なIAMポリシーをアタッチすることで、命名規則も併せて必須タグの付与を強制しつつ、それ以外のタグは任意で付与が可能です。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EnforceRequiredTags",
      "Effect": "Allow",
      "Action": ["iam:CreateRole", "iam:TagRole"],
      "Resource": "*",
      "Condition": {
        "StringLike": {
          "aws:RequestTag/CostCenter": "?*",
          "aws:RequestTag/ManagedBy": "?*"
        }
      }
    },
    {
      "Sid": "EnforceCostCenterTagCapitalization",
      "Effect": "Deny",
      "Action": ["iam:CreateRole", "iam:TagRole"],
      "Resource": "*",
      "Condition": {
        "ForAllValues:StringNotEquals": {
          "aws:TagKeys": ["CostCenter"]
        }
      }
    },
    {
      "Sid": "EnforceCostCenterTagCapitalization",
      "Effect": "Deny",
      "Action": ["iam:CreateRole", "iam:TagRole"],
      "Resource": "*",
      "Condition": {
        "ForAllValues:StringNotEquals": {
          "aws:TagKeys": ["ManagedBy"]
        }
      }
    },
    {
      "Sid": "DenyUnTagActionForRequiredTags",
      "Action": "iam:UnTagRole",
      "Resource": "*",
      "Effect": "Allow",
      "Condition": {
        "ForAllValues:StringNotEquals": {
          "aws:TagKeys": ["CostCenter", "ManagedBy"]
        }
      }
    }
  ]
}

こちらはIAMロールに限定したポリシーですが、必要なアクションをActionキーに追加することで拡張が可能です。

先頭のステートメント、以下のConditionだけで必須化できるんじゃないか?と思ったかもしれませんが、条件キー名は大文字と小文字が区別されないという特徴があります。(以下の例だと、例えばcostcenterCOSTCENTERが付与できてしまう)

"Condition": {
  "StringLike": {
    "aws:RequestTag/CostCenter": "?*",
    "aws:RequestTag/ManagedBy": "?*"
  }
}

そのため、aws:TagKeys条件キーで必須タグを個別に比較してあげることで、命名規則(大文字、小文字を区別)を厳守できます。

{
  "Sid": "EnforceCostCenterTagCaseSensitive",
  "Effect": "Deny",
  "Action": ["iam:CreateRole", "iam:TagRole"],
  "Resource": "*",
  "Condition": {
    "ForAllValues:StringNotEquals": {
      "aws:TagKeys": ["CostCenter"]
    }
  }
}

// その他の必須タグも同様に比較

これで粛々とポリシーを書き足していけばクリアかと思ったのですが、残念ながらAWSの全てのリソース、アクションが、タグに基づく認証をサポートしているわけではありません。つまり、aws:RequestTag/tag-keyaws:TagKeysを条件に使えないリソース、アクションがあります。アクション毎にどの条件キーを使えるかは、AWS サービスのアクション、リソース、条件キーで確認できます。

iam:CreateRoleアクションを例に取ると、aws:TagKeysや、aws:RequestTag/tag-key条件キーでサポートしていることがわかります。

一方、例えばDynamoDBはタグに基づく認証をサポートしていないことがわかります。

そのためタグに基づく認証をサポートしていないリソースの対策が必要になります。

AWS Config - required-tags マネージドルールで運用カバー

AWS Config自体の説明は割愛しますが、 AWS Config - required-tags マネージドルールで、指定したタグがリソースに設定されなかった場合、通知や自動修復アクションを実装できます。

ここでは必須タグが漏れていた場合メールで通知する方法を説明します。
Serverworksさんのブログに詳細な設定方法の説明がありますので、併せてご参照ください。

まず以下の様なConfig Ruleを作成します。required-tags マネージドルールをベースにして、Scope.ComplianceResourceTypesに対象になるリソース群を指定、InputParametersに必須タグのキー名を全て追加します。tag{n}Valueは何も設定しないことで、Nullチェックの挙動になります。

$ aws configservice describe-config-rules --config-rule-names kuritify-required-tags
{
    "ConfigRules": [
        {
            "ConfigRuleName": "kuritify-required-tags",
            "ConfigRuleArn": "arn:aws:config:ap-northeast-1:111111111111:config-rule/config-rule-k9q5to",
            "ConfigRuleId": "config-rule-k9q5to",
            "Description": "Checks whether your resources have the tags that you specify.",
            "Scope": {
                "ComplianceResourceTypes": [
                    "AWS::DynamoDB::Table"
                ]
            },
            "Source": {
                "Owner": "AWS",
                "SourceIdentifier": "REQUIRED_TAGS"
            },
            "InputParameters": "{\"tag1Key\":\"CostCenter\",\"tag2Key\":\"ManagedBy\"}",
            "ConfigRuleState": "ACTIVE",
            "EvaluationModes": [
                {
                    "Mode": "DETECTIVE"
                }
            ]
        }
    ]
}

$ aws configservice describe-config-rules --config-rule-names kuritify-required-tags | jq -r ConfigRules[0].InputParameters | jq .
{
  "tag1Key": "CostCenter",
  "tag2Key": "ManagedBy"
}

次に、EventBirdge経由で、上記のConfig Ruleに違反したリソースを検知します。ここではEmailで受信するので、SNSのトピックを作成し、Emailでサブスクリプション登録をします。そのSNSをターゲットにするEventBridgeの設定を追加します。

$ aws events describe-rule --name kuritify-required-tags
{
    "Name": "kuritify-required-tags",
    "Arn": "arn:aws:events:ap-northeast-1:111111111111:rule/kuritify-required-tags",
    "EventPattern": "{\"source\":[\"aws.config\"],\"detail-type\":[\"Config Rules Compliance Change\"],\"detail\":{\"messageType\":[\"ComplianceChangeNotification\"],\"configRuleName\":[\"kuritify-required-tags\"],\"newEvaluationResult\":{\"complianceType\":[\"NON_COMPLIANT\"]}}}",
    "State": "ENABLED",
    "EventBusName": "default",
    "CreatedBy": "111111111111"
}

$ aws events describe-rule --name kuritify-required-tags | jq -r .EventPattern | jq
{
  "source": [
    "aws.config"
  ],
  "detail-type": [
    "Config Rules Compliance Change"
  ],
  "detail": {
    "messageType": [
      "ComplianceChangeNotification"
    ],
    "configRuleName": [
      "kuritify-required-tags"
    ],
    "newEvaluationResult": {
      "complianceType": [
        "NON_COMPLIANT"
      ]
    }
  }
}

$ aws events list-targets-by-rule --rule kuritify-required-tags
{
    "Targets": [
        {
            "Id": "Id1d******************************3267",
            "Arn": "arn:aws:sns:ap-northeast-1:111111111111:kuritify-topic"
        }
    ]
}

これで必須タグが漏れているリソースを新規作成、更新した場合、SNSをサブスクライブしたメールアドレスに通知が届きます。以下は必須タグが漏れたリソースを新規作成した際に通知されるイベントの例です。

{
  "version": "0",
  "id": "a32e*************************cd06eb4",
  "detail-type": "Config Rules Compliance Change",
  "source": "aws.config",
  "account": "111111111111",
  "time": "2024-05-12T12:47:33Z",
  "region": "ap-northeast-1",
  "resources": [],
  "detail": {
    "resourceId": "rancid-table-no-tag",
    "awsRegion": "ap-northeast-1",
    "awsAccountId": "111111111111",
    "configRuleName": "kuritify-required-tags",
    "recordVersion": "1.0",
    "configRuleARN": "arn:aws:config:ap-northeast-1:111111111111:config-rule/config-rule-k9q5to",
    "messageType": "ComplianceChangeNotification",
    "newEvaluationResult": {
      "evaluationResultIdentifier": {
        "evaluationResultQualifier": {
          "configRuleName": "kuritify-required-tags",
          "resourceType": "AWS::DynamoDB::Table",
          "resourceId": "rancid-table-no-tag",
          "evaluationMode": "DETECTIVE"
        },
        "orderingTimestamp": "2024-05-12T12:47:26.950Z"
      },
      "complianceType": "NON_COMPLIANT",
      "resultRecordedTime": "2024-05-12T12:47:33.568Z",
      "configRuleInvokedTime": "2024-05-12T12:47:33.376Z"
    },
    "notificationCreationTime": "2024-05-12T12:47:33.866Z",
    "resourceType": "AWS::DynamoDB::Table"
  }
}

AWS Configのget-compliance-details-by-config-ruleAPIで、ルール違反(必須タグが漏れている)のリソース一覧を取得することも可能です。

$ aws configservice get-compliance-details-by-config-rule --config-rule-name kuritify-required-tags --compliance-types NON_COMPLIANT
{
    "EvaluationResults": [
        {
            "EvaluationResultIdentifier": {
                "EvaluationResultQualifier": {
                    "ConfigRuleName": "kuritify-required-tags",
                    "ResourceType": "AWS::DynamoDB::Table",
                    "ResourceId": "rancid-table-no-tag",
                    "EvaluationMode": "DETECTIVE"
                },
                "OrderingTimestamp": "2024-05-12T21:47:27.580000+09:00"
            },
            "ComplianceType": "NON_COMPLIANT",
            "ResultRecordedTime": "2024-05-12T21:47:33.727000+09:00",
            "ConfigRuleInvokedTime": "2024-05-12T21:47:33.585000+09:00"
        }
    ]
}

AWS Configのリソースタイムラインの機能により、違反リソースを誰がいつ操作したかを把握できます。これらの機能を利用することで、IAMポリシーで機械的にタグルールを強制できないリソースを運用対応でカバーしていくとこができます。

どちらもサポートしていないリソースは、AWS CloudTrailのイベントを拾うで運用カバー

現状この対応が必要なリソースは管理対象になっていないので、机上検証結果のみ紹介しておきます。

re:Postで紹介されていますが、AWS CloudTrailの管理イベントの記録をトリガーに、AWS Lambdaの呼び出しが可能です。タグに基づく認証をサポートしないリソースかつ、AWS Config - required-tagsもサポートしていないリソースの運用カバーが必要な場合、同リソースのタグを変更するアクションでフィルタリングし、Lambdaでタグをチェックして必須タグが漏れていたら通知する関数を作成することで、リアルタイムで通知を受けとれます。

代替案

”代替案”というタイトルからはズレますが、ここまでしてタグ強制の価値があるか否かの検討も重要です。私の状況では、ABAC権限モデル実現のためにどうしても重要な機能ではありますが、マルチアカウント環境が完備されているチーム、完全に中央管理でAWSリソース管理をしたいチーム、IaCでの開発プラクティスが浸透しているチームでは無用の長物かもしれません。AWS リソースのタグ付けのベストプラクティス - タグ付けの実装と実施といったWhitepapersも用意されているので、自身のチームの現在地と照らし合わせ、必要な要件を整理いただければと思います。

Discussion

ここからは実際に行った検証を順を追って説明して行きます。

IAMポリシーでのタグ付与強制の検証

まずはIAMポリシーだけでタグ付与の強制をできないか、IAM - タグを使用した AWS リソースへのアクセスの制御を起点に検証していきます。

以下のaws cliで--tagsオプションを切り替えながらAPIの成否を確認していきます。

$  aws iam create-role --role-name <$ROLE_NAME> --assume-role-policy-document file://trusted-policy.json --tags $TAGS_OPTION

まず以下のポリシーを検証しました。Null条件キーで必須タグを強制できるかの検証です。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "SID": "EnforceRequiredTags",
      "Effect": "Allow",
      "Action": ["iam:CreateRole", "iam:TagRole"],
      "Resoure": "*",
      "Condition": {
        "Null": {
          "aws:RequestTag/CostCenter": "false",
          "aws:RequestTag/ManagedBy": "false"
        }
      }
    }
  ]
}
ケース $TAGS_OPTION 期待 結果
タグ無し (No --tags option) ✖️ ✖️
必須タグ不足 Key=ManagedBy,Value=Rancid ✖️ ✖️
必須タグ完備 Key=ManagedBy,Value=Rancid Key=CostCenter,Value=Epitaph
必須タグ完備 + 任意タグ Key=ManagedBy,Value=Rancid Key=CostCenter,Value=Epitaph Key=Any,Value=Any
必須タグ完備だが小文字 Key=managedby,Value=Rancid Key=costcenter,Value=Epitaph ✖️
必須タグNullバリュー Key=managedby,Value= Key=costcenter,Value= ✖️ ◯️

まず、最下部の”必須タグNullバリュー”の失敗に対応します。Null条件演算子だと、必須キーの存在チェックはしてくれるものの、バリューがNullでも成功になってします。Null条件演算子をStringLike条件演算子に置き換え、ワイルドカードで1文字以上の任意の値に変更します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "SID": "EnforceRequiredTags",
      "Effect": "Allow",
      "Action": [
        "iam:CreateRole",
        "iam:TagRole"
      ],
      "Resoure": "*",
      "Condition": {
+       "StringLike": {
+         "aws:RequestTag/CostCenter": "?*",
+         "aws:RequestTag/ManagedBy": "?*"
+       }
-       "Null": {
-         "aws:RequestTag/CostCenter": "false",
-         "aws:RequestTag/ManagedBy": "false"
-       }
      }
    }
  ]
}
ケース $TAGS_OPTION 期待 結果
タグ無し (No --tags option) ✖️ ✖️
必須タグ不足 Key=ManagedBy,Value=Rancid ✖️ ✖️
必須タグ完備 Key=ManagedBy,Value=Rancid Key=CostCenter,Value=Epitaph
必須タグ完備 + 任意タグ Key=ManagedBy,Value=Rancid Key=CostCenter,Value=Epitaph Key=Any,Value=Any
必須タグ完備だが小文字 Key=managedby,Value=Rancid Key=costcenter,Value=Epitaph ✖️
必須タグNullバリュー Key=managedby,Value= Key=costcenter,Value= ✖️ ✖️

続いて、"必須タグ完備だが小文字"の対策を考えます。IAM JSON ポリシー要素Conditionに説明がありますが、条件キー名は大文字と小文字が区別されないため、必須タグキーが全て小文字でもAPIが成功します。公式ページ内の注記で、タグキーの大文字と小文字を区別したい場合は、aws:TagKeysの利用が推奨されていますので、次はaws:TagKeysと組み合わせを検証します。

aws:TagKeysの説明を引用します。

このキーを使用して、リクエスト内のタグキーとポリシーで指定したキーを比較します。ポリシーでタグを使用してアクセスを制御する場合は、aws:TagKeys 条件キーを使用して、許可されるタグキーを定義することをお勧めします。

  • 可用性 – このキーは、オペレーションがリクエストのタグを渡すサポートしている場合にのみ、リクエストコンテキストに含まれます。
  • データ型 – 文字列 (リスト)
  • 値タイプ — 複数値

1 つのリクエストに複数のタグとキーバリューのペアを含めることができるため、リクエストのコンテンツは複数の値を持つリクエストである場合があります。この場合、ForAllValues または ForAnyValue 集合演算子を使用する必要があります。

起点のチュートリアルでもaws:TagKeysでタグキーの大文字と小文字の区別をできるとの記載があるのでポリシーに追記をし検証します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "SID": "EnforceRequiredTags",
      "Effect": "Allow",
      "Action": [
        "iam:CreateRole",
        "iam:TagRole"
      ],
      "Resoure": "*",
      "Condition": {
        "StringLike": {
          "aws:RequestTag/CostCenter": "?*",
          "aws:RequestTag/ManagedBy": "?*"
        }
+       "ForAllValues:StringEquals": {
+         "aws:TagKeys": [
+             "CostCenter",
+             "ManagedBy"
+         ]
+       }
      }
    }
  ]
}
ケース $TAGS_OPTION 期待 結果
タグ無し (No --tags option) ✖️ ✖️
必須タグ不足 Key=ManagedBy,Value=Rancid ✖️ ✖️
必須タグ完備 Key=ManagedBy,Value=Rancid Key=CostCenter,Value=Epitaph
必須タグ完備 + 任意タグ Key=ManagedBy,Value=Rancid Key=CostCenter,Value=Epitaph Key=Any,Value=Any ✖️
必須タグ完備だが小文字 Key=managedby,Value=Rancid Key=costcenter,Value=Epitaph ✖️ ✖️
必須タグNullバリュー Key=managedby,Value= Key=costcenter,Value= ✖️ ✖️

大文字小文字の問題はクリアできたのですが、代わりに必須タグ完備 + 任意タグがエラーになりました。ForAllValues複数値のコンテキストキーでの説明、リクエストセットのすべてのメンバーの値が条件コンテキストキーセットのサブセットであるかどうかをテストします。とあるように、リクエストセット側に付与される可能性のある全てのタグキーを列挙する必要があります。
任意タグを列挙できればこれで問題ありませんが、本件の要件から外れます。

そのため、必須タグ毎にチェックステートメンを分割し、必須タグのみチェックする様に改修します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EnforceRequiredTags"
      "Effect": "Allow",
      "Action": [
        "iam:CreateRole",
        "iam:TagRole"
      ],
      "Resource": "*",
      "Condition": {
        "StringLike": {
          "aws:RequestTag/CostCenter": "?*",
          "aws:RequestTag/ManagedBy": "?*"
        }
      }
    },
-   {
-     "ForAllValues:StringEquals": {
-       "aws:TagKeys": [
-           "CostCenter",
-           "ManagedBy"
-       ]
-     }
-   },
+   {
+     "Sid": "EnforceCostCenterTagCapitalization"
+     "Effect": "Deny",
+     "Action": [
+       "iam:CreateRole",
+       "iam:TagRole"
+     ],
+     "Resource": "*",
+     "Condition": {
+       "ForAllValues:StringNotEquals": {
+         "aws:TagKeys": [
+           "CostCenter"
+         ]
+       }
+     }
+   },
+   {
+     "Sid": "EnforceCostCenterTagCapitalization"
+     "Effect": "Deny",
+     "Action": [
+       "iam:CreateRole",
+       "iam:TagRole"
+     ],
+     "Resource": "*",
+     "Condition": {
+       "ForAllValues:StringNotEquals": {
+         "aws:TagKeys": [
+           "ManagedBy"
+         ]
+       }
+     }
+   }
  ]
}
ケース $TAGS_OPTION 期待 結果
タグ無し (No --tags option) ✖️ ✖️
必須タグ不足 Key=ManagedBy,Value=Rancid ✖️ ✖️
必須タグ完備 Key=ManagedBy,Value=Rancid Key=CostCenter,Value=Epitaph
必須タグ完備 + 任意タグ Key=ManagedBy,Value=Rancid Key=CostCenter,Value=Epitaph Key=Any,Value=Any
必須タグ完備だが小文字 Key=managedby,Value=Rancid Key=costcenter,Value=Epitaph ✖️ ✖️
必須タグNullバリュー Key=managedby,Value= Key=costcenter,Value= ✖️ ✖️

少しポリシーが複雑ですが、期待通りの結果を得ることができました。最後に必須タグを外されない様にiam:UnTagRole"アクションを禁止するステートメントを追加して完成です。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EnforceRequiredTags",
      "Effect": "Allow",
      "Action": [
        "iam:CreateRole",
        "iam:TagRole"
      ],
      "Resource": "*",
      "Condition": {
        "StringLike": {
          "aws:RequestTag/CostCenter": "?*",
          "aws:RequestTag/ManagedBy": "?*"
        }
      }
    },
    {
      "Sid": "EnforceCostCenterTagCapitalization",
      "Effect": "Deny",
      "Action": [
        "iam:CreateRole",
        "iam:TagRole"
      ],
      "Resource": "*",
      "Condition": {
        "ForAllValues:StringNotEquals": {
          "aws:TagKeys": [
            "CostCenter"
          ]
        }
      }
    },
    {
      "Sid": "EnforceCostCenterTagCapitalization",
      "Effect": "Deny",
      "Action": [
        "iam:CreateRole",
        "iam:TagRole"
      ],
      "Resource": "*",
      "Condition": {
        "ForAllValues:StringNotEquals": {
          "aws:TagKeys": [
            "ManagedBy"
          ]
        }
      }
    },
+   {
+     "Sid": "DenyUnTagActionForRequiredTags",
+     "Action": "iam:UnTagRole",
+     "Resource": "*",
+     "Effect": "Allow",
+     "Condition": {
+       "ForAllValues:StringNotEquals": {
+         "aws:TagKeys": [
+           "CostCenter",
+           "ManagedBy"
+         ]
+       }
+     }
+   }
  ]
}

改めてaws:TagKeys説明の、可用性 – このキーは、オペレーションがリクエストのタグを渡すサポートしている場合にのみ、リクエストコンテキストに含まれます。の一文を確認していきます。これはAWSの全てのサービス、アクションが、タグに基づく認証をサポートしているわけではないということです。これはaws:RequestTag/tag-keyも同様です。

アクション毎にどの条件キーをサポートしているかどうかは、AWS サービスのアクション、リソース、条件キーで確認できます。

具体例を挙げると、ここまで動作検証してきたiam:CreateRoleアクションは、aws:tagKeysや、aws:RequestTag/tag-key条件キーをサポートしています。

一方でDynamoDBはタグに基づく認証をサポートしていないことがわかります。

以下の様にdynamodb:CreateTableアクションを追加してみます。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "SID": "EnforceRequiredTags",
      "Effect": "Allow",
      "Action": [
        "iam:CreateRole",
        "iam:TagRole",
+       "dynamodb:CreateTable",
+       "dynamodb:TagResource"
      ],
      "Resoure": "*",
      "Condition": {
        "StringLike": {
          "aws:RequestTag/CostCenter": "?*",
          "aws:RequestTag/ManagedBy": "?*"
        }
      }
    },
    {
      "Sid": "EnforceCostCenterTagCapitalization",
      "Effect": "Deny",
      "Action": [
        "iam:CreateRole",
        "iam:TagRole",
+       "dynamodb:CreateTable",
+       "dynamodb:TagResource"

      ],
      "Resource": "*",
      "Condition": {
        "ForAllValues:StringNotEquals": {
          "aws:TagKeys": [
            "CostCenter"
          ]
        }
      }
    },
    {
      "Sid": "EnforceCostCenterTagCapitalization",
      "Effect": "Deny",
      "Action": [
        "iam:CreateRole",
        "iam:TagRole",
+       "dynamodb:CreateTable",
+       "dynamodb:TagResource"

      ],
      "Resource": "*",
      "Condition": {
        "ForAllValues:StringNotEquals": {
          "aws:TagKeys": [
            "ManagedBy"
          ]
        }
      }
    }
  ]
}

タグに基づく認証をサポートしていないアクションでは、リクエストコンテキストにリクエストタグが含まれてこないため、正しいタグを付与していてもエラーになってしまいます。

$ aws dynamodb create-table --table-name rancid-table \
   --attribute-definition AttributeName=pk,AttributeType=S \
   --key-schema  AttributeName=pk,KeyType=HASH \
   --billing-mode PAY_PER_REQUEST  \
   --tags Key=ManagedBy,Value=Rancid Key=CostCenter,Value=Epitap

An error occurred (AccessDeniedException) when calling the CreateTable operation: User: arn:aws:sts::111111111111:assumed-role/yyyy/zzzz is not authorized to perform: dynamodb:CreateTable on resource: arn:aws:dynamodb:ap-northeast-1:1111111111:table/rancid-table because no identity-based policy allows the dynamodb:CreateTable action

タグに基づく認証をサポートしていないリソース、アクションのための追加の対応が必要なことが判明したので、対策を検討していきます。

Organizationタグポリシーでタグの強制の検証

いくつか記事を漁ってみたところ、以下のAWS Blogに当たりました。

双方ともAWS Organizationのタグポリシーでタグの命名規則を厳守させ、AWS OrganizationのSCPでタグの付与を必須化するというソリューションを紹介しています。

タグポリシーにより大文字と小文字の区別を強制できそうなので、DynamoDBのAPIで検証していきます。

タグポリシーの構文と例 - 組織全体のタグキーの大文字小文字取り扱いの定義を参考に、以下のタグポリシーを検証していきます。CostCenterenforced_fordynamodb:*を指定、ManagedByenforced_forを未指定にしました。

{
  "tags": {
    "CostCenter": {
      "tag_key": {
        "@@assign": "CostCenter",
        "@@operators_allowed_for_child_policies": ["@@none"]
      },
      "enforced_for": {
        "@@assign": ["dynamodb:*"]
      }
    },
    "ManagedBy": {
      "tag_key": {
        "@@assign": "ManagedBy",
        "@@operators_allowed_for_child_policies": ["@@none"]
      }
    }
  }
}

上記タグポリシーがアタッチされたアカウントで、前章同様aws cliで--tagsオプションを切り替えながらAPIの成否を確認していきます。

$ aws dynamodb create-table --table-name <$TABLE_NAME> \
   --attribute-definition AttributeName=pk,AttributeType=S \
   --key-schema  AttributeName=pk,KeyType=HASH \
   --billing-mode PAY_PER_REQUEST  \
   --tags $TAGS_OPTION
ケース $TAGS_OPTION 結果
タグ無し (No --tags option)
必須タグ完備 Key=ManagedBy,Value=Rancid Key=CostCenter,Value=Epitaph
必須タグ完備 + 任意タグ Key=ManagedBy,Value=Rancid Key=CostCenter,Value=Epitaph Key=Any,Value=Any
必須タグ完備だがenforced_for有りタグ小文字 Key=ManagedBy,Value=Rancid Key=costcenter,Value=Epitaph ✖️
必須タグ完備だがenforced_for無しタグ小文字 Key=managedby,Value=Rancid Key=CostCenter,Value=Epitaph
必須タグの一部にtypoあり Key=Managedbyy,Value=Rancid Key=CostCenter,Value=Epitaph

注目点として、タグ無しでもcreate-tableが成功する点です。以下の様にtag_valueキーでタグ値をワイルドーカードにしても、特定の値を指定しても、タグ無しでcreate-tableは成功します。

{
  "tags": {
    "CostCenter": {
      "tag_key": {
        "@@assign": "CostCenter",
        "@@operators_allowed_for_child_policies": [
          "@@none"
        ]
      },
+     "tag_value": {
+       "@@assign": ["*"]
+     },
      "enforced_for": {
        "@@assign": [
          "dynamodb:*"
        ]
      }
    },
    "ManagedBy": {
      "tag_key": {
        "@@assign": "ManagedBy",
        "@@operators_allowed_for_child_policies": [
          "@@none"
        ]
      }
    }
  }
}

これはタグポリシー - 強制でも注記されています。タグが空の場合はこのチェックは実行されません。

How to create and enforce your tagging strategy for more granular cost visibility
AWS Blogでは、このタグポリシーと以下の様な必須タグのNullチェックを組み合わせるソリューションを紹介しています。

{
  "SID": "DenyIAMROleCreation",
  "Effect": "Deny",
  "Action": ["iam:CreateRole", "iam:TagRole"],
  "Resoure": "*",
  "Condition": {
    "Null": {
      "aws:RequestTag/CostCenter": "false",
      "aws:RequestTag/ManagedBy": "false"
    }
  }
}

前章で記載した様に、aws:RequestTag/tags-keyはタグに基づく認証をサポートしているアクションでのみ利用可能です。そのため、DynamoDBでは利用できないソリューションになります。かつタグポリシーをサポートするリソースも限定的です。全てのリソースが、タグポリシーと、タグに基づく認証をサポートしてくれれば完璧なソリューションなのですが、現段階での導入はあまり意味のない状況です。うーん。惜しい!

AWS Configによる運用カバー

タグポリシーでは対策ができないため、タグに基づく認証をサポートしていないリソースについては運用でカバーしていきます。詳細はSoltutionをご確認ください。

まとめ

以上AWSでタグを強制する方法について紹介させていただきました。必要な技術要素は揃っているものの、サービス毎に実装状況がまちまちなので非常に歯がゆい状況です。IAMユーザーのSCPによるMFA強制に続き歯切れの悪いソリューションの紹介になってしまいましたが、同じ課題をお持ちの方のお役に立てていれば幸いです。

Discussion