🔐

IAMユーザーのMFA強制がウルトラCだった件 - IAMポリシー Deep Dive

2024/04/14に公開
2

AWSアカウントのIAMユーザー(人間のアクセスのみ)にMFA認証を強制させるのに悪戦苦闘したので、奮闘記と併せIAMポリシーのDeep Diveしていきます。SCPでサクッと実現できるんじゃない?の思ってるそこのあなた。私もそう思ってましたよ!

CCoEやプラットフォームチーム所属でAWSセキュリティ周りをみてる方はもちろん、AWSをより深く理解したい方にも有益な情報になればと思っています。

Problem

私が所属する企業のAWS環境はAWS Control Towerをベースにマルチアカウント構成をとっています。IAMでのセキュリティのペストプラクティスに則り、AWS IAM アイデンティティセンター (AWS Single Sign-On の後継)経由で社内のIdP(Microsoft Entra ID)とのSSO連携を実現しています。

SSO連携ユーザーはIdP側でMFA認証されるので問題ないのですが、例えば受託先パートナー企業のユーザーを全てIdPに登録するのも限界があり、一部にはIAMユーザーを直接発行する方針になっています。そのIAMで直接管理するユーザーにMFAを強制したいというのが本件の背景になります。

前置きが長くなりましたが、機能要件はAWSアカウントレベルで、IAMユーザーのアクセスにMFAを強制するになります。

要件をもう少しブレイクダウンし、IAMのプリンシパルを基にアクセスパターン毎のMFAの強制の有無を定義します。

アクセスパターン MFA強制 説明
人間ユーザー 🔐 人間によるマネジメントコンソールへのアクセス。
人間ユーザーのアクセスキー 🔐 人間ユーザーがアクセスキーを発行し、プログラマティカルにアクセス。正確には、LoginProfileが設定されているIAMユーザーのアクセスキー経由のアクセス。
マシンユーザーのアクセスキー 例えばオンプレサーバーのバッチ処理からのアクセスなど人間を介さないアクセス。 LoginProfileが設定されていないIAMユーザーのアクセスキー経由のアクセス。
ワークロード(IAMロール) EC2にアタッチするAWSサービスロールなど、AWSサービスがユーザーに代わってするアクセス。
外部で認証されたユーザー 🔐 SSO連携で認証されたアクセス。これまで通りIdP側でのMFA認証で動作する必要がある。

Solution

マルチアカウント環境限定になりますが、AWS Organizationの機能であるSCP(Service Control Policy)で実現します。完成したSCPはこちらです。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EnforceMFAOnHumanIAMUser",
      "Effect": "Deny",
      "NotAction": [
        "iam:CreateVirtualMFADevice",
        "iam:EnableMFADevice",
        "iam:ChangePassword",
        "iam:GetUser",
        "iam:GetLoginProfile",
        "iam:ListMFADevices",
        "iam:ListVirtualMFADevices",
        "iam:ResyncMFADevice",
        "iam:DeleteVirtualMFADevice",
        "sts:GetSessionToken",
        "ecr:BatchCheckLayerAvailability",
        "ecr:initiateLayerUpload",
        "ecr:UploadLayerPart",
        "ecr:CompleteLayerUpload",
        "ecr:PutImage",
        "ecr:BatchGetImage",
        "ecr:GetDownloadUrlForLayer",
        "codeartifact:PublishPackageVersion",
        "codeartifact:ReadFromRepository"
      ],
      "Resource": "*",
      "Condition": {
        "StringLike": {
          "aws:PrincipalArn": ["arn:aws:iam::*:user/*"]
        },
        "Bool": {
          "aws:MultiFactorAuthPresent": "false",
          "aws:ViaAWSService": "false"
        }
      }
    }
  ]
}

詳細は事項で説明しますが、ポイントだけ箇条書きします。

  • 人間ユーザーは自身のMFA認証をするまで、MFAの設定、パスワード変更以外のアクションは禁止。
    • (MFA設定後IAMユーザーは再ログインをする必要あり。)
  • Amazon ECR、AWS CodeArtifactへのアクセスは例外ケースになるため、一部アクションはMFA認証の有無に関わらず許可。

ただし、人間ユーザーのーアクセスキーマシンユーザーのアクセスキーをSCP(IAMポリシー)上での区別が不可能だったので、双方MFAの強制は”無し”となります。結果アクセスパターン毎のMFAの強制は以下になりました。

