AWS Config + Lambdaで特定ユーザーを除外しつつ未使用IAMユーザーを自動無効化してみた

AWS Config + Lambdaで特定ユーザーを除外しつつ未使用IAMユーザーを自動無効化してみた

2025.08.28

はじめに

みなさんこんにちは、クラウド事業本部コンサルティング部の浅野です。

AWS Config のマネージドルール 「iam-user-unused-credentials-check」 を使うと、一定期間パスワードやアクセスキーを利用していない IAM ユーザーを「非準拠」として検出し、自動的に SSM Document を使って無効化してくれます。

ただし、実際の運用では 利用頻度は少ないけれど重要な管理者アカウントや運用用アカウント など、無効化の対象から外したいユーザーが存在する場合があります。

そこで今回は、特定の IAM ユーザーを除外できる仕組みも含めて、CloudFormation で AWS Config のカスタム Lambda ルールを構築し、未使用の IAM ユーザーを自動的に無効化するシステムを一括でデプロイしてみました。

マネージドルール 「iam-user-unused-credentials-check」 を使って、一定期間未使用の IAM ユーザーを無効化する方法については、以下のブログが参考になります!

https://dev.classmethod.jp/articles/automation-revoke-unused-iam-user-credentials/

CloudFormation

ソースコード
GitHub - haruki-0408/aws-iam-unused-user-cleanup

以下のリソースタイプを内包しています。

  • AWS::IAM::Role: カスタムLambda Config規則用の実行ロール
  • AWS::Lambda::Function: 未使用IAMユーザーを検知するカスタムLambda関数
  • AWS::Lambda::Permission: LambdaにConfig権限を付与
  • AWS::Config::ConfigRule: カスタムConfig規則(24時間間隔で定期実行)
  • AWS::IAM::Role: Config自動修復用のサービスロール
  • AWS::Config::RemediationConfiguration: 自動修復設定(SSM Documentによる無効化実行)
AWSTemplateFormatVersion: '2010-09-09'
Description: Detect unused IAM users with custom Lambda Config rule and auto-remediate excluding specific IAM users

Parameters:
  ExcludedUsersParameterName:
    Type: String
    Default: "/iam-unused-user-cleanup/excluded-users"
    Description: SSM Parameter Store path for excluded IAM users list
  
  MaxCredentialUsageAge:
    Type: String
    Default: "30"
    Description: Maximum number of days a credential can be unused before being revoked

