AWS Configを使用して、古いアクセスキーを検出したら、SSMランブックを使って無効化する

AWS Configを使用して、古いアクセスキーを検出したら、SSMランブックを使って無効化する

AWS SOAの出題範囲の1つである、モニタリング、ロギング、および修復に関する内容です。

以下のページで、AWS Configを使用して、古いアクセスキーを検出する方法をご紹介しました。

あわせて読みたい
AWS Configを使用して、古いアクセスキーを検出する 【AWS Configを使用して、古いアクセスキーを検出する】 以下のページで、AWS Config入門ということで、S3バケットのロギング設定を監査する方法をご紹介しました。 htt...

本ページでは、その発展ということで、SSMランブックを使用して、自動的に検出した古いアクセスキーを無効化する方法を確認します。

構築する環境

Diagram of using AWS Config to detect old access keys and disable them with SSM runbook.

構成は先述のページと概ね同様です。

異なる点は、修復アクションを設定する点です。
AWS Configルールによって、古いアクセスキーが検出されて、非準拠と判定されると、SSMランブックを使用して、自動的にそのキーを無効化します。

CloudFormationテンプレートファイル

上記の構成をCloudFormationで構築します。
以下のURLにCloudFormationテンプレートを配置しています。

https://github.com/awstut-an-r/awstut-soa/tree/main/01/002

テンプレートファイルのポイント解説

本ページでは、AWS Configによって検出された古いアクセスキーを、SSMランブックを使用して、自動的に無効化する方法を中心に取り上げます。

AWS Configを使って古いアクセスキーを検出する方法については、以下のページをご確認ください。

あわせて読みたい
AWS Configを使用して、古いアクセスキーを検出する 【AWS Configを使用して、古いアクセスキーを検出する】 以下のページで、AWS Config入門ということで、S3バケットのロギング設定を監査する方法をご紹介しました。 htt...

AWS Configルール

Resources:
  ConfigRule:
    Type: AWS::Config::ConfigRule
    DependsOn:
      - ConfigurationRecorder
    Properties:
      ConfigRuleName: !Sub "${Prefix}-IAM-Access-Keys-Rotated"
      InputParameters:
        maxAccessKeyAge: !Ref MaxCredentialUsageAge
      Source:
        Owner: AWS
        SourceIdentifier: ACCESS_KEYS_ROTATED
Code language: YAML (yaml)

ローテーションされていない古いアクセスキーを検出するためには、マネージドルールaccess-key-rotatedが使用できます。

https://docs.aws.amazon.com/ja_jp/config/latest/developerguide/access-keys-rotated.html

SourceIdentifierプロパティに「ACCESS_KEYS_ROTATED」を指定します。
maxAccessKeyAgeプロパティに「90」を指定し、90日を過ぎたアクセスキー(IAMユーザ)を非準拠のリソースとします。

AWS Config修復アクション

Resources:
  RemediationConfiguration:
    Type: AWS::Config::RemediationConfiguration
    Properties:
      Automatic: true
      ConfigRuleName: !Ref ConfigRule
      MaximumAutomaticAttempts: 5
      Parameters:
        AutomationAssumeRole:
          StaticValue:
            Values:
              - !GetAtt RemediationConfigurationRole.Arn
        IAMResourceId:
          ResourceValue:
            Value: RESOURCE_ID
        MaxCredentialUsageAge:
          StaticValue:
            Values:
              - !Ref MaxCredentialUsageAge
      RetryAttemptSeconds: 60
      TargetId: AWSConfigRemediation-RevokeUnusedIAMUserCredentials
      TargetType: SSM_DOCUMENT
      TargetVersion: "1"
Code language: YAML (yaml)

修復アクションに関する基本的な事項については、以下のページをご確認ください。

あわせて読みたい
AWS Configで非準拠リソースを検出時に修復アクションを設定する 【AWS Configで非準拠リソースを検出時に修復アクションを設定する】 AWS SOAの出題範囲の1つである、モニタリング、ロギング、および修復に関する内容です。 AWS Confi...

ポイントは2点です。