アクセスパターン MFA強制 備考
人間ユーザー 🔐 一部アクションはMFA認証無しで可能
人間ユーザーのアクセスキー
マシンユーザーのアクセスキー
ワークロード(IAMロール)
外部で認証されたユーザー 🔐 IdP側でのMFA認証

代替案

一応は人間ユーザーにMFAの強制はできたものの、漏洩のリスクが高い人間ユーザーのアクセスキーにはMFAを強制できないなんとも中途半端な状態になっています。加えて以下の懸念点があります。

  • ECR、CodeArtifactへのアクセスと類似の例外が今後も発生する可能性があり、AWSとのイタチゴッコ。
  • SIEM(Security Information and Event Management)などのCloud Trailのログ分析ソリューションでノイズになる。

前者は次章で詳しく説明していきますが、後者のSIEMのノイズについても重要なポイントだと考えています。SIEMの有名どころでいうとSplunk、AWSでいうと、SIEM on Amazon OpenSearch ServiceCloudTrail Lakeがありますが、それぞれOOTBで利用できるCloudTrailの分析ダッシュボードが提供されており、AccessDeniedを検出するクエリとパネルが用意されています。

CloudTrail LakeのTop access denied eventsパネルのクエリを眺めてみます。条件はAccessDeniedUnauthorizedといった幅広なテキストマッチングのため、SCPで想定通りに拒否したアクションも対象になってきます。

SELECT
    concat(
        replace(eventSource, '.amazonaws.com'),
        ':',
        eventName
    ) as event,
    count(*) as eventCount
FROM
    <$TRAIL_ID>
WHERE
    eventTime > '2024-03-31T08:54:44.584Z'
    AND eventTime < '2024-04-01T08:54:44.584Z'
    AND (
        errorCode = 'AccessDenied'
        OR regexp_like(errorCode, 'Unauthorized')
    )
    AND eventCategory = 'Management'
GROUP BY
    eventSource,
    eventName
ORDER BY
    eventCount DESC
LIMIT
    100

初回ログインからMFAを設定するまでに辿るマネジメントコンソールの導線で、複数のAccessDeniedの発生(例えばIAMコンソールのIAM Dashboard経由で実行されるiam:GetAccountSummary)が多発する可能性があります。

諸々鑑みると、SCPによる強制は見送り、代わりにMFA未設定のIAMユーザーをリアルタイムで検出する仕組みを導入する方が筋がいいのではと思っています。AWS SecurityHubでは[IAM.5] コンソールパスワードを使用するすべての IAM ユーザーに対して MFA を有効にする必要がありますというデフォルトコントロールも用意されているため手軽に実現が可能です。

または、SCPではなく各AWSアカウントのIAMポリシーで対象スコープを絞ってMFAを強制するなども有効な手段かと思います。IAM チュートリアル: ユーザーに自分の認証情報および MFA 設定を許可するで説明があります。

Discussion

最終的に辿り着いたSCPを解剖しつつ、IAMアクセス、IAMポリシーのDeep Diveをしていきます。

前提としてSCPはAWS Organizationのデフォルトである拒否リスト戦略を取っています。AWS Control Towerでも拒否リスト戦略が採用されているため、カスタムSCPを構築する際はDeny(拒否)アクションを積み上げていくことになります。許可リスト戦略を採用できる環境であれば、違うポリシーでも実現できるかと思います。

IAMポリシーの評価論理

まずIAMポリシーがどの様に評価されるかを簡単に説明します。詳細はポリシーの評価論理を確認いただければと思いますが、ざっくりまとめると以下の様になります。

  1. プリンシパル(IAMユーザーなど)認証する。
  2. リクエストコンテキストを収集し、適用するポリシーを決定する。
  3. ポリシーを評価し認可を実施する。

2番のリクエストコンテキストは以下の様に説明されています。

リクエストコンテキストの処理

AWS はリクエストを処理して、以下の情報をリクエストコンテキスト内に取り込みます。

  • アクション (またはオペレーション) – プリンシパルが実行するアクションまたはオペレーション。
  • リソース – アクションまたはオペレーションを実行する対象の AWS リソースオブジェクト。
  • プリンシパル – リクエストの送信元のユーザー、ロール、フェデレーティッドユーザー、またはアプリケーション。プリンシパルに関する情報には、そのプリンシパルに関連付けられたポリシーが含まれます。
  • 環境データ – IP アドレス、ユーザーエージェント、SSL 有効化ステータス、または時刻に関する情報。
  • リソースデータ – リクエストされているリソースに関連するデータ。これには、DynamoDB テーブル名、Amazon EC2 インスタンスのタグなどの情報が含まれる場合があります。
    次に、AWS は以上の情報を使用してリクエストコンテキストに適用するポリシーを見つけます。