Resources:

  # カスタムLambda Config規則用の実行ロール
  ConfigRuleLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: ConfigRuleLambdaRole
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
        - arn:aws:iam::aws:policy/service-role/AWSConfigRulesExecutionRole
      Policies:
        - PolicyName: IAMReadAndParameterAccess
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - iam:ListUsers
                  - iam:GetUser
                  - iam:GetLoginProfile
                  - iam:ListAccessKeys
                  - iam:GetAccessKeyLastUsed
                  - ssm:GetParameter
                Resource: "*"

  # カスタムLambda Config規則
  ConfigRuleLambda:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: custom-unused-iam-user-credentials-check-function
      Runtime: python3.13
      Timeout: 300
      MemorySize: 256
      Handler: index.lambda_handler
      Role: !GetAtt ConfigRuleLambdaRole.Arn
      Environment:
        Variables:
          EXCLUDED_USERS_PARAMETER: !Ref ExcludedUsersParameterName
          MAX_CREDENTIAL_AGE: !Ref MaxCredentialUsageAge
      Code:
        ZipFile: |
          import boto3
          import os
          from datetime import datetime, timedelta, timezone
          
          iam = boto3.client('iam')
          ssm = boto3.client('ssm')
          config_client = boto3.client('config')
          
          def lambda_handler(event, context):
              evaluations = []
              excluded_users = get_excluded_users()
              max_days = int(os.environ.get('MAX_CREDENTIAL_AGE', '90'))
              
              # 全IAMユーザーを取得して評価
              paginator = iam.get_paginator('list_users')
              for page in paginator.paginate():
                  for user in page['Users']:
                      user_name = user['UserName']
                      print(f"評価中: {user_name}")
                      
                      if user_name in excluded_users:
                          compliance = 'COMPLIANT'
                          print(f"除外ユーザー: {user_name}")
                      elif is_credentials_unused(user_name, max_days):
                          compliance = 'NON_COMPLIANT'
                          print(f"未使用ユーザー: {user_name}")
                      else:
                          compliance = 'COMPLIANT'
                          print(f"使用中ユーザー: {user_name}")
                      
                      evaluations.append({
                          'ComplianceResourceType': 'AWS::IAM::User',
                          'ComplianceResourceId': user['UserId'],
                          'ComplianceType': compliance,
                          'OrderingTimestamp': datetime.now(timezone.utc)
                      })
              
              # Config に評価結果を送信
              if evaluations:
                  config_client.put_evaluations(
                      Evaluations=evaluations,
                      ResultToken=event['resultToken']
                  )
              
              print(f"評価完了: {len(evaluations)}ユーザー")
              return {'evaluations_count': len(evaluations)}
          
          def get_excluded_users():
              parameter_name = os.environ.get('EXCLUDED_USERS_PARAMETER', '')
              if not parameter_name:
                  return []
              
              try:
                  response = ssm.get_parameter(Name=parameter_name, WithDecryption=False)
                  return [u.strip() for u in response['Parameter']['Value'].split(',') if u.strip()]
              except ssm.exceptions.ParameterNotFound:
                  print(f"除外リストパラメータが見つかりません: {parameter_name}")
                  return []
          
          def is_credentials_unused(user_name, max_days):
              cutoff_date = datetime.now(timezone.utc) - timedelta(days=max_days)
              print(f"カットオフ日: {cutoff_date}")
              
              # パスワード使用チェック(修復ロジックに合わせて)
              user_info = iam.get_user(UserName=user_name)
              password_valid = True  # デフォルトは有効
              
              try:
                  login_profile = iam.get_login_profile(UserName=user_name)
                  password_needs_remediation = False
                  
                  if 'PasswordLastUsed' in user_info['User']:
                      last_used = user_info['User']['PasswordLastUsed']
                      last_used_days = (datetime.now(timezone.utc) - last_used).days
                      print(f"パスワード最終使用: {last_used} ({last_used_days}日前)")
                      if last_used_days >= max_days:
                          password_needs_remediation = True
                  else:
                      # パスワード未使用の場合、作成日をチェック
                      create_date = login_profile['LoginProfile']['CreateDate'].replace(tzinfo=timezone.utc)
                      create_days = (datetime.now(timezone.utc) - create_date).days
                      print(f"パスワード作成日: {create_date} ({create_days}日前)")
                      if create_days >= max_days:
                          password_needs_remediation = True
                  
                  if password_needs_remediation:
                      print(f"期間超過パスワード検出: {user_name}")
                      password_valid = False
                      
              except iam.exceptions.NoSuchEntityException:
                  print(f"ログインプロファイルなし: {user_name}")
                  # パスワードがない場合は有効とみなす(修復対象外)
              
              # アクセスキー使用チェック(修復ロジックに合わせて個別評価)
              access_keys = iam.list_access_keys(UserName=user_name)
              access_key_valid = True  # デフォルトは有効
              
              if not access_keys['AccessKeyMetadata']:
                  print(f"アクセスキーなし: {user_name}")
              else:
                  for key in access_keys['AccessKeyMetadata']:
                      if key['Status'] == 'Active':  # Activeキーのみチェック
                          key_usage = iam.get_access_key_last_used(AccessKeyId=key['AccessKeyId'])
                          key_needs_remediation = False
                          
                          if 'LastUsedDate' in key_usage['AccessKeyLastUsed']:
                              last_used = key_usage['AccessKeyLastUsed']['LastUsedDate']
                              last_used_days = (datetime.now(timezone.utc) - last_used).days
                              print(f"アクセスキー最終使用: {last_used} ({last_used_days}日前)")
                              if last_used_days >= max_days:
                                  key_needs_remediation = True
                          else:
                              # アクセスキー未使用の場合、作成日をチェック
                              create_date = key['CreateDate']
                              create_days = (datetime.now(timezone.utc) - create_date).days
                              print(f"アクセスキー作成日: {create_date} ({create_days}日前)")
                              if create_days >= max_days:
                                  key_needs_remediation = True
                          
                          if key_needs_remediation:
                              print(f"期間超過アクセスキー検出: {key['AccessKeyId']}")
                              access_key_valid = False  # 一つでも期間超過があれば無効
              
              # OR条件: どちらか一方でも無効(修復が必要)な場合はNON_COMPLIANT
              result = not password_valid or not access_key_valid
              print(f"パスワード有効: {password_valid}, アクセスキー有効: {access_key_valid}, 未使用判定: {result}")
              return result

  # Lambdaに対するConfig権限
  ConfigRuleLambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref ConfigRuleLambda
      Action: lambda:InvokeFunction
      Principal: config.amazonaws.com
      SourceAccount: !Ref AWS::AccountId

  # カスタムConfig規則
  ConfigRule:
    Type: AWS::Config::ConfigRule
    DependsOn: ConfigRuleLambdaPermission
    Properties:
      ConfigRuleName: custom-unused-iam-user-credentials-check
      Description: "Detect IAM users with unused credentials (excluding specified users)"
      Source:
        Owner: CUSTOM_LAMBDA
        SourceIdentifier: !GetAtt ConfigRuleLambda.Arn
        SourceDetails:
          - EventSource: aws.config
            MessageType: ScheduledNotification
            MaximumExecutionFrequency: TwentyFour_Hours
      Scope:
        ComplianceResourceTypes:
          - "AWS::IAM::User"

  # Config Remediation用のサービスロール
  RemediationServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: ConfigRemediationServiceRole
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: ssm.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: IAMUserManagementPolicy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - config:ListDiscoveredResources
                  - ssm:GetAutomationExecution
                  - iam:DeleteAccessKey
                  - ssm:StartAutomationExecution
                  - iam:GetAccessKeyLastUsed
                  - iam:UpdateAccessKey
                  - iam:GetUser
                  - iam:GetLoginProfile
                  - iam:DeleteLoginProfile
                  - iam:ListAccessKeys
                Resource: "*"

  # Config Remediation設定
  RemediationConfiguration:
    Type: AWS::Config::RemediationConfiguration
    Properties:
      ConfigRuleName: !Ref ConfigRule
      TargetType: SSM_DOCUMENT
      TargetId: AWSConfigRemediation-RevokeUnusedIAMUserCredentials
      TargetVersion: "5"
      Parameters:
        AutomationAssumeRole:
          StaticValue:
            Values:
              - !GetAtt RemediationServiceRole.Arn
        IAMResourceId:
          ResourceValue:
            Value: RESOURCE_ID
        MaxCredentialUsageAge:
          StaticValue:
            Values:
              - !Ref MaxCredentialUsageAge
      Automatic: true
      MaximumAutomaticAttempts: 5
      RetryAttemptSeconds: 60