1点目は使用するSSMランブックです。
今回の要件ですと、AWSConfigRemediation-RevokeUnusedIAMUserCredentialsが使えます。

The AWSConfigRemediation-RevokeUnusedIAMUserCredentials runbook revokes unused AWS Identity and Access Management (IAM) passwords and active access keys. This runbook also deactivates expired access keys, and deletes expired login profiles.

AWSConfigRemediation-RevokeUnusedIAMUserCredentials

TargetIdプロパティにブック名を指定します。
2点目は本ブック用のパラメータです。このブックは以下の3つのパラメータを取ります。

AutomationAssumeRole
Type: String
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.

IAMResourceId
Type: String
Description: (Required) The ID of the IAM resource you want to revoke unused credentials from.

MaxCredentialUsageAge
Type: String
Default: 90
Description: (Required) The number of days within which the credential must have been used.

AWSConfigRemediation-RevokeUnusedIAMUserCredentials

IAMResourceIdは検出されたリソース毎に異なる動的な値です。
動的な値の場合は、ResourceValueプロパティを使用し、内部のValueプロパティに「RESOURCE_ID」を指定します。

MaxCredentialUsageAgeに「90」を指定します。
これで90日を超えて使用されていないアクセスキーが無効化の対象となります。

AutomationAssumeRoleに以下のIAMロールを指定します。

Resources:
  RemediationConfigurationRole:
    Type: AWS::IAM::Role
    DeletionPolicy: Delete
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service:
                - ssm.amazonaws.com
      Policies:
        - PolicyName: RemediationConfigurationPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - ssm:StartAutomationExecution
                  - ssm:GetAutomationExecution
                  - config:ListDiscoveredResources
                  - iam:DeleteAccessKey
                  - iam:DeleteLoginProfile
                  - iam:GetAccessKeyLastUsed
                  - iam:GetLoginProfile
                  - iam:GetUser
                  - iam:ListAccessKeys
                  - iam:UpdateAccessKey
                Resource: "*"
Code language: YAML (yaml)

本ブックを実行するために必要な権限も、先述のページにまとめられています。
合計10アクションの権限を与えます。

環境構築

CloudFormationを使用して、本環境を構築し、実際の挙動を確認します。

CloudFormationスタックを作成し、スタック内のリソースを確認する

CloudFormationスタックを作成します。
スタックの作成および各スタックの確認方法については、以下のページをご確認ください。

あわせて読みたい
CloudFormationのネストされたスタックで環境を構築する 【CloudFormationのネストされたスタックで環境を構築する方法】 CloudFormationにおけるネストされたスタックを検証します。 CloudFormationでは、スタックをネストす...

各スタックのリソースを確認した結果、今回作成された主要リソースの情報は以下の通りです。

  • AWS Configルール:soa-01-002-IAM-Access-Keys-Rotated

AWS Management Consoleから各リソースを確認します。

AWS Configルールを確認します。

Detail of AWS Config 1.

ルールが正常に作成されています。
加えて修復アクションも作成されています。

監査結果を確認します。

Detail of AWS Config 2.

IAMユーザ2名が非準拠との判定となりました。

Statusの列を見ると、修復アクションが実行されたことがわかります。

各ユーザのアクセスキーの状況を確認します。

Detail of IAM 1.
Detail of IAM 2.

それぞれ作成から90日以上が経過していますが、結果が分かれています。
一方は無効化されていますが、もう一方は有効化されたままです。

各ユーザに対して実行されたSSMオートメーションのログを確認します。

まずアクセスキーが無効化されたユーザに対する実行ログを確認します。

Detail of SSM 1.

Outputsを見ると、確かにこのユーザに紐づくアクセスキーに対して無効化処理が実行されたことがわかります。

一方、アクセスキーが有効のままのユーザに対する実行ログを確認します。

Detail of SSM 2.

Outputsを見ると、「successful」とはありますが、アクセスキーが選択されておらず、何も処理を実行していないことがわかります。