実際に取集されたリクエストコンテキストの中身はCloudTrailイベントで確認できます。

以下のCloudTrail管理イベントは人間ユーザーがマネジメントコンソール経由でs3:ListBucketsアクションを実施した例です。例えば環境データのIPアドレスは、sourceIPAddressフィールドに格納されていることがわかります。

{
  "eventVersion": "1.09",
  "userIdentity": {
    "type": "IAMUser",
    "principalId": "*******",
    "arn": "arn:aws:iam::111111111111:user/kuritify",
    "accountId": "111111111111",
    "accessKeyId": "*******",
    "userName": "kuritify",
    "sessionContext": {
      "attributes": {
        "creationDate": "2024-03-22T08:17:38Z",
        "mfaAuthenticated": "false"
      }
    }
  },
  "eventTime": "2024-03-22T09:08:14Z",
  "eventSource": "s3.amazonaws.com",
  "eventName": "ListBuckets",
  "awsRegion": "ap-northeast-1",
  "sourceIPAddress": "**.**.**.**",
  "userAgent": "[S3Console/0.4, aws-internal/3 aws-sdk-java/1.12.488 Linux/5.10.210-178.855.amzn2int.x86_64 OpenJDK_64-Bit_Server_VM/25.372-b08 java/1.8.0_372 vendor/Oracle_Corporation cfg/retry-mode/standard]",
  "requestParameters": {
    "Host": "s3.ap-northeast-1.amazonaws.com"
  },
  "responseElements": null,
  "additionalEventData": {
    "SignatureVersion": "SigV4",
    "CipherSuite": "ECDHE-RSA-AES128-GCM-SHA256",
    "bytesTransferredIn": 0,
    "AuthenticationMethod": "AuthHeader",
    "x-amz-id-2": "************",
    "bytesTransferredOut": 373
  },
  "requestID": "*****************",
  "eventID": "*****************",
  "readOnly": true,
  "eventType": "AwsApiCall",
  "managementEvent": true,
  "recipientAccountId": "111111111111",
  "vpcEndpointId": "vpce-****",
  "eventCategory": "Management",
  "tlsDetails": {
    "tlsVersion": "TLSv1.2",
    "cipherSuite": "ECDHE-RSA-AES128-GCM-SHA256",
    "clientProvidedHostHeader": "s3.ap-northeast-1.amazonaws.com"
  }
}

このリクエストコンテキストにどのような条件(Condition)のSCP(IAMポリシー)を書けば要件を達成できるかを試行錯誤していく作業となります。最終SCPのCondition句を再掲しますが、このCondition句に辿り着くまでの検証内容を順に振り返っていきます。

"Condition": {
  "Bool": {
    "aws:MultiFactorAuthPresent": "false"
    "aws:ViaAWSService": "false",
  },
  "StringLike": {
    "aws:PrincipalArn": [
      "arn:aws:iam::*:user/*"
    ]
  },
}

人間ユーザーのアクセスキーとマシンユーザーのアクセスキーをーを区別する方法

AWSグローバル条件キーを熟読し調査を重ねましたが、これらの区別はできないことがわかりました。人間ユーザーと、マシンユーザーの差はLoginProfileの有無でしか判断できず、リクエストコンテキストに含まれるデータではないためです。

この時点でアクセスキー経由のアクセスにはMFAを強制しない(できない)意思決定を行いました。これ以降は人間ユーザーのアクセス(マネジメントコンソールアクセス)へのMFAを強制する方法の探索となります。

MFAを強制する方法

次にMFAの強制方法を調査しました。MFA強制を実現する肝となるのがaws:MultiFactorAuthPresent条件キーで、リクエストコンテキストではuserIdentity.sessionContext.mfaAuthenticatedフィールドのBoolean値です。