以下の項目はCloudFormation外で管理しています。

  • 除外ユーザーリスト: SSM Parameter Storeで管理
    • キー: /iam-unused-user-cleanup/excluded-users
    • 値の例: admin-user,emergency-user,service-account
    • 形式: カンマ区切りの文字列(StringList)で複数のIAMユーザー名を指定

また、以下の箇所は自由に設定可能です。

  • Configルール評価の実行頻度: 3時間毎、6時間毎、12時間毎、24時間毎、週1回から選択可能(CloudFormationテンプレートで設定)
  • 非準拠リソース自動修復のリトライ回数: デフォルト5回(修復失敗時の最大リトライ回数)
  • リトライ間隔: デフォルト60秒(修復リトライの実行間隔)
  • IAMユーザーを「未使用」と定義する対象日数: デフォルト90日(CloudFormationパラメータ MaxCredentialUsageAge で変更可能)

処理フロー

今回作成したシステムの全体フローは以下に従います。
IAMユーザーの未使用判定を、後のSSM修復ドキュメント「AWSConfigRemediation-RevokeUnusedIAMUserCredentials」のスクリプトにおける判定内容と揃えなければ誤検知や未修復につながる可能性があるので注意が必要でした。

パスワードとアクセスキーの有効/無効検知の箇所はSSM修復ドキュメントのバージョン5内のスクリプトとロジックを合わせており、修復ドキュメントのバージョンをCloudFormationで変更する場合はカスタムLambdaルールとロジックがずれないか注意が必要です。

