AWSConfigRemediation-DeleteIAMUserドキュメントは、仮想認証アプリケーション以外のMFAデバイスタイプの場合、実行に失敗します

AWSConfigRemediation-DeleteIAMUserドキュメントは、仮想認証アプリケーション以外のMFAデバイスタイプの場合、実行に失敗します

2025.09.12

はじめに

以前、AWS Step FunctionsでAWS Systems Manager(以下、SSM)ドキュメントAWSConfigRemediation-DeleteIAMUserを利用したSSMオートメーションを実行し、IAMユーザーを削除する記事を書きました。
https://dev.classmethod.jp/articles/aws-stepfunctions-iam-user-delete/

上記記事の方法でIAMユーザーを削除していたところ、「仮想MFAデバイス」以外のMFAデバイスタイプが設定されているユーザーで実行が失敗する事象を確認しました。

AWSにおけるIAMユーザーに設定可能なMFAデバイスタイプは、以下の3つのタイプがあります。

  • 仮想認証アプリケーション(Google AuthenticatorやMicrosoft Authenticatorなどのアプリ)
  • ハードウェアTOTPトークン(YubiKeyなど)
  • パスキーまたはセキュリティキー
    cm-hirai-screenshot 2025-08-25 14.52.55
    cm-hirai-screenshot 2025-08-25 14.50.59

このうち、「仮想MFAデバイス」として分類されるのは仮想認証アプリケーションのみです。

本記事では、AWSConfigRemediation-DeleteIAMUserドキュメントが仮想認証アプリケーション以外のMFAデバイス(パスキー/セキュリティキーやハードウェアTOTPトークン)を持つIAMユーザーの削除に失敗する理由と、その対応方法について解説します。

発生した事象

実際に発生した事象は以下のとおりです。

  • 1回目の実行:パスキー/セキュリティキーを設定したIAMユーザーに対する削除実行でエラー発生
  • 2回目の実行:同じIAMユーザーが正常に削除される
    cm-hirai-screenshot 2025-08-25 14.45.29
    1回目
    cm-hirai-screenshot 2025-08-25 14.45.13
    2回目