MFA認証されたリクエストにはtrueが設定されるのが原則ですが、なかなかに複雑な振る舞いをします。

  • 一時的な認証情報を使用してリクエストを行う場合にのみリクエストコンテキストに含まれます。
    • 長期的な認証情報(アクセスキー経由のCLI、SDKのアクセス)でのアクセスはリクエストコンテキストに含まれませんが、人間ユーザーのId/Passwordで認証したマネジメントコンソールアクセスでは含まれます。マネジメントコンソールの裏側ではSTS経由で取得した一時的な認証情報でアクセスするためです。
    • 長期的な認証情報(アクセスキー経由のCLI、SDKのアクセス)であっても、例えばAssumeRoleしてからアクセスする場合は、一時的な認証情報になるためリクエストコンテキストに含まれてきます。
  • MFA認証済みのリクエストであっても、AWSサービスがサービスロール経由で別のAWSサービスを呼び出すケースでは、同サービスロール経由のアクセスはfalseでリクエストコンテキストに含まれます。
    • 例えばCloud Formationが、サービスロール経由でS3バケットを作成する場合、Cloud FormationにリクエストしたIAMユーザーのMFA認証の有無に関わらず、s3:CreateBucketアクションの aws:MultiFactorAuthPresentfalseでリクエストコンテキストに含まれます。
  • (ご想像の通り)例えばLambda経由の別リソースへのアクセスは、lambdaのサービスロール経由(一時的な認証情報)のため、aws:MultiFactorAuthPresentfalseでリクエストコンテキストに含まれます。

表形式でまとめると以下の様になります。

アクセスパターン aws:MultiFactorAuthPresent
MFA認証済みの人間ユーザー(※1) true
MFA認証済みのアクセスキー(※2) true
MFA認証済みの人間ユーザーがAssumeRole true
MFA認証済みのアクセスキーがAssumeRole true
MFA認証していない人間ユーザー(※1) false
MFA認証していないアクセスキー キーが含まれない
MFA認証していない人間ユーザーがAssumeRole false
MFA認証していないアクセスキーがAssumeRole false
AWSサービス経由(Cloud Formationのサービスロールなど) false
AWSサービスロールのアクセス(Lambdaのサービスロールなど) false
IdP側でMFA認証済みの外部認証ユーザー false
  • ※1: cloud shell経由も同じ振る舞い
  • ※2: $ aws sts get-session-token --serial-number <$ARN_OF_THE_MFA_DEVICE> --token-code <$CODE_FROM_TOKEN> ...

1点補足するとIdP側でMFA認証済みの外部認証ユーザーfalseになる点です。MFA認証はAWSの外で(IdP側で)行われていることなので当然といえば当然かもしれませんが、私個人が混乱したポイントだったため強調しておきます。

aws:MultiFactorAuthPresentにも例が記載されている様に、以下の条件で人間ユーザーにはMFA認証が必須、かつアクセスキーにはMFA認証の強制をしないが実現できます。

{
  "Condition": {
    "Bool": {
      "aws:MultiFactorAuthPresent": "false"
    }
  }
}

Bool条件演算子は、条件で指定したキーがリクエストコンテキストに存在しない場合、条件要素は false と評価されます。という振る舞いをするので、リクエストコンテキスにキーが含まれていないアクセスキーでのアクセス(唯一の長期的な認証情報でのアクセス)は必ずfalse評価になり、リクエストコンテキストにキーが含まれるそれ以外のアクセスは、aws:MultiFactorAuthPresentがtrueなのか、falseなのかが評価されます。

人間ユーザーにMFAは強制できたものの、AssumeRole、サービスリンクロール、サービスロールのアクセスまでMFAを強制することになってしまうので、IAMロールのアクセスを対象外にする方法を検討していきます。

IAMロールのアクセスを対象外にする方法

IAMロールのアクセスを対象外にしたい。言い換えるなら、対象をIAMユーザーに限定したいので、aws:PrincipalArnグローバル条件キーを利用していきます。

このキーを使用して、リクエストを行ったプリンシパルの Amazon リソースネーム (ARN) をポリシーで指定した ARN と比較します。IAM ロールの場合、リクエストコンテキストは、ロールを引き受けたユーザーの ARN ではなく、ロールの ARN を返します。

IAMユーザーがAssumeRoleした場合、aws:PrincipalArnarn:aws:iam::111111111111:role/kuritify-roleになり、IAM Identity Centerで外部認証されたユーザーであればarn:aws:iam::111111111111:role/aws-reserved/sso.amazonaws.com/ap-northeast-1/AWSReservedSSO_AWSAdministratorAccess_xxxxxxxxxxx"になります。

CloudTrail userIdentity 要素で詳しく説明がありますが、リクエストコンテキストではuserIdentity.sessionContext.sessionIssuer.arnが該当し、同フィールドが含まれない場合userIdentity.arnが該当するフィールドです。いくつかCloudTrail管理イベントの例をあげます。

MFA認証していないアクセスキーのリクエスト。

"userIdentity": {
    "type": "IAMUser",
    "principalId": "*******",
    "arn": "arn:aws:iam::111111111111:user/kuritify",
    "accountId": "111111111111",
    "accessKeyId": "*******",
    "userName": "kuritify"
}

