AWS Configを使用して、古いアクセスキーを検出したら、SSMランブックを使って無効化する
AWS SOAの出題範囲の1つである、モニタリング、ロギング、および修復に関する内容です。
以下のページで、AWS Configを使用して、古いアクセスキーを検出する方法をご紹介しました。
本ページでは、その発展ということで、SSMランブックを使用して、自動的に検出した古いアクセスキーを無効化する方法を確認します。
構築する環境
構成は先述のページと概ね同様です。
異なる点は、修復アクションを設定する点です。
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ルール
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)
修復アクションに関する基本的な事項については、以下のページをご確認ください。
ポイントは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
AWSConfigRemediation-RevokeUnusedIAMUserCredentials
Type: String
Default: 90
Description: (Required) The number of days within which the credential must have been used.
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スタックを作成します。
スタックの作成および各スタックの確認方法については、以下のページをご確認ください。
各スタックのリソースを確認した結果、今回作成された主要リソースの情報は以下の通りです。
- AWS Configルール:soa-01-002-IAM-Access-Keys-Rotated
AWS Management Consoleから各リソースを確認します。
AWS Configルールを確認します。
ルールが正常に作成されています。
加えて修復アクションも作成されています。
監査結果を確認します。
IAMユーザ2名が非準拠との判定となりました。
Statusの列を見ると、修復アクションが実行されたことがわかります。
各ユーザのアクセスキーの状況を確認します。
それぞれ作成から90日以上が経過していますが、結果が分かれています。
一方は無効化されていますが、もう一方は有効化されたままです。
各ユーザに対して実行されたSSMオートメーションのログを確認します。
まずアクセスキーが無効化されたユーザに対する実行ログを確認します。
Outputsを見ると、確かにこのユーザに紐づくアクセスキーに対して無効化処理が実行されたことがわかります。
一方、アクセスキーが有効のままのユーザに対する実行ログを確認します。
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の挙動についても確認しました。