1回目のSSM Automationエラーメッセージは以下のとおりです。

			
			Traceback (most recent call last):
  File "/tmp/2d1fce7a-0a81-4b1a-98d8-9528141c263e-2025-08-25-06-11-30/customer_script.py", line 76, in handler
    delete_mfa_devices(iam_client, mfa_devices)
  File "/tmp/2d1fce7a-0a81-4b1a-98d8-9528141c263e-2025-08-25-06-11-30/customer_script.py", line 47, in delete_mfa_devices
    response = iam_client.delete_virtual_mfa_device(SerialNumber=mfa_device)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/var/lang/lib/python3.11/site-packages/botocore/client.py", line 598, in _api_call
    return self._make_api_call(operation_name, kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/var/lang/lib/python3.11/site-packages/botocore/context.py", line 123, in wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/var/lang/lib/python3.11/site-packages/botocore/client.py", line 1061, in _make_api_call
    raise error_class(parsed_response, operation_name)
botocore.errorfactory.NoSuchEntityException: An error occurred (NoSuchEntity) when calling the DeleteVirtualMFADevice operation: MFA Device with serial number arn:aws:iam::アカウントID:u2f/user/iam-passkey/test-CRESUIN32BFJPIIGIL2PLPODPM does not exist.

NoSuchEntityException - An error occurred (NoSuchEntity) when calling the DeleteVirtualMFADevice operation: MFA Device with serial number arn:aws:iam::アカウントID:u2f/user/iam-passkey/test-CRESUIN32BFJPIIGIL2PLPODPM does not exist.

		

問題の原因と対応の可否

結論から言うと、現在のAWSConfigRemediation-DeleteIAMUserドキュメントでは、仮想認証アプリケーション以外のMFAデバイスを持つIAMユーザーを1回の実行で正常に削除することはできません

AWSConfigRemediation-DeleteIAMUserドキュメントの公式説明では、MFAデバイスの削除が可能と記載されていますが、実際には「仮想認証アプリケーション」のみを対象にしています。他のデバイスタイプは2回実行すれば削除できますが、1回目は失敗します。

AWSConfigRemediation-DeleteIAMUser ランブックは指定した AWS Identity and Access Management (IAM) ユーザーを削除します。このオートメーションは、IAM ユーザーに関連付けられた次のリソースを削除またはデタッチします。

  • 多要素認証 (MFA) デバイス

https://docs.aws.amazon.com/ja_jp/systems-manager-automation-runbooks/latest/userguide/automation-aws-delete-iam-user.html

この問題は、AWSConfigRemediation-DeleteIAMUserドキュメントの実装に起因するものです。

ドキュメント実装の問題点

AWSConfigRemediation-DeleteIAMUserドキュメント(バージョン4)内のMFAデバイス処理部分は、以下のような実装になっています。

			
			def get_mfa_devices(iam_client, iam_username):
    paginator = iam_client.get_paginator("list_mfa_devices")
    page_iterator = paginator.paginate(UserName=iam_username)
    mfa_devices = []
    for page in page_iterator:
        for mfa_device in page["MFADevices"]:
            mfa_devices.append(mfa_device["SerialNumber"])
        sleep(THROTTLE_PERIOD)
    return mfa_devices

def deactivate_mfa_devices(iam_client, iam_username, mfa_devices):
    responses = []
    for mfa_device in mfa_devices:
        response = iam_client.deactivate_mfa_device(UserName=iam_username, SerialNumber=mfa_device)
        responses.append(response)
    return responses

def delete_mfa_devices(iam_client, mfa_devices):
    responses = []
    for mfa_device in mfa_devices:
        response = iam_client.delete_virtual_mfa_device(SerialNumber=mfa_device)
        responses.append(response)
    return responses

		
AWSConfigRemediation-DeleteIAMUserドキュメント(ドキュメントのバージョン4)の全コード
			
			schemaVersion: "0.3"
description: |
  ### Document name - AWSConfigRemediation-DeleteIAMUser

  ## What does this document do?
  This runbook deletes the AWS Identity and Access Management (IAM) user you specify using the [DeleteUser](https://docs.aws.amazon.com/IAM/latest/APIReference/API_DeleteUser.html) API. This automation deletes or detaches the following resources associated with the IAM user.

  - Access keys
  - Attached managed policies
  - Git credentials
  - IAM group memberships
  - IAM user password
  - Inline policies
  - Multi-factor authentication (MFA) devices
  - Signing certificates
  - SSH public keys

  ## Input Parameters
  * AutomationAssumeRole: (Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf.
  * IAMUserId: (Required) The ID of the IAM user you want to delete.

  ## Output Parameters
  * DeleteIAMUserAndVerify.Output: Output of the step indicating successful deletion of the AWS IAM User.

assumeRole: "{{ AutomationAssumeRole }}"
parameters:
  AutomationAssumeRole:
    type: AWS::IAM::Role::Arn
    description: (Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf.
  IAMUserId:
    type: String
    description: (Required) The ID of the IAM user you want to delete.
    allowedPattern: ^AIDA[A-Z0-9]+$
outputs:
  - DeleteIAMUserAndVerify.Output
mainSteps:
  - name: GetUsername
    action: aws:executeScript
    description: |
      ## GetUsername
      Gathers the user name of the IAM user you specify in the `IAMUserId` parameter.
      ## Outputs
      * UserName: The name of the user.
    isEnd: false
    timeoutSeconds: 600
    inputs:
      Runtime: python3.11
      Handler: handler
      InputPayload:
        IAMUserId: "{{ IAMUserId }}"
      Script: |-
        from time import sleep

        import boto3

        THROTTLE_PERIOD = 0.05

        def get_username(iam_client, iam_user_id):
            paginator = iam_client.get_paginator("list_users")
            page_iterator = paginator.paginate()
            for page in page_iterator:
                for user in page["Users"]:
                    if user["UserId"] == iam_user_id:
                        return user["UserName"]
                sleep(THROTTLE_PERIOD)

        def handler(event, context):
            iam_client = boto3.client("iam")
            iam_user_id = event["IAMUserId"]

            iam_username = get_username(iam_client, iam_user_id)

            if iam_username is not None:
                return {"UserName": iam_username}
            else:
                error_message = f"AWS IAM USER ID, {iam_user_id} DOES NOT EXIST."
                raise Exception(error_message)
    outputs:
      - Name: UserName
        Selector: $.Payload.UserName
        Type: String
  - name: GetKeysCertificatesMfaAndCredentials
    action: aws:executeScript
    description: |
      ## GetKeysCertificatesMfaAndCredentials
      Gathers access keys, certificates, credentials, MFA devices, and SSH keys associated with the IAM user.
      ## Outputs
      * Output: The access keys, ssh keys, certificates, mfa device and credentials for the AWS IAM user.
    isEnd: false
    timeoutSeconds: 600
    inputs:
      Runtime: python3.11
      Handler: handler
      InputPayload:
        UserName: "{{ GetUsername.UserName }}"
      Script: |-
        from time import sleep

        import boto3

        THROTTLE_PERIOD = 0.05

        def get_access_keys(iam_client, iam_username):
            paginator = iam_client.get_paginator("list_access_keys")
            page_iterator = paginator.paginate(UserName=iam_username)
            access_keys = []
            for page in page_iterator:
                for access_key in page["AccessKeyMetadata"]:
                    access_keys.append(access_key["AccessKeyId"])
                sleep(THROTTLE_PERIOD)
            return access_keys

        def get_ssh_public_keys(iam_client, iam_username):
            paginator = iam_client.get_paginator("list_ssh_public_keys")
            page_iterator = paginator.paginate(UserName=iam_username)
            ssh_keys = []
            for page in page_iterator:
                for ssh_key in page["SSHPublicKeys"]:
                    ssh_keys.append(ssh_key["SSHPublicKeyId"])
                sleep(THROTTLE_PERIOD)
            return ssh_keys

        def get_signing_certificates(iam_client, iam_username):
            paginator = iam_client.get_paginator("list_signing_certificates")
            page_iterator = paginator.paginate(UserName=iam_username)
            signing_certificates = []
            for page in page_iterator:
                for access_key in page["Certificates"]:
                    signing_certificates.append(access_key["CertificateId"])
                    sleep(THROTTLE_PERIOD)
            return signing_certificates

        def get_mfa_devices(iam_client, iam_username):
            paginator = iam_client.get_paginator("list_mfa_devices")
            page_iterator = paginator.paginate(UserName=iam_username)
            mfa_devices = []
            for page in page_iterator:
                for mfa_device in page["MFADevices"]:
                    mfa_devices.append(mfa_device["SerialNumber"])
                sleep(THROTTLE_PERIOD)
            return mfa_devices

        def get_service_specific_credentials(iam_client, iam_username):
            response = iam_client.list_service_specific_credentials(UserName=iam_username)
            service_specific_credential_ids = []
            for service in response["ServiceSpecificCredentials"]:
                service_specific_credential_ids.append(service["ServiceSpecificCredentialId"])
            return service_specific_credential_ids

        def handler(event, context):
            iam_client = boto3.client("iam")
            iam_username = event["UserName"]

            access_keys = get_access_keys(iam_client, iam_username)
            ssh_public_keys = get_ssh_public_keys(iam_client, iam_username)
            signing_certificates = get_signing_certificates(iam_client, iam_username)
            mfa_devices = get_mfa_devices(iam_client, iam_username)
            service_specific_credentials = get_service_specific_credentials(iam_client, iam_username)

            return {
                "access_keys": access_keys,
                "ssh_public_keys": ssh_public_keys,
                "signing_certificates": signing_certificates,
                "mfa_devices": mfa_devices,
                "service_specific_credentials": service_specific_credentials,
            }
    outputs:
      - Name: AccessKeys
        Selector: $.Payload.access_keys
        Type: StringList
      - Name: SSHPublicKeys
        Selector: $.Payload.ssh_public_keys
        Type: StringList
      - Name: SigningCertificates
        Selector: $.Payload.signing_certificates
        Type: StringList
      - Name: MFADevices
        Selector: $.Payload.mfa_devices
        Type: StringList
      - Name: ServiceSpecificCredentials
        Selector: $.Payload.service_specific_credentials
        Type: StringList
  - name: GetGroupsAndPolicies
    action: aws:executeScript
    description: |
      ## GetGroupsAndPolicies
      Gathers group memberships and policies for the IAM user.
      ## Outputs
      * Output: The group memberships and policies for the AWS IAM user.
    isEnd: false
    timeoutSeconds: 600
    inputs:
      Runtime: python3.11
      Handler: handler
      InputPayload:
        UserName: "{{ GetUsername.UserName }}"
      Script: |-
        from time import sleep

        import boto3

        THROTTLE_PERIOD = 0.05

        def get_user_groups(iam_client, iam_username):
            paginator = iam_client.get_paginator("list_groups_for_user")
            page_iterator = paginator.paginate(UserName=iam_username)
            groups = []
            for page in page_iterator:
                for group in page["Groups"]:
                    groups.append(group["GroupName"])
                sleep(THROTTLE_PERIOD)
            return groups

        def get_user_policies(iam_client, iam_username):
            paginator = iam_client.get_paginator("list_user_policies")
            page_iterator = paginator.paginate(UserName=iam_username)
            policies = []
            for page in page_iterator:
                policies.extend(page["PolicyNames"])
                sleep(THROTTLE_PERIOD)
            return policies

        def get_attached_user_policies(iam_client, iam_username):
            paginator = iam_client.get_paginator("list_attached_user_policies")
            page_iterator = paginator.paginate(UserName=iam_username)
            policies = []
            for page in page_iterator:
                for policy in page["AttachedPolicies"]:
                    policies.append(policy["PolicyArn"])
                sleep(THROTTLE_PERIOD)
            return policies

        def handler(event, context):
            iam_client = boto3.client("iam")
            iam_username = event["UserName"]

            groups = get_user_groups(iam_client, iam_username)
            user_policies = get_user_policies(iam_client, iam_username)
            attached_user_policies = get_attached_user_policies(iam_client, iam_username)

            return {"groups": groups, "user_policies": user_policies, "attached_user_policies": attached_user_policies}
    outputs:
      - Name: Groups
        Selector: $.Payload.groups
        Type: StringList
      - Name: UserPolicies
        Selector: $.Payload.user_policies
        Type: StringList
      - Name: AttachedUserPolicies
        Selector: $.Payload.attached_user_policies
        Type: StringList
  - name: DeleteKeysCertificatesMfaAndCredentials
    action: aws:executeScript
    description: |
      ## DeleteKeysCertificatesMfaAndCredentials
      Deletes access keys, certificates, credentials, MFA devices, and SSH keys associated with the IAM user.
      ## Outputs
      * Output: The output of this step indicating successful deletion of the access keys, ssh keys, certificates, MFA device and credentials for the AWS IAM user.
    isEnd: false
    timeoutSeconds: 600
    inputs:
      Runtime: python3.11
      Handler: handler
      InputPayload:
        UserName: "{{ GetUsername.UserName }}"
        AccessKeys: "{{ GetKeysCertificatesMfaAndCredentials.AccessKeys }}"
        SSHPublicKeys: "{{ GetKeysCertificatesMfaAndCredentials.SSHPublicKeys }}"
        SigningCertificates: "{{ GetKeysCertificatesMfaAndCredentials.SigningCertificates }}"
        MFADevices: "{{ GetKeysCertificatesMfaAndCredentials.MFADevices }}"
        ServiceSpecificCredentials: "{{ GetKeysCertificatesMfaAndCredentials.ServiceSpecificCredentials }}"
      Script: |-
        import boto3

        def delete_login_profile(iam_client, iam_username):
            try:
                response = iam_client.delete_login_profile(UserName=iam_username)
                return response
            except iam_client.exceptions.NoSuchEntityException:
                return None

        def delete_access_keys(iam_client, iam_username, access_keys):
            responses = []
            for access_key in access_keys:
                response = iam_client.delete_access_key(UserName=iam_username, AccessKeyId=access_key)
                responses.append(response)
            return responses

        def delete_ssh_public_keys(iam_client, iam_username, ssh_public_keys):
            responses = []
            for ssh_key in ssh_public_keys:
                response = iam_client.delete_ssh_public_key(UserName=iam_username, SSHPublicKeyId=ssh_key)
                responses.append(response)
            return responses

        def delete_signing_certificate(iam_client, iam_username, signing_certificates):
            responses = []
            for certificate in signing_certificates:
                response = iam_client.delete_signing_certificate(UserName=iam_username, CertificateId=certificate)
                responses.append(response)
            return responses

        def deactivate_mfa_devices(iam_client, iam_username, mfa_devices):
            responses = []
            for mfa_device in mfa_devices:
                response = iam_client.deactivate_mfa_device(UserName=iam_username, SerialNumber=mfa_device)
                responses.append(response)
            return responses

        def delete_mfa_devices(iam_client, mfa_devices):
            responses = []
            for mfa_device in mfa_devices:
                response = iam_client.delete_virtual_mfa_device(SerialNumber=mfa_device)
                responses.append(response)
            return responses

        def delete_service_specific_credential(iam_client, iam_username, service_specific_credentials):
            responses = []
            for service_credential in service_specific_credentials:
                response = iam_client.delete_service_specific_credential(
                    UserName=iam_username, ServiceSpecificCredentialId=service_credential
                )
                responses.append(response)
            return responses

        def handler(event, context):
            iam_client = boto3.client("iam")
            iam_username = event["UserName"]
            access_keys = event["AccessKeys"]
            ssh_public_keys = event["SSHPublicKeys"]
            signing_certificates = event["SigningCertificates"]
            mfa_devices = event["MFADevices"]
            service_specific_credentials = event["ServiceSpecificCredentials"]

            delete_login_profile(iam_client, iam_username)
            delete_access_keys(iam_client, iam_username, access_keys)
            delete_ssh_public_keys(iam_client, iam_username, ssh_public_keys)
            delete_signing_certificate(iam_client, iam_username, signing_certificates)
            deactivate_mfa_devices(iam_client, iam_username, mfa_devices)
            delete_mfa_devices(iam_client, mfa_devices)
            delete_service_specific_credential(iam_client, iam_username, service_specific_credentials)

            return "Processed deleting login profile, deleting access keys, deleting ssh public keys, deleting signing certificates, deactivating & deleting MFA devices, and deleting service specific credentials."
    outputs:
      - Name: Output
        Selector: $.Payload.output
        Type: String
  - name: DeleteGroupsAndPolicies
    action: aws:executeScript
    description: |
      ## DeleteGroupsAndPolicies
      Deletes group memberships and policies for the IAM user.
      ## Outputs
      * Output: The output of this step indicating successful deletion of the group memberships and policies for the AWS IAM User.
    isEnd: false
    timeoutSeconds: 600
    inputs:
      Runtime: python3.11
      Handler: handler
      InputPayload:
        UserName: "{{ GetUsername.UserName }}"
        Groups: "{{ GetGroupsAndPolicies.Groups }}"
        UserPolicies: "{{ GetGroupsAndPolicies.UserPolicies }}"
        AttachedUserPolicies: "{{ GetGroupsAndPolicies.AttachedUserPolicies }}"
      Script: |-
        import boto3

        def remove_user_from_group(iam_client, iam_username, groups):
            responses = []
            for group in groups:
                response = iam_client.remove_user_from_group(UserName=iam_username, GroupName=group)
                responses.append(response)
            return responses

        def delete_user_policies(iam_client, iam_username, user_policies):
            responses = []
            for policy in user_policies:
                response = iam_client.delete_user_policy(UserName=iam_username, PolicyName=policy)
                responses.append(response)
            return responses

        def detach_attached_user_policies(iam_client, iam_username, attached_user_policies):
            responses = []
            for policy in attached_user_policies:
                response = iam_client.detach_user_policy(UserName=iam_username, PolicyArn=policy)
                responses.append(response)
            return responses

        def handler(event, context):
            iam_client = boto3.client("iam")
            iam_username = event["UserName"]
            groups = event["Groups"]
            user_policies = event["UserPolicies"]
            attached_user_policies = event["AttachedUserPolicies"]

            remove_user_from_group(iam_client, iam_username, groups)
            delete_user_policies(iam_client, iam_username, user_policies)
            detach_attached_user_policies(iam_client, iam_username, attached_user_policies)

            return "Processed removal of user from groups, deleting user policies, and detaching user attached policies."
    outputs:
      - Name: Output
        Selector: $.Payload.output
        Type: String
  - name: DeleteIAMUserAndVerify
    action: aws:executeScript
    description: |
      ## DeleteIAMUserAndVerify
      Deletes the IAM user and verifies the user has been deleted.
      ## Outputs
      * Output: A success message or failure exception.
    isEnd: true
    timeoutSeconds: 600
    inputs:
      Runtime: python3.11
      Handler: handler
      InputPayload:
        UserName: "{{ GetUsername.UserName }}"
      Script: |-
        import boto3

        def delete_iam_user(iam_client, iam_username):
            response = iam_client.delete_user(UserName=iam_username)
            return response

        def verify_iam_user_status(iam_client, iam_username):
            try:
                iam_client.get_user(UserName=iam_username)
                error_message = f"VERIFICATION FAILED. AWS IAM USER {iam_username} DELETION UNSUCCESSFUL."
                raise Exception(error_message)
            except iam_client.exceptions.NoSuchEntityException:
                return {"output": "Verification of AWS IAM user deletion is successful."}

        def handler(event, context):
            iam_client = boto3.client("iam")
            iam_username = event["UserName"]
            response = delete_iam_user(iam_client, iam_username)
            results = verify_iam_user_status(iam_client, iam_username)
            results["DeleteUserResponse"] = response
            return results

    outputs:
      - Name: Output
        Selector: $.Payload.output
        Type: String
      - Name: DeleteUserResponse
        Selector: $.Payload.DeleteUserResponse
        Type: StringMap


		

この実装では、MFAデバイスの種類を判定せずに、すべてのMFAデバイスに対して仮想MFAデバイス専用の削除APIを実行してしまうという問題があります。

問題1:MFAデバイス取得時の種類未判定

list_mfa_devices APIは、IAMユーザーに関連付けられたすべての種類のMFAデバイスを返します。この時点で、デバイスの種類(仮想/非仮想)を判定する処理が存在しません。

			
			# get_mfa_devices関数の問題点
def get_mfa_devices(iam_client, iam_username):
    paginator = iam_client.get_paginator("list_mfa_devices")
    # ↑ すべてのMFAデバイス(仮想・ハードウェア・パスキー)を取得

		

問題2:削除処理での不適切なAPI使用

delete_virtual_mfa_device APIは仮想MFAデバイス専用ですが、コードではMFAデバイスの種類に関係なく、すべてのデバイスに対してこのAPIを実行しています。

			
			# delete_mfa_devices関数の問題点
def delete_mfa_devices(iam_client, mfa_devices):
    responses = []
    for mfa_device in mfa_devices:
        # ↓ すべてのMFAデバイスに対して仮想MFAデバイス削除APIを実行
        response = iam_client.delete_virtual_mfa_device(SerialNumber=mfa_device)
        responses.append(response)
    return responses

		

MFAデバイス種類別のシリアル番号例:

			
			仮想認証アプリ: arn:aws:iam::123456789012:mfa/user-name
ハードウェア:   arn:aws:iam::123456789012:mfa/hardware-device-name  
パスキー:       arn:aws:iam::123456789012:u2f/user/user-name/device-id

		

パスキー(u2f)やハードウェアデバイスのシリアル番号に対してdelete_virtual_mfa_deviceを実行すると、以下のエラーが発生します。

NoSuchEntityException - An error occurred (NoSuchEntity) when calling the DeleteVirtualMFADevice operation: MFA Device with serial number arn:aws:iam::アカウントID:u2f/user/iam-passkey/test-CRESUIN32BFJPIIGIL2PLPODPM does not exist.

仮想MFAデバイス以外のMFAデバイス(パスキーやハードウェアTOTPトークン)の場合、デアクティベート処理のみで削除が完了するため、削除処理の実行は不要です。

つまり、仮想ではないMFAデバイスは、デアクティベート処理のみで完了し、削除処理は本来不要なのです。

実行時の動作フロー

1回目の実行:

  1. get_mfa_devices() → パスキーのシリアル番号を取得
  2. deactivate_mfa_devices() → パスキーのデアクティベート成功
  3. delete_mfa_devices() → パスキーに対してdelete_virtual_mfa_device実行 → エラー発生

2回目の実行:

  1. get_mfa_devices() → MFAデバイスが既にデアクティベートされているため空のリストを返す
  2. deactivate_mfa_devices() → 処理対象なし
  3. delete_mfa_devices() → 処理対象なし → 成功

この動作により、2回目の実行では成功するという結果になります。

解決方法

この問題を解決するためには、カスタムドキュメントの作成が必要です。以下の2つのアプローチが考えられます。

アプローチ1:事前チェックによる分岐処理

仮想MFAデバイスかどうかを事前に判定し、該当する場合のみ削除処理を実行する方法です。

元のコード:

			
			def delete_mfa_devices(iam_client, mfa_devices):
    responses = []
    for mfa_device in mfa_devices:
        response = iam_client.delete_virtual_mfa_device(SerialNumber=mfa_device)
        responses.append(response)
    return responses

		

修正後のコード:

			
			def delete_mfa_devices(iam_client, mfa_devices):
    """
    アプローチ1: 事前チェックによる分岐処理
    仮想MFAデバイスかどうかを事前に判定し、該当する場合のみ削除処理を実行
    """
    if not mfa_devices:
        return []

    # 仮想MFAデバイスの一覧を取得
    virtual_mfa_devices = []
    paginator = iam_client.get_paginator("list_virtual_mfa_devices")
    for page in paginator.paginate():
        for device in page["VirtualMFADevices"]:
            virtual_mfa_devices.append(device["SerialNumber"])
        sleep(THROTTLE_PERIOD)

    # 仮想MFAデバイスのみ削除
    responses = []
    for mfa_device in mfa_devices:
        if mfa_device in virtual_mfa_devices:
            print(f"Deleting virtual MFA device: {mfa_device}")
            response = iam_client.delete_virtual_mfa_device(SerialNumber=mfa_device)
            responses.append(response)
        else:
            print(f"Skipping non-virtual MFA device: {mfa_device}")

    return responses

		

追加で必要な変更:
DeleteKeysCertificatesMfaAndCredentialsステップのスクリプト冒頭に以下を追加:

			
			from time import sleep
import boto3

THROTTLE_PERIOD = 0.05

		
DeleteIAMUser-VirtualMfaPrecheckドキュメントの全コード
			
			schemaVersion: '0.3'
description: |
  ### Document name - AWSConfigRemediation-DeleteIAMUser

  ## What does this document do?
  This runbook deletes the AWS Identity and Access Management (IAM) user you specify using the [DeleteUser](https://docs.aws.amazon.com/IAM/latest/APIReference/API_DeleteUser.html) API. This automation deletes or detaches the following resources associated with the IAM user.

  - Access keys
  - Attached managed policies
  - Git credentials
  - IAM group memberships
  - IAM user password
  - Inline policies
  - Multi-factor authentication (MFA) devices
  - Signing certificates
  - SSH public keys

  ## Input Parameters
  * AutomationAssumeRole: (Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf.
  * IAMUserId: (Required) The ID of the IAM user you want to delete.

  ## Output Parameters
  * DeleteIAMUserAndVerify.Output: Output of the step indicating successful deletion of the AWS IAM User.
assumeRole: '{{ AutomationAssumeRole }}'
parameters:
  AutomationAssumeRole:
    type: AWS::IAM::Role::Arn
    description: (Required) The Amazon Resource Name (ARN) of the AWS Identity and Access Management (IAM) role that allows Systems Manager Automation to perform the actions on your behalf.
  IAMUserId:
    type: String
    description: (Required) The ID of the IAM user you want to delete.
    allowedPattern: ^AIDA[A-Z0-9]+$
mainSteps:
  - description: |
      ## GetUsername
      Gathers the user name of the IAM user you specify in the `IAMUserId` parameter.
      ## Outputs
      * UserName: The name of the user.
    name: GetUsername
    action: aws:executeScript
    timeoutSeconds: 600
    nextStep: GetKeysCertificatesMfaAndCredentials
    isEnd: false
    inputs:
      Runtime: python3.11
      Handler: handler
      InputPayload:
        IAMUserId: '{{ IAMUserId }}'
      Script: |-
        from time import sleep

        import boto3

        THROTTLE_PERIOD = 0.05

        def get_username(iam_client, iam_user_id):
            paginator = iam_client.get_paginator("list_users")
            page_iterator = paginator.paginate()
            for page in page_iterator:
                for user in page["Users"]:
                    if user["UserId"] == iam_user_id:
                        return user["UserName"]
                sleep(THROTTLE_PERIOD)

        def handler(event, context):
            iam_client = boto3.client("iam")
            iam_user_id = event["IAMUserId"]

            iam_username = get_username(iam_client, iam_user_id)

            if iam_username is not None:
                return {"UserName": iam_username}
            else:
                error_message = f"AWS IAM USER ID, {iam_user_id} DOES NOT EXIST."
                raise Exception(error_message)
    outputs:
      - Name: UserName
        Selector: $.Payload.UserName
        Type: String
  - description: |
      ## GetKeysCertificatesMfaAndCredentials
      Gathers access keys, certificates, credentials, MFA devices, and SSH keys associated with the IAM user.
      ## Outputs
      * Output: The access keys, ssh keys, certificates, mfa device and credentials for the AWS IAM user.
    name: GetKeysCertificatesMfaAndCredentials
    action: aws:executeScript
    timeoutSeconds: 600
    nextStep: GetGroupsAndPolicies
    isEnd: false
    inputs:
      Runtime: python3.11
      Handler: handler
      InputPayload:
        UserName: '{{ GetUsername.UserName }}'
      Script: |-
        from time import sleep

        import boto3

        THROTTLE_PERIOD = 0.05

        def get_access_keys(iam_client, iam_username):
            paginator = iam_client.get_paginator("list_access_keys")
            page_iterator = paginator.paginate(UserName=iam_username)
            access_keys = []
            for page in page_iterator:
                for access_key in page["AccessKeyMetadata"]:
                    access_keys.append(access_key["AccessKeyId"])
                sleep(THROTTLE_PERIOD)
            return access_keys

        def get_ssh_public_keys(iam_client, iam_username):
            paginator = iam_client.get_paginator("list_ssh_public_keys")
            page_iterator = paginator.paginate(UserName=iam_username)
            ssh_keys = []
            for page in page_iterator:
                for ssh_key in page["SSHPublicKeys"]:
                    ssh_keys.append(ssh_key["SSHPublicKeyId"])
                sleep(THROTTLE_PERIOD)
            return ssh_keys

        def get_signing_certificates(iam_client, iam_username):
            paginator = iam_client.get_paginator("list_signing_certificates")
            page_iterator = paginator.paginate(UserName=iam_username)
            signing_certificates = []
            for page in page_iterator:
                for access_key in page["Certificates"]:
                    signing_certificates.append(access_key["CertificateId"])
                    sleep(THROTTLE_PERIOD)
            return signing_certificates

        def get_mfa_devices(iam_client, iam_username):
            paginator = iam_client.get_paginator("list_mfa_devices")
            page_iterator = paginator.paginate(UserName=iam_username)
            mfa_devices = []
            for page in page_iterator:
                for mfa_device in page["MFADevices"]:
                    mfa_devices.append(mfa_device["SerialNumber"])
                sleep(THROTTLE_PERIOD)
            return mfa_devices

        def get_service_specific_credentials(iam_client, iam_username):
            response = iam_client.list_service_specific_credentials(UserName=iam_username)
            service_specific_credential_ids = []
            for service in response["ServiceSpecificCredentials"]:
                service_specific_credential_ids.append(service["ServiceSpecificCredentialId"])
            return service_specific_credential_ids

        def handler(event, context):
            iam_client = boto3.client("iam")
            iam_username = event["UserName"]

            access_keys = get_access_keys(iam_client, iam_username)
            ssh_public_keys = get_ssh_public_keys(iam_client, iam_username)
            signing_certificates = get_signing_certificates(iam_client, iam_username)
            mfa_devices = get_mfa_devices(iam_client, iam_username)
            service_specific_credentials = get_service_specific_credentials(iam_client, iam_username)

            return {
                "access_keys": access_keys,
                "ssh_public_keys": ssh_public_keys,
                "signing_certificates": signing_certificates,
                "mfa_devices": mfa_devices,
                "service_specific_credentials": service_specific_credentials,
            }
    outputs:
      - Name: AccessKeys
        Selector: $.Payload.access_keys
        Type: StringList
      - Name: SSHPublicKeys
        Selector: $.Payload.ssh_public_keys
        Type: StringList
      - Name: SigningCertificates
        Selector: $.Payload.signing_certificates
        Type: StringList
      - Name: MFADevices
        Selector: $.Payload.mfa_devices
        Type: StringList
      - Name: ServiceSpecificCredentials
        Selector: $.Payload.service_specific_credentials
        Type: StringList
  - description: |
      ## GetGroupsAndPolicies
      Gathers group memberships and policies for the IAM user.
      ## Outputs
      * Output: The group memberships and policies for the AWS IAM user.
    name: GetGroupsAndPolicies
    action: aws:executeScript
    timeoutSeconds: 600
    nextStep: DeleteKeysCertificatesMfaAndCredentials
    isEnd: false
    inputs:
      Runtime: python3.11
      Handler: handler
      InputPayload:
        UserName: '{{ GetUsername.UserName }}'
      Script: |-
        from time import sleep

        import boto3

        THROTTLE_PERIOD = 0.05

        def get_user_groups(iam_client, iam_username):
            paginator = iam_client.get_paginator("list_groups_for_user")
            page_iterator = paginator.paginate(UserName=iam_username)
            groups = []
            for page in page_iterator:
                for group in page["Groups"]:
                    groups.append(group["GroupName"])
                sleep(THROTTLE_PERIOD)
            return groups

        def get_user_policies(iam_client, iam_username):
            paginator = iam_client.get_paginator("list_user_policies")
            page_iterator = paginator.paginate(UserName=iam_username)
            policies = []
            for page in page_iterator:
                policies.extend(page["PolicyNames"])
                sleep(THROTTLE_PERIOD)
            return policies

        def get_attached_user_policies(iam_client, iam_username):
            paginator = iam_client.get_paginator("list_attached_user_policies")
            page_iterator = paginator.paginate(UserName=iam_username)
            policies = []
            for page in page_iterator:
                for policy in page["AttachedPolicies"]:
                    policies.append(policy["PolicyArn"])
                sleep(THROTTLE_PERIOD)
            return policies

        def handler(event, context):
            iam_client = boto3.client("iam")
            iam_username = event["UserName"]

            groups = get_user_groups(iam_client, iam_username)
            user_policies = get_user_policies(iam_client, iam_username)
            attached_user_policies = get_attached_user_policies(iam_client, iam_username)

            return {"groups": groups, "user_policies": user_policies, "attached_user_policies": attached_user_policies}
    outputs:
      - Name: Groups
        Selector: $.Payload.groups
        Type: StringList
      - Name: UserPolicies
        Selector: $.Payload.user_policies
        Type: StringList
      - Name: AttachedUserPolicies
        Selector: $.Payload.attached_user_policies
        Type: StringList
  - description: |
      ## DeleteKeysCertificatesMfaAndCredentials
      Deletes access keys, certificates, credentials, MFA devices, and SSH keys associated with the IAM user.
      ## Outputs
      * Output: The output of this step indicating successful deletion of the access keys, ssh keys, certificates, MFA device and credentials for the AWS IAM user.
    name: DeleteKeysCertificatesMfaAndCredentials
    action: aws:executeScript
    timeoutSeconds: 600
    nextStep: DeleteGroupsAndPolicies
    isEnd: false
    inputs:
      Runtime: python3.11
      Handler: handler
      InputPayload:
        UserName: '{{ GetUsername.UserName }}'
        AccessKeys: '{{ GetKeysCertificatesMfaAndCredentials.AccessKeys }}'
        SSHPublicKeys: '{{ GetKeysCertificatesMfaAndCredentials.SSHPublicKeys }}'
        SigningCertificates: '{{ GetKeysCertificatesMfaAndCredentials.SigningCertificates }}'
        MFADevices: '{{ GetKeysCertificatesMfaAndCredentials.MFADevices }}'
        ServiceSpecificCredentials: '{{ GetKeysCertificatesMfaAndCredentials.ServiceSpecificCredentials }}'
      Script: |-
        import boto3
        from time import sleep
        THROTTLE_PERIOD = 0.05

        def delete_login_profile(iam_client, iam_username):
            try:
                response = iam_client.delete_login_profile(UserName=iam_username)
                return response
            except iam_client.exceptions.NoSuchEntityException:
                return None

        def delete_access_keys(iam_client, iam_username, access_keys):
            responses = []
            for access_key in access_keys:
                response = iam_client.delete_access_key(UserName=iam_username, AccessKeyId=access_key)
                responses.append(response)
            return responses

        def delete_ssh_public_keys(iam_client, iam_username, ssh_public_keys):
            responses = []
            for ssh_key in ssh_public_keys:
                response = iam_client.delete_ssh_public_key(UserName=iam_username, SSHPublicKeyId=ssh_key)
                responses.append(response)
            return responses

        def delete_signing_certificate(iam_client, iam_username, signing_certificates):
            responses = []
            for certificate in signing_certificates:
                response = iam_client.delete_signing_certificate(UserName=iam_username, CertificateId=certificate)
                responses.append(response)
            return responses

        def deactivate_mfa_devices(iam_client, iam_username, mfa_devices):
            responses = []
            for mfa_device in mfa_devices:
                response = iam_client.deactivate_mfa_device(UserName=iam_username, SerialNumber=mfa_device)
                responses.append(response)
            return responses

        def delete_mfa_devices(iam_client, mfa_devices):
            """
            アプローチ1: 事前チェックによる分岐処理
            仮想MFAデバイスかどうかを事前に判定し、該当する場合のみ削除処理を実行
            """
            if not mfa_devices:
                return []

            # 仮想MFAデバイスの一覧を取得
            virtual_mfa_devices = []
            paginator = iam_client.get_paginator("list_virtual_mfa_devices")
            for page in paginator.paginate():
                for device in page["VirtualMFADevices"]:
                    virtual_mfa_devices.append(device["SerialNumber"])
                sleep(THROTTLE_PERIOD)

            # 仮想MFAデバイスのみ削除
            responses = []
            for mfa_device in mfa_devices:
                if mfa_device in virtual_mfa_devices:
                    print(f"Deleting virtual MFA device: {mfa_device}")
                    response = iam_client.delete_virtual_mfa_device(SerialNumber=mfa_device)
                    responses.append(response)
                else:
                    print(f"Skipping non-virtual MFA device: {mfa_device}")

            return responses

        def delete_service_specific_credential(iam_client, iam_username, service_specific_credentials):
            responses = []
            for service_credential in service_specific_credentials:
                response = iam_client.delete_service_specific_credential(
                    UserName=iam_username, ServiceSpecificCredentialId=service_credential
                )
                responses.append(response)
            return responses

        def handler(event, context):
            iam_client = boto3.client("iam")
            iam_username = event["UserName"]
            access_keys = event["AccessKeys"]
            ssh_public_keys = event["SSHPublicKeys"]
            signing_certificates = event["SigningCertificates"]
            mfa_devices = event["MFADevices"]
            service_specific_credentials = event["ServiceSpecificCredentials"]

            delete_login_profile(iam_client, iam_username)
            delete_access_keys(iam_client, iam_username, access_keys)
            delete_ssh_public_keys(iam_client, iam_username, ssh_public_keys)
            delete_signing_certificate(iam_client, iam_username, signing_certificates)
            deactivate_mfa_devices(iam_client, iam_username, mfa_devices)
            delete_mfa_devices(iam_client, mfa_devices)
            delete_service_specific_credential(iam_client, iam_username, service_specific_credentials)

            return "Processed deleting login profile, deleting access keys, deleting ssh public keys, deleting signing certificates, deactivating & deleting MFA devices, and deleting service specific credentials."
    outputs:
      - Name: Output
        Selector: $.Payload.output
        Type: String
  - description: |
      ## DeleteGroupsAndPolicies
      Deletes group memberships and policies for the IAM user.
      ## Outputs
      * Output: The output of this step indicating successful deletion of the group memberships and policies for the AWS IAM User.
    name: DeleteGroupsAndPolicies
    action: aws:executeScript
    timeoutSeconds: 600
    nextStep: DeleteIAMUserAndVerify
    isEnd: false
    inputs:
      Runtime: python3.11
      Handler: handler
      InputPayload:
        UserName: '{{ GetUsername.UserName }}'
        Groups: '{{ GetGroupsAndPolicies.Groups }}'
        UserPolicies: '{{ GetGroupsAndPolicies.UserPolicies }}'
        AttachedUserPolicies: '{{ GetGroupsAndPolicies.AttachedUserPolicies }}'
      Script: |-
        import boto3

        def remove_user_from_group(iam_client, iam_username, groups):
            responses = []
            for group in groups:
                response = iam_client.remove_user_from_group(UserName=iam_username, GroupName=group)
                responses.append(response)
            return responses

        def delete_user_policies(iam_client, iam_username, user_policies):
            responses = []
            for policy in user_policies:
                response = iam_client.delete_user_policy(UserName=iam_username, PolicyName=policy)
                responses.append(response)
            return responses

        def detach_attached_user_policies(iam_client, iam_username, attached_user_policies):
            responses = []
            for policy in attached_user_policies:
                response = iam_client.detach_user_policy(UserName=iam_username, PolicyArn=policy)
                responses.append(response)
            return responses

        def handler(event, context):
            iam_client = boto3.client("iam")
            iam_username = event["UserName"]
            groups = event["Groups"]
            user_policies = event["UserPolicies"]
            attached_user_policies = event["AttachedUserPolicies"]

            remove_user_from_group(iam_client, iam_username, groups)
            delete_user_policies(iam_client, iam_username, user_policies)
            detach_attached_user_policies(iam_client, iam_username, attached_user_policies)

            return "Processed removal of user from groups, deleting user policies, and detaching user attached policies."
    outputs:
      - Name: Output
        Selector: $.Payload.output
        Type: String
  - description: |
      ## DeleteIAMUserAndVerify
      Deletes the IAM user and verifies the user has been deleted.
      ## Outputs
      * Output: A success message or failure exception.
    name: DeleteIAMUserAndVerify
    action: aws:executeScript
    timeoutSeconds: 600
    isEnd: true
    inputs:
      Runtime: python3.11
      Handler: handler
      InputPayload:
        UserName: '{{ GetUsername.UserName }}'
      Script: |-
        import boto3

        def delete_iam_user(iam_client, iam_username):
            response = iam_client.delete_user(UserName=iam_username)
            return response

        def verify_iam_user_status(iam_client, iam_username):
            try:
                iam_client.get_user(UserName=iam_username)
                error_message = f"VERIFICATION FAILED. AWS IAM USER {iam_username} DELETION UNSUCCESSFUL."
                raise Exception(error_message)
            except iam_client.exceptions.NoSuchEntityException:
                return {"output": "Verification of AWS IAM user deletion is successful."}

        def handler(event, context):
            iam_client = boto3.client("iam")
            iam_username = event["UserName"]
            response = delete_iam_user(iam_client, iam_username)
            results = verify_iam_user_status(iam_client, iam_username)
            results["DeleteUserResponse"] = response
            return results
    outputs:
      - Name: Output
        Selector: $.Payload.output
        Type: String
      - Name: DeleteUserResponse
        Selector: $.Payload.DeleteUserResponse
        Type: StringMap
outputs:
  - DeleteIAMUserAndVerify.Output

		

このアプローチでは権限の追加が必要です。

以下の記事で作成したステートマシンの用のIAMロールとAutomationを実行する際のIAMロールAWSConfigRemediation-DeleteIAMUserRoleに、それぞれ追加の権限が必要です。

https://dev.classmethod.jp/articles/aws-stepfunctions-iam-user-delete/

ステートマシンのIAMロールには、以下の権限を追加します。

			
			    {
      "Effect": "Allow",
      "Action": ["ssm:StartAutomationExecution"],
      "Resource": [
        "arn:aws:ssm:*:211125575342:document/*",
        "arn:aws:ssm:*:211125575342:automation-execution/*"
      ]
    },

		

Automationを実行する際のIAMロールAWSConfigRemediation-DeleteIAMUserRoleに以下の権限を追加します。

			
			    {
      "Action": ["iam:ListVirtualMfaDevices"],
      "Resource": "*",
      "Effect": "Allow"
    },

		

iam:ListVirtualMfaDevicesの権限がない場合、以下のエラーが発生します

			
			ClientError: An error occurred (AccessDenied) when calling the ListVirtualMFADevices operation: User: arn:aws:sts::アカウントID:assumed-role/AWSConfigRemediation-DeleteIAMUserRole/Automation-xxx is not authorized to perform: iam:ListVirtualMFADevices

		

アプローチ2:例外処理による対応

NoSuchEntityExceptionエラーをキャッチして処理を継続する方法です。

元のコード:

			
			def delete_mfa_devices(iam_client, mfa_devices):
    responses = []
    for mfa_device in mfa_devices:
        response = iam_client.delete_virtual_mfa_device(SerialNumber=mfa_device)
        responses.append(response)
    return responses

		

修正後のコード:

			
			def delete_mfa_devices(iam_client, mfa_devices):
    """
    アプローチ2: 例外処理による対応
    NoSuchEntityExceptionエラーをキャッチして処理を継続
    """
    responses = []
    for mfa_device in mfa_devices:
        try:
            response = iam_client.delete_virtual_mfa_device(SerialNumber=mfa_device)
            responses.append(response)
            print(f"Successfully deleted virtual MFA device: {mfa_device}")
        except iam_client.exceptions.NoSuchEntityException:
            print(f"Non-virtual MFA device detected: {mfa_device}. Skipping deletion.")
        except Exception as e:
            print(f"Unexpected error for device {mfa_device}: {str(e)}")
            raise

    return responses

		

このアプローチでは権限の追加が必要です。

以下の記事で作成したステートマシンの用のIAMロールに追加の権限が必要です。
https://dev.classmethod.jp/articles/aws-stepfunctions-iam-user-delete/

ステートマシンのIAMロールには、以下の権限を追加します。

			
			    {
      "Effect": "Allow",
      "Action": ["ssm:StartAutomationExecution"],
      "Resource": [
        "arn:aws:ssm:*:211125575342:document/*",
        "arn:aws:ssm:*:211125575342:automation-execution/*"
      ]
    },

		

テスト結果の例

修正後のドキュメントでは以下のような動作になります。

			
			実行前:IAMユーザー「iam-passkey」にパスキー/セキュリティキーが設定

実行中:
  1. MFAデバイス情報取得 → 成功
  2. MFAデバイスデアクティベート → 成功  
  3. MFAデバイス削除 → 仮想デバイスでないためスキップ
  4. IAMユーザー削除 → 成功

実行後:IAMユーザーが正常に削除される(1回で完了)

		

cm-hirai-screenshot 2025-08-25 14.45.13

最後に

AWSConfigRemediation-DeleteIAMUserドキュメントの現在の実装では、すべてのMFAデバイスに対して仮想MFAデバイス専用のAPI(delete_virtual_mfa_device)を実行してしまうため、仮想認証アプリケーション以外のMFAデバイスを持つIAMユーザーの削除が1回の実行では正常に完了しません。

根本的な問題

  • MFAデバイスの種類を判定するロジックが実装されていない
  • 仮想MFAデバイス専用APIをすべてのMFAデバイスに適用している

今後、AWS側でドキュメントが修正される可能性もありますが、現時点で同様の問題に遭遇された場合は、本記事で紹介したカスタマイズ方法をご参考ください。

この記事をシェアする

FacebookHatena blogX

関連記事