MFA認証していないアクセスキーがAssume Roleした後のリクエスト。

{
  "userIdentity": {
    "type": "AssumedRole",
    "principalId": "*******",
    "arn": "arn:aws:sts::111111111111:assumed-role/kuritify-role/kuritify-assume-role",
    "accountId": "111111111111",
    "accessKeyId": "*******",
    "sessionContext": {
      "sessionIssuer": {
        "type": "Role",
        "principalId": "*******",
        "arn": "arn:aws:iam::111111111111:role/kuritify-role",
        "accountId": "111111111111",
        "userName": "kuritify-role"
      },
      "attributes": {
        "creationDate": "2024-03-22T08:54:54Z",
        "mfaAuthenticated": "false"
      }
    }
  }
}

IAM Identity Centerで外部認証されたユーザーのリクエスト。(IdP側でMFA認証済み)

{
  "userIdentity": {
    "type": "AssumedRole",
    "principalId": "*******",
    "arn": "arn:aws:sts::111111111111:assumed-role/AWSReservedSSO_AWSAdministratorAccess_xxxxxxxxxxx/shintaro.kurihara@kinto-technologies.com",
    "accountId": "111111111111",
    "accessKeyId": "*******",
    "sessionContext": {
      "sessionIssuer": {
        "type": "Role",
        "principalId": "*******",
        "arn": "arn:aws:iam::111111111111:role/aws-reserved/sso.amazonaws.com/ap-northeast-1/AWSReservedSSO_AWSAdministratorAccess_xxxxxxxxxxx",
        "accountId": "111111111111",
        "userName": "AWSReservedSSO_AWSAdministratorAccess_xxxxxxxxxxx"
      },
      "attributes": {
        "creationDate": "2024-04-04T02:35:12Z",
        "mfaAuthenticated": "false"
      }
    }
  }
}

前章のアクセスパターン表にaws:PrincipalArnのパターンを追加すると以下になります。

アクセスパターン aws:MultiFactorAuthPresent aws:PrincipalArnのパターン
MFA認証済みの人間ユーザー true arn:aws:iam::<$ACCOUNT_ID>:user/$<USER_NAME>
MFA認証済みのアクセスキー true arn:aws:iam::<$ACCOUNT_ID>:user/$<USER_NAME>
MFA認証済みの人間ユーザーがAssumeRole true arn:aws:iam::<$ACCOUNT_ID>:role/<$ROLE_NAME>
MFA認証済みのアクセスキーがAssumeRole true arn:aws:iam::<$ACCOUNT_ID>:role/<$ROLE_NAME>
MFA認証していない人間ユーザー false arn:aws:iam::<$ACCOUNT_ID>:user/$<USER_NAME>
MFA認証していないアクセスキー キーが含まれない arn:aws:iam::<$ACCOUNT_ID>:user/$<USER_NAME>
MFA認証していない人間ユーザーがAssumeRole false arn:aws:iam::<$ACCOUNT_ID>:role/<$ROLE_NAME>
MFA認証していないアクセスキーがAssumeRole false arn:aws:iam::<$ACCOUNT_ID>:role/<$ROLE_NAME>
AWSサービス経由(Cloud Formationのサービスロールなど) false arn:aws:iam::<$ACCOUNT_ID>:role/<$ROLE_NAME>
AWSサービスロールのアクセス(Lambdaのサービスロールなど) false arn:aws:iam::<$ACCOUNT_ID>:role/<$ROLE_NAME>
IdP側でMFA認証済みの外部認証ユーザー false arn:aws:iam::<$ACCOUNT_ID>:role/aws-reserved/sso.amazonaws.com/<$REGION_OF_IAM_IDENTIY_CENTER>/<$PERMISSION_SET_NAME>_<$UNIQ_ID>

ArnLike条件演算子aws:PrincipalArnを条件キーに、IAMユーザーに対象を限定します。