これはSSMランブックAWSConfigRemediation-RevokeUnusedIAMUserCredentialsの仕様によるものです。
名前の通り、本ブックは使用されていないクリデンシャル(アクセスキー等)を対象とします。
つまりたとえAWS Configルールの期日を超過していたとしても、最近使用した実績があるアクセスキーは、本ランブックによる修復アクションの対象外となります。

このことはAWSConfigRemediation-RevokeUnusedIAMUserCredentialsのコードからも判断できます。
以下に本ブックのコードを引用します。

schemaVersion: "0.3"
description: |
   ### Document Name - AWSConfigRemediation-RevokeUnusedIAMUserCredentials

   ## What does this document do?
   This document revokes unused IAM passwords and active access keys. This document will deactivate expired access keys by using the [UpdateAccessKey API](https://docs.aws.amazon.com/IAM/latest/APIReference/API_UpdateAccessKey.html) and delete expired login profiles by using the [DeleteLoginProfile API](https://docs.aws.amazon.com/IAM/latest/APIReference/API_DeleteLoginProfile.html). Please note, this automation document requires AWS Config to be enabled.

   ## Input Parameters
   * AutomationAssumeRole: (Required) The ARN of the role that allows Automation to perform the actions on your behalf.
   * IAMResourceId: (Required) IAM resource unique identifier.
   * MaxCredentialUsageAge: (Required) Maximum number of days within which a credential must be used. The default value is 90 days.

   ## Output Parameters
   * RevokeUnusedIAMUserCredentialsAndVerify.Output - Success message or failure Exception.

assumeRole: "{{ AutomationAssumeRole }}"
parameters:
  AutomationAssumeRole:
    type: String
    description: (Required) The ARN of the role that allows Automation to perform the actions on your behalf.
    allowedPattern: '^arn:aws(-cn|-us-gov)?:iam::\d{12}:role\/[\w+=,.@_\/-]+|^$'
  IAMResourceId:
    type: String
    description: (Required) IAM resource unique identifier.
    allowedPattern: ^[\w+=,.@_-]{1,128}$
  MaxCredentialUsageAge:
    type: String
    description: (Required) Maximum number of days within which a credential must be used. The default value is 90 days.
    allowedPattern: ^(\b([0-9]|[1-8][0-9]|9[0-9]|[1-8][0-9]{2}|9[0-8][0-9]|99[0-9]|[1-8][0-9]{3}|9[0-8][0-9]{2}|99[0-8][0-9]|999[0-9]|10000)\b)$
    default: "90"
outputs:
  - RevokeUnusedIAMUserCredentialsAndVerify.Output
mainSteps:
  - name: RevokeUnusedIAMUserCredentialsAndVerify
    action: aws:executeScript
    timeoutSeconds: 600
    isEnd: true
    description: |
      ## RevokeUnusedIAMUserCredentialsAndVerify
      This step deactivates expired IAM User access keys, deletes expired login profiles and verifies credentials were revoked
      ## Outputs
      * Output: Success message or failure Exception.
    inputs:
      Runtime: python3.6
      Handler: unused_iam_credentials_handler
      InputPayload:
        IAMResourceId: "{{ IAMResourceId }}"
        MaxCredentialUsageAge: "{{ MaxCredentialUsageAge }}"
      Script: |-
        import boto3
        from datetime import datetime
        from datetime import timedelta

        iam_client = boto3.client("iam")
        config_client = boto3.client("config")

        responses = {}
        responses["DeactivateUnusedKeysResponse"] = []

        def list_access_keys(user_name):
          return iam_client.list_access_keys(UserName=user_name).get("AccessKeyMetadata")

        def deactivate_key(user_name, access_key):
          responses["DeactivateUnusedKeysResponse"].append({"AccessKeyId": access_key, "Response": iam_client.update_access_key(UserName=user_name, AccessKeyId=access_key, Status="Inactive")})

        def deactivate_unused_keys(access_keys, max_credential_usage_age, user_name):
          for key in access_keys:
            last_used = iam_client.get_access_key_last_used(AccessKeyId=key.get("AccessKeyId")).get("AccessKeyLastUsed")
            if last_used.get("LastUsedDate"):
              last_used_date = last_used.get("LastUsedDate").replace(tzinfo=None)
              last_used_days = (datetime.now() - last_used_date).days
              if last_used_days >= max_credential_usage_age:
                deactivate_key(user_name, key.get("AccessKeyId"))
            else:
              create_date = key.get("CreateDate").replace(tzinfo=None)
              days_since_creation = (datetime.now() - create_date).days
              if days_since_creation >= max_credential_usage_age:
                deactivate_key(user_name, key.get("AccessKeyId"))

        def get_login_profile(user_name):
          try:
            return iam_client.get_login_profile(UserName=user_name)["LoginProfile"]
          except iam_client.exceptions.NoSuchEntityException:
            return False

        def delete_unused_password(user_name, max_credential_usage_age):
          user = iam_client.get_user(UserName=user_name).get("User")
          password_last_used_days = 0
          login_profile = get_login_profile(user_name)
          if login_profile and user.get("PasswordLastUsed"):
            password_last_used = user.get("PasswordLastUsed").replace(tzinfo=None)
            password_last_used_days = (datetime.now() - password_last_used).days
          elif login_profile and not user.get("PasswordLastUsed"):
            password_creation_date = login_profile.get("CreateDate").replace(tzinfo=None)
            password_last_used_days = (datetime.now() - password_creation_date).days
          if password_last_used_days >= max_credential_usage_age:
            responses["DeleteUnusedPasswordResponse"] = iam_client.delete_login_profile(UserName=user_name)

        def verify_expired_credentials_revoked(responses, user_name):
          if responses.get("DeactivateUnusedKeysResponse"):
            for key in responses.get("DeactivateUnusedKeysResponse"):
              key_data = next(filter(lambda x: x.get("AccessKeyId") == key.get("AccessKeyId"), list_access_keys(user_name)))
              if key_data.get("Status") != "Inactive":
                error_message = "VERIFICATION FAILED. ACCESS KEY {} NOT DEACTIVATED".format(key_data.get("AccessKeyId"))
                raise Exception(error_message)
          if responses.get("DeleteUnusedPasswordResponse"):
            try:
              iam_client.get_login_profile(UserName=user_name)
              error_message = "VERIFICATION FAILED. IAM USER {} LOGIN PROFILE NOT DELETED".format(user_name)
              raise Exception(error_message)
            except iam_client.exceptions.NoSuchEntityException:
              pass
          return {
              "output": "Verification of unused IAM User credentials is successful.",
              "http_responses": responses
          }

        def get_user_name(resource_id):
          list_discovered_resources_response = config_client.list_discovered_resources(
              resourceType='AWS::IAM::User',
              resourceIds=[resource_id]
          )
          resource_name = list_discovered_resources_response.get("resourceIdentifiers")[0].get("resourceName")
          return resource_name

        def unused_iam_credentials_handler(event, context):
          iam_resource_id = event.get("IAMResourceId")
          user_name = get_user_name(iam_resource_id)

          max_credential_usage_age = int(event.get("MaxCredentialUsageAge"))

          access_keys = list_access_keys(user_name)
          unused_keys = deactivate_unused_keys(access_keys, max_credential_usage_age, user_name)

          delete_unused_password(user_name, max_credential_usage_age)

          return verify_expired_credentials_revoked(responses, user_name)
    outputs:
      - Name: Output
        Selector: $.Payload
        Type: StringMap
Code language: PHP (php)

deactivate_unused_keys関数でアクセスキーの無効化に関する処理を実装しています。
最後にアクセスキーを使用した日時から起算して、本日に至るまでの日数が閾値を超えた場合に限り、アクセスキーを無効化する仕様です。

改めて両アクセスキーを確認します。
一人目のIAMユーザのアクセスキーは、作成してから90日以上経過しており、かつ使用された実績がなかったため、無効化されたということになります。
二人目のIAMユーザのアクセスキーは、作成から90日以上経過していますが、本日使用しているため、無効化されなかったということになります。

まとめ

AWS Configを使用して、古いアクセスキーを検出したら、SSMランブックを使って無効化する方法を確認しました。
AWSConfigRemediation-RevokeUnusedIAMUserCredentialsの挙動についても確認しました。