動作確認

今回は「未使用」と定義する日数(MaxCredentialUsageAge)を30に設定し、CloudFormationをデプロイしました。
これにより30日以上パスワードまたはアクセスキーが未使用のIAMユーザーが「非準拠」の対象となります。
では実際に動作を確認していきましょう。

1. 検証用IAMユーザー

今回は検証用として以下IAMユーザーを3つ作成しています。

2025-08-28-aws-config-lambda-unused-iam-user-cleanup-01

ユーザー名 最後のアクティビティ パスワードが作成されてから経過した期間 コンソールの最終サインイン アクセスキー ID アクティブなキーが作成されてから経過した期間
test-active-user 48 日前 49 日 July 10, 2025, 11:21 (UTC+09:00) - -
test-active-user2 - 48 日 - Active - AKIA*********** 57 分
test-active-user3 - 17 時間 - - -

各ユーザーはConfigルールにより以下のように評価されるはずです。

  • test-active-user : コンソールにサインイン可能かつアクセスキーを持ちません。最後にログインしたのが現在から48日前 → 「非準拠」対象
  • test-active-user2 : コンソールにサインイン可能かつアクセスキーを1時間前に作成、パスワードが作成されてから48日以上未ログイン → 「非準拠」対象
  • test-active-user2 : コンソールにサインイン可能かつアクセスキーを持ちません。未ログイン → 「準拠」対象

2. 除外リストへの登録

動作を検証する前に、今回機能の目玉であるSSM Parameter Storeにこのルールから除外したいIAMユーザーリストを作成します。

以下のようにtest-active-userをConfigカスタムルールの対象外として登録しました。

2025-08-28-aws-config-lambda-unused-iam-user-cleanup-02

本来であればtest-active-userは最終的なパスワードの使用(コンソールの最終サインイン)から30日以上経過しているため、「非準拠」と判定されるのですが、この除外リストに登録することで検知対象外とさせます。

3. 検知内容

デプロイすると以下のConfigカスタムルールが作成されました。
2025-08-28-aws-config-lambda-unused-iam-user-cleanup-03

2025-08-28-aws-config-lambda-unused-iam-user-cleanup-04

修復ドキュメント上の設定値として無事にMaxCredentialUsageAge30に設定されています。

カスタムルールの実態はLambdaコンソール画面ににあるこの関数ですね。

2025-08-28-aws-config-lambda-unused-iam-user-cleanup-05

2025-08-28-aws-config-lambda-unused-iam-user-cleanup-06

Lambdaの環境変数として、除外リストの場所と、こちらにも「未使用」と判定する日数を定義しています。この値と先ほどの修復ドキュメントの上の値がズレると動作がおかしくなるので合わせてください。
今回のプロジェクトではCloudFormationのパラメータとして統一しているのでその部分だけ変更すればよいです。

続いてこのルールによって検知されたリソースを確認していきます。

2025-08-28-aws-config-lambda-unused-iam-user-cleanup-07

仕様通りtest-active-user, test-active-user3は評価により「準拠」と判断されました。

2025-08-28-aws-config-lambda-unused-iam-user-cleanup-08

こちらも予想通りきちんとtest-active-user2が「非準拠」と判定されています!

カスタムLambdaルールのログ内容を確認してみましょう。それぞれどのようなロジックで評価されたかが確認できます。

2025-08-28-aws-config-lambda-unused-iam-user-cleanup-09
適切に評価ロジックが通っていることが確認できました。素晴らしい〜

また「非準拠」となったリソースを自動で修復する設定にしているので、数分後以下のように「アクションが正常に実行されたました」と出てtest-active-user2の修復が終えたことがわかります。