"Condition" : {
  "Bool" : {
    "aws:MultiFactorAuthPresent" : "false"
  },
  "ArnLike": {
    "aws:PrincipalArn": [
      "arn:aws:iam::*:user/*"
    ]
}

これで、aws:MultiFactorAuthPresentがリクエストコンテキストに含まれfalseかつ、aws:PrincipalArnarn:aws:iam::<$ACCOUNT_ID>:user/$<USER_NAME>のみを対象になり、結果MFA認証していない人間ユーザーのみを対象にできました。と思っていたのですがここからが本当の闘いでした。これだと問題になる例外ケースが2つあります。

例外になる2つのケース

MFA認証していないアクセスキーでのアクセス(長期認証情報)は、aws:MultiFactorAuthPresentがリクエストコンテキストに含まれない。が前提にSCPを構築してきましたが、例外的に含まれくるケースが2つあります。ポリシー視点でいうと以下のケースです。

  • aws:MultiFactorAuthPresentfalse(リクエストコンテキストに含まれる)
  • aws:PrincipalArnのパターンがarn:aws:iam::<$ACCOUNT_ID>:user/$<USER_NAME>

例外1: AWS サービスがユーザーに代わって別のサービスにリクエストを実行するケース

AWSサービスがユーザーに代わって別のサービスにリクエストを実行する場合例外が発生する可能性があります。

例えば以下のようなAWS CLIを実行するとConditionがtrueに評価されてしまいます。

$ aws cloudformation create-stack --stack-name <$STACK_NAME> \
    --template-url https://kuritify.s3.ap-northeast-1.amazonaws.com/cf.template.yaml

問題になるリクエストコンテキストはCloudTrail管理イベントでなくデータイベントに記録されるのですが、CloudFormationがアクセスキーの認証情報を使って、cf.template.yamlをS3に取得しに行った際に、aws:MultiFactorAuthPresentがリクエストコンテキストに含まれていることがわかります。

{
  "eventVersion": "1.09",
  "userIdentity": {
    "type": "IAMUser",
    "principalId": "*******",
    "arn": "arn:aws:iam::111111111111:user/kuritify",
    "accountId": "111111111111",
    "accessKeyId": "*******",
    "userName": "kuritify",
    "sessionContext": {
      "attributes": {
        "creationDate": "2024-04-04T06:58:05Z",
        "mfaAuthenticated": "false"
      }
    },
    "invokedBy": "cloudformation.amazonaws.com"
  },
  "eventTime": "2024-04-04T06:58:05Z",
  "eventSource": "s3.amazonaws.com",
  "eventName": "GetObject",
  "awsRegion": "ap-northeast-1",
  "sourceIPAddress": "**.**.**.**",
  "userAgent": "cloudformation.amazonaws.com",
  "errorCode": "AccessDenied",
  "errorMessage": "Access Denied",
  "requestParameters": {
    "bucketName": "kuritify",
    "Host": "kuritify.s3.ap-northeast-1.amazonaws.com",
    "key": "cf.template.yaml"
  },
  "responseElements": null,
  "additionalEventData": {
    "SignatureVersion": "SigV4",
    "CipherSuite": "ECDHE-RSA-AES128-GCM-SHA256",
    "bytesTransferredIn": 0,
    "AuthenticationMethod": "AuthHeader",
    "x-amz-id-2": "************",
    "bytesTransferredOut": 243
  },
  "requestID": "*****************",
  "eventID": "*****************",
  "readOnly": true,
  "resources": [
    {
      "type": "AWS::S3::Object",
      "ARN": "arn:aws:s3:::kuritify/cf.template.yaml"
    },
    {
      "accountId": "111111111111",
      "type": "AWS::S3::Bucket",
      "ARN": "arn:aws:s3:::kuritify"
    }
  ],
  "eventType": "AwsApiCall",
  "managementEvent": false,
  "recipientAccountId": "111111111111",
  "vpcEndpointId": "vpce-****",
  "eventCategory": "Data"
}

この例外パターンに対処するには、aws:ViaAWSServiceが使えそうなので検証します。説明を引用します。

このキーを使用して、AWS サービスがユーザーに代わって別のサービスにリクエストを実行するかどうか確認します。

サービスが IAM プリンシパルの認証情報を使用し、プリンシパルに代わってリクエストを実行すると、リクエストコンテキストキーは true を返します。サービスがサービスロールまたはサービスリンクロールを使用してプリンシパルに代わって呼び出しを行う場合、コンテキストキーは false を返します。リクエストコンテキストキーは、プリンシパルが直接呼び出しを行ったときも false を返します。

  • 可用性 – このキーは常にリクエストコンテキストに含まれます
  • データ型 – ブール値
  • 値タイプ — 単一値

この条件キーを使用して、リクエストがサービスによって行われたかどうかに基づいてアクセスを許可または拒否できます。

こちらはCloudTrailイベントに該当するフィールドが無いようで、プリンシパルがIAMユーザーかつ、userIdentity.invokedByに何かしらのAWSサービスのドメインが含まれているケースだとtrueに評価される条件キーのようです。

そのためaws:ViaAWSServicefalseをBool条件演算子に追加します。

{
  "Condition": {
    "Bool": {
      "aws:MultiFactorAuthPresent": "false",
      "aws:ViaAWSService": "false"
    },
    "ArnLike": {
      "aws:PrincipalArn": ["arn:aws:iam::*:user/*"]
    }
  }
}

Bool条件演算子内はAndで評価されるため。前述のCloudFormationのパターンではaws:ViaAWSServiceはtrueになるので除外でき、別のケース、例えば $ aws s3 lsは、aws:ViaAWSServiceはfalseになるもののaws:MultiFactorAuthPresentがリクエストコンテキストに含まれてこないので、Bool条件演算子はリクエストコンテキストに含まれてない場合はfalse評価になる振る舞いから、こちらも除外されるということになります。

例外2:"その他"としか抽象化できない例外ケース

長くなっていますが、これで最後になります。さらに例外ケースがあります。

  • aws:MultiFactorAuthPresentfalse(リクエストコンテキストに含まれる)
  • aws:PrincipalArnのパターンがarn:aws:iam::<$ACCOUNT_ID>:user/$<USER_NAME>
  • aws:ViaAWSServicefalse

現在確認できているだけで以下の2ケースがあります。

  1. docker cli経由でECRにアクセスするパターンです。以下の操作をすると、docker pushがSCPでDenyに倒れます。
    $ aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin  <$ECR_REPOGITORY>
    $ docker push <$ECR_Repogitory>/<$TAG>
    
  2. npmでCodeArtifactにアクセスするパターンです。以下の操作をすると、npm publishがSCPでDenyに倒れます。
    $ aws codeartifact login --tool npm --domain kuritify-poc --domain-owner $(aws sts get-caller-identity | jq -r .Account) --repository <$REPOSITORY_NAME> --namespace <$NAME_SPACE>
    $ npm publish
    

双方のCloudTrail管理イベントを掲載します。これまでのどれにも当てはまらないリクエストコンテキストになっていることが確認できます。

MFA認証していないアクセスキーでdocker cli経由のECRアクセス。

{
  "eventVersion": "1.08",
  "userIdentity": {
    "type": "IAMUser",
    "principalId": "*******",
    "arn": "arn:aws:iam::111111111111:user/kuritify",
    "accountId": "111111111111",
    "accessKeyId": "*******",
    "userName": "kuritify",
    "sessionContext": {
      "sessionIssuer": {},
      "webIdFederationData": {},
      "attributes": {
        "creationDate": "2024-02-07T02:28:10Z",
        "mfaAuthenticated": "false"
      }
    },
    "invokedBy": "AWS Internal"
  },
  "eventTime": "2024-02-07T02:28:39Z",
  "eventSource": "ecr.amazonaws.com",
  "eventName": "InitiateLayerUpload",
  "awsRegion": "ap-northeast-1",
  "sourceIPAddress": "**.**.**.**",
  "userAgent": "AWS Internal",
  "errorCode": "AccessDenied",
  "errorMessage": "User: arn:aws:iam::111111111111:user/kuritify is not authorized to perform: ecr:InitiateLayerUpload on resource: arn:aws:ecr:ap-northeast-1:111111111111:repository/kuritify/tag with an explicit deny in a service control policy",
  "requestParameters": null,
  "responseElements": null,
  "requestID": "*****************",
  "eventID": "*****************",
  "readOnly": false,
  "eventType": "AwsApiCall",
  "managementEvent": true,
  "recipientAccountId": "111111111111",
  "eventCategory": "Management"
}

MFA認証していないアクセスキーでnpm cli経由のCodeArtifactアクセス。

{
  "eventVersion": "1.08",
  "userIdentity": {
    "type": "IAMUser",
    "principalId": "*******",
    "arn": "arn:aws:iam::111111111111:user/kuritify",
    "accountId": "111111111111",
    "accessKeyId": "*******",
    "userName": "kuritify",
    "sessionContext": {
      "sessionIssuer": {},
      "webIdFederationData": {},
      "attributes": {
        "creationDate": "2024-04-04T09:47:41Z",
        "mfaAuthenticated": "false"
      }
    }
  },
  "eventTime": "2024-04-04T09:47:58Z",
  "eventSource": "codeartifact.amazonaws.com",
  "eventName": "PublishPackageVersion",
  "awsRegion": "ap-northeast-1",
  "sourceIPAddress": "**.**.**.**",
  "userAgent": "npm/10.2.4 node/v20.11.1 darwin arm64 workspaces/false",
  "errorCode": "AccessDenied",
  "errorMessage": "Access denied. User: arn:aws:iam::111111111111:user/kuritify is not authorized to perform: codeartifact:PublishPackageVersion on resource: arn:aws:codeartifact:ap-northeast-1:111111111111:package/kuritify-poc/kuritify-poc/npm/kuritify-poc/kuritify-module with an explicit deny in a service control policy",
  "requestParameters": {
    "domainName": "kuritify-poc",
    "domainOwner": "111111111111",
    "repositoryName": "kuritify-poc",
    "packageName": "kuritify-module",
    "packageFormat": "npm",
    "packageNamespace": "kuritify-poc"
  },
  "responseElements": null,
  "additionalEventData": {
    "httpMethod": "PUT",
    "requestUri": "/npm/@kuritify-poc%2fkuritify-module"
  },
  "requestID": "*****************",
  "eventID": "*****************",
  "readOnly": false,
  "resources": [
    {
      "accountId": "111111111111",
      "type": "AWS::CodeArtifact::Domain",
      "ARN": "arn:aws:codeartifact:ap-northeast-1:111111111111:domain/kuritify-poc"
    },
    {
      "accountId": "111111111111",
      "type": "AWS::CodeArtifact::Repository",
      "ARN": "arn:aws:codeartifact:ap-northeast-1:111111111111:repository/kuritify-poc/kuritify-poc"
    },
    {
      "accountId": "111111111111",
      "type": "AWS::CodeArtifact::Package",
      "ARN": "arn:aws:codeartifact:ap-northeast-1:111111111111:package/kuritify-poc/kuritify-poc/npm/kuritify-poc/kuritify-module"
    }
  ],
  "eventType": "AwsApiCall",
  "managementEvent": true,
  "recipientAccountId": "111111111111",
  "eventCategory": "Management",
  "tlsDetails": {
    "tlsVersion": "TLSv1.3",
    "cipherSuite": "TLS_AES_128_GCM_SHA256",
    "clientProvidedHostHeader": "kuritify-poc-111111111111.d.codeartifact.ap-northeast-1.amazonaws.com"
  }
}

AWS re:Invent2023 - Dive deep into Amazon ECRという資料でECRのアーキテクチャーが説明されていますが、docker cli経由でECRにアクセスする際にはProxy Serviceを経由してリクエストが実行されます。

$ ecr loginで返される認証情報は一時的な認証情報なので、aws:MultiFactorAuthPresentがfalseになるのは理解できますが、aws:ViaAWSServiceはtrueじゃないと整合性とれなくないですかAWSさん。。CodeArtifactについてはアーキテクチャの詳細が記載されている資料を発見できていないのですが、同じような動きなんだろうと想像しています。

こちらはCondition句では対応不可能なので、MFA認証の有無に関わらず実行できるよう、該当するアクションを列挙することで対処します。

{
  "Effect": "Deny",
  "NotAction": [
    "...最低限許可したいアクション",
    "ecr:BatchCheckLayerAvailability",
    "ecr:initiateLayerUpload",
    "ecr:UploadLayerPart",
    "ecr:CompleteLayerUpload",
    "ecr:PutImage",
    "ecr:BatchGetImage",
    "ecr:GetDownloadUrlForLayer",
    "codeartifact:PublishPackageVersion",
    "codeartifact:ReadFromRepository"
  ]
}

まとめ

SCPでIAMユーザーにMFAを強制させるのは非常に難しいという話をさせていただきました。弊社では実環境でこのSCPを適用しており、現状トラブルは無いですが、利用サービスが限定的なので予断は許さない状況であります。このSCPを参考にしていただける場合、十分に動作検証をして採用の判断をしていただければと思います。

Zenn初投稿ということで気合入りすぎました。長時間お付き合いいただきありがとうございます。

Discussion

knziiyknziiy

素晴らしい記事をありがとうございます。ご指摘のとおり、注意しながら参考にさせていただきます。

AWSアカウントのIAMユーザー(人間のアクセスのみ)にMFA認証を強制させる

本当に、シンプルな要件であるこれをしたいだけなのに、大変過ぎますね。

Customer Obsessionに期待しましょうw

くりてぃふぁい 👾くりてぃふぁい 👾

@knziiy

コメントありがとうございます。
お役に立てたなら苦戦した甲斐がありましたw

AWS進化しすぎましたからね。根幹のIAMが複雑になるのも致し方なしですかね。