AWSConfigRemediation-DeleteIAMUserドキュメントは、仮想認証アプリケーション以外のMFAデバイスタイプの場合、実行に失敗します
はじめに
以前、AWS Step FunctionsでAWS Systems Manager(以下、SSM)ドキュメントAWSConfigRemediation-DeleteIAMUser
を利用したSSMオートメーションを実行し、IAMユーザーを削除する記事を書きました。
上記記事の方法でIAMユーザーを削除していたところ、「仮想MFAデバイス」以外のMFAデバイスタイプが設定されているユーザーで実行が失敗する事象を確認しました。
AWSにおけるIAMユーザーに設定可能なMFAデバイスタイプは、以下の3つのタイプがあります。
- 仮想認証アプリケーション(Google AuthenticatorやMicrosoft Authenticatorなどのアプリ)
- ハードウェアTOTPトークン(YubiKeyなど)
- パスキーまたはセキュリティキー
このうち、「仮想MFAデバイス」として分類されるのは仮想認証アプリケーションのみです。
本記事では、AWSConfigRemediation-DeleteIAMUserドキュメントが仮想認証アプリケーション以外のMFAデバイス(パスキー/セキュリティキーやハードウェアTOTPトークン)を持つIAMユーザーの削除に失敗する理由と、その対応方法について解説します。
発生した事象
実際に発生した事象は以下のとおりです。
- 1回目の実行:パスキー/セキュリティキーを設定したIAMユーザーに対する削除実行でエラー発生
- 2回目の実行:同じIAMユーザーが正常に削除される
1回目
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) デバイス
この問題は、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回目の実行:
get_mfa_devices()
→ パスキーのシリアル番号を取得deactivate_mfa_devices()
→ パスキーのデアクティベート成功delete_mfa_devices()
→ パスキーに対してdelete_virtual_mfa_device
実行 → エラー発生
2回目の実行:
get_mfa_devices()
→ MFAデバイスが既にデアクティベートされているため空のリストを返すdeactivate_mfa_devices()
→ 処理対象なし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
に、それぞれ追加の権限が必要です。
ステートマシンの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ロールに追加の権限が必要です。
ステートマシンの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回で完了)
最後に
AWSConfigRemediation-DeleteIAMUserドキュメントの現在の実装では、すべてのMFAデバイスに対して仮想MFAデバイス専用のAPI(delete_virtual_mfa_device
)を実行してしまうため、仮想認証アプリケーション以外のMFAデバイスを持つIAMユーザーの削除が1回の実行では正常に完了しません。
根本的な問題
- MFAデバイスの種類を判定するロジックが実装されていない
- 仮想MFAデバイス専用APIをすべてのMFAデバイスに適用している
今後、AWS側でドキュメントが修正される可能性もありますが、現時点で同様の問題に遭遇された場合は、本記事で紹介したカスタマイズ方法をご参考ください。