2025-08-28-aws-config-lambda-unused-iam-user-cleanup-10

AWS CLIでも修復実行に関する記録を詳しくみてみましょう。

# 修復実行ステータスを確認するコマンド
aws configservice describe-remediation-execution-status --config-rule-name custom-unused-iam-user-credentials-check \

{
    "RemediationExecutionStatuses": [
        {
            "ResourceKey": {
                "resourceType": "AWS::IAM::User",
                "resourceId": "******************"
            },
            "State": "SUCCEEDED",
            "StepDetails": [
                {
                    "Name": "RevokeUnusedIAMUserCredentialsAndVerify",
                    "State": "SUCCEEDED",
                    "StartTime": "2025-08-28T11:27:52.846000+09:00",
                    "StopTime": "2025-08-28T11:28:06.138000+09:00"
                }
            ],
            "InvocationTime": "2025-08-28T11:27:52.515000+09:00",
            "LastUpdatedTime": "2025-08-28T11:28:06.514000+09:00"
        }
    ]
}

設定した修復アクションが実行され、成功ステータスに変わっています。

SSM ドキュメントの実行ログも見てます。

aws ssm describe-automation-executions \
    --filters "Key=DocumentNamePrefix,Values=AWSConfigRemediation-RevokeUnusedIAMUserCredentials" \
    --max-results 1 
{
    {
        "AutomationExecutionMetadataList": [
            {
                "AutomationExecutionId": "******************",
                "DocumentName": "AWSConfigRemediation-RevokeUnusedIAMUserCredentials",
                "DocumentVersion": "5",
                "AutomationExecutionStatus": "Success",
                "ExecutionStartTime": "2025-08-28T11:27:52.370000+09:00",
                "ExecutionEndTime": "2025-08-28T11:28:06.197000+09:00",
                "ExecutedBy": "arn:aws:sts::**********:assumed-role/AWSServiceRoleForConfigRemediation/AwsConfigRemediation",
                "LogFile": "",
                "Outputs": {
                    "RevokeUnusedIAMUserCredentialsAndVerify.Output": [
                        "{\"output\":\"Verification of unused IAM User credentials is successful.\",\"http_responses\":{\"DeactivateUnusedKeysResponse\":[],\"DeleteUnusedPasswordResponse\":{\"ResponseMetadata\":{\"RequestId\":\"22bce8dc-9371-46f3-a366-abda08b72b9d\",\"HTTPStatusCode\":200,\"HTTPHeaders\":{\"date\":\"Thu, 28 Aug 2025 02:28:01 GMT\",\"x-amzn-requestid\":\"22bce8dc-9371-46f3-a366-abda08b72b9d\",\"content-type\":\"text/xml\",\"content-length\":\"216\"},\"RetryAttempts\":0}}}}"
                    ]
                },
                "Mode": "Auto",
                "Targets": [],
                "ResolvedTargets": {
                    "ParameterValues": [],
                    "Truncated": false
                },
                "AutomationType": "Local"
            }
        ],
        "NextToken": "**********"
}

きちんとドキュメントバージョン5を使用しておりDeleteUnusedPasswordResponseの記載があるので未使用のIAMユーザーからパスワードが削除されたことがわかります。

実際にコンソール上で確認してみましょう。

2025-08-28-aws-config-lambda-unused-iam-user-cleanup-11

無事test-active-user2の「コンソールを通じたアクセス」が「無効」に切り替わっておりパスワードが削除されたことが確認できました!

さらに修復後、Configルールの再評価を行うと「非準拠」リソースが修復されて全て「準拠」に変わったことも確認できました。

最後に

今回はカスタムLambdaルールを用いてAWS Configのルール評価により一定期間「パスワード」 or 「アクセスキー」が未使用のユーザーを検知して無効化するシステムを作成してみました。
SSM Parameter Storeに除外リストを作成することで、より柔軟な設定が可能なシステムを目指しました。
このシステムとブログが誰かの役に立てば幸いです。

この記事をシェアする

facebookのロゴhatenaのロゴtwitterのロゴ

© Classmethod, Inc. All rights reserved.