【AWS Config Rule】すぐできる!Administratorロールの検知を実装するAWS Configルール作成

記事タイトルとURLをコピーする

はじめに

お久しぶりです。服部です。 最近、AWS LambdaやAWS CloudFormationテンプレートなどでコードを書くことが楽しくなってきている今日この頃です。

さて、突然ではありますが、皆様のAWSアカウントでAdministrator権限を不用意にアタッチしてしまっていませんでしょうか? IAMユーザーに比べ、IAMロールはアクセス情報の漏洩リスクは少ないものの、内部からの乗っ取りなどを考えるとやはり制御すべき対象になるのではないでしょうか?

そこで今回は、AdministratorAccessポリシーを付与した場合、またはAdministrator権限と同格のポリシーを作成した際に検知する仕組みをAWS Configで作成していきたいと思います。

まずは、利用する関連する用語の解説から…

AWS Configとは

AWS Configの基本概要
AWS Configは、クラウドリソースの構成設定を監視、管理、評価するAWSのサービスです。これにより、リソース間の依存関係を視覚化し、問題のトラブルシューティングを迅速に行うことが可能になります。

AWS Configの利点と活用例
AWS Configを利用することで、コンプライアンスの確保やセキュリティチェックの自動化が可能になります。例えば、構成設定が組織の基準に適合しているかどうかを継続的に評価できるため、効率的なITインフラストラクチャ管理が実現します。

Administrator権限の重要性

Administrator権限とは
Administrator権限とは、AWSアカウントの全てのリソースとサービスにアクセスし、操作することができる最も強力な権限です。この権限を持つユーザーは、システム全体に影響を与える重大な変更を行うことが可能です。

権限管理が重要な理由
権限管理を徹底することで、不正アクセスや意図しない変更からシステムを守ることができます。適切な権限設定は、セキュリティ対策の基本中の基本です。

AWS Config Rulesとは

AWS Config Rules は、AWS Config の機能の一つで、AWS リソースがセキュリティやコンプライアンスのポリシーに準拠しているかを評価する ルール を定義し、自動的に監視・管理するサービスです。

AWS Config Rules の主な機能

リソースのコンプライアンス評価

事前定義またはカスタムルールに基づいて AWS リソースを評価し、「準拠(Compliant)」または「非準拠(Non-compliant)」を判定し、マネジメントコンソールから確認ができる。 また、リソースの詳細についてもAWS Configのマネジメントコンソールから確認ができる。

自動修復(Auto Remediation)

非準拠のリソースに対して、AWS Systems Manager Automation などを活用して修正アクションを自動適用可能。

変更履歴の記録

ルールがリソースの変更を監視し、非準拠の状態が発生した場合に通知(SNS 連携)が可能。

AWS Config Rules の種類

マネージドルール(AWS Managed Rules)

AWS が提供する 事前定義ルール。数百種類以上のルールがあり、設定するだけでリソースのコンプライアンスを監視可能。
例:s3-bucket-public-read-prohibited(S3 バケットのパブリック読み取りを禁止)、ec2-instance-no-public-ip(EC2 インスタンスにパブリック IP を許可しない)

※今回は、iam-policy-no-statements-with-admin-access(Administrator権限と同格のポリシーを作成した際に検知する)ルールを有効化します。
🔗 マネージドルールの一覧: AWS マネージドルールのリスト

カスタムルール(Custom Rules)

AWS Lambda を利用して、独自の評価ロジック を持つルールを作成可能。

カスタムルールの流れ:

  1. AWS Lambda 関数を作成(評価ロジックを記述)
  2. AWS Config Rule に Lambda を関連付ける 3.ルールが定期的に実行され、結果を AWS Config に記録

※今回は、AdministratorAccessポリシーを付与した場合のルールを作成します。

AWS Config Managed Rules 作成

実装する評価ロジックの概要

  • Administrator権限と同格のポリシーを作成した際に検知し、「非準拠」と評価する
  • 上記以外は「準拠」と評価する

前提条件

よく利用するリージョンのAWS Configをすでに有効化していること

AWS Config Managed Rulesの作成

AWSアカウントにログインしたら、検索窓に「Config」と入力し対象のページに移動します。

左側の三本線から「ルール」を開きます。
アグリゲーター配下の「ルール」ではないので、注意してください。

ルールを追加をクリックします。

検索窓に【iam-policy-no-statements-with-admin-access】と入れ、表示されたルールにチェックボタンを入れ、「次へ」を押します。

任意の名前と説明を入力します。※すでに「iam-policy-no-statements-with-admin-access」を作ってしまっているので少し手を加えてます 他はそのままで「次へ」をクリックします。

問題がなければ「保存」をクリックします。

作成されました!

テスト

適当にAdministrator権限を持ったポリシーを作ってみます。

アクションから再評価したもののすぐに適用されず、一晩おいておいたら非準拠として出てきました! ポリシーを作成して20~30分では、評価されないようです。

AWS Config Custom Rules 作成

実装する評価ロジックの概要

  • 変更のあったIAMロールにを確認し、アタッチされたポリシーにAdministratorAccessが付与されていた場合、「非準拠」と評価する
  • 上記以外は「準拠」と評価する

構成イメージ

Lambda用のIAMロール作成

まずはLambdaに設定するためのIAMロールを作成します。
IAMロールを参照する権限についてはConfigから借りるので、一旦ログ出力用とConfigと連携するためのポリシーを設定します。 以下のIAM PolicyをアタッチしたIAM Roleをを作成します

  • AWSLambdaBasicExecutionRole(AWS管理ポリシー)
  • ConfigEvaluation(インラインポリシー)
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "config:PutEvaluations",
                "config:GetResourceConfigHistory"
            ],
            "Resource": "*"
        }
    ]
}

Lambda関数の作成

対象のリソースを確認するためのLambdaを作成します。 関数名:iam-role-has-attached-policies(任意) ランタイム:Python 3.13 タイムアウト:15分0秒(念のために) リソースベースのポリシーステートメント※CloudShellで下記実行

aws lambda add-permission \
  --function-name 【lambda 関数名】 \
  --statement-id "AddConfigPermission" \
  --action lambda:InvokeFunction \
  --principal config.amazonaws.com

コードは下記の通りです。独学で書いているので、つたないコードである点はご容赦ください。
テスト環境で想定通りの動作だったので、問題なく動くはず…

以下参考にしています。

AWS Config ルールの AWS Lambda 関数の例 (Python) - AWS Config

import datetime
import json
import logging
import boto3
import botocore
 
 
logger = logging.getLogger()
logger.setLevel(logging.INFO)
 
ASSUME_ROLE_MODE = False
 
def check_defined(reference, reference_name):
    if not reference:
        return Exception('Error:', reference_name, 'is not defined')
    return reference
 
def get_client(service, event):
    if not ASSUME_ROLE_MODE:
        return boto3.client(service)
    executionRoleArn = event['executionRoleArn']
    logging.info(f'executionRoleArn: {executionRoleArn}')
    credentials = get_assume_role_credentials(executionRoleArn)
    return boto3.client(
        service,
        aws_access_key_id=credentials['AccessKeyId'],
        aws_secret_access_key=credentials['SecretAccessKey'],
        aws_session_token=credentials['SessionToken']
    )
 
def get_assume_role_credentials(role_arn):
    sts_client = boto3.client('sts')
    try:
        credentials = sts_client.assume_role(
            RoleArn=role_arn,
            RoleSessionName='configLambdaExecution'
        )
    except botocore.exceptions.ClientError as error:
        if 'AccessDenied' in error.response['Error']['Code']:
            error.response['Error']['Message'] = 'AWS Config does not have permission to assume the IAM Role.'
        else:
            error.response['Error']['Code'] = 'InternalError'
            error.response['Error']['Code'] = 'InternalError'
        raise error
 
def is_oversized_changed_notification(message_type):
    check_defined(message_type, 'messageType')
    return message_type == 'OversizedConfigurationItemChangeNotification'
 
def convert_api_configuration(configurationItem):
    for key, value in configurationItem.items():
        if isinstance(value, datetime.datetime):
            configurationItem[key] = str(value)
    configurationItem['awsAccountId'] = configurationItem['accountId']
    configurationItem['ARN'] = configurationItem['arn']
    configurationItem['configurationStateMd5Hash'] = configurationItem['configurationItemMD5Hash']
    configurationItem['configurationItemVersion'] = configurationItem['version']
    configurationItem['configuration'] = json.loads(configurationItem['configuration'])
    if 'relationships' in configurationItem:
        for i in range(len(configurationItem['relationships'])):
            configurationItem['relationships'][i]['name'] = configurationItem['relationships'][i]['relationshipsName']
    return configurationItem
 
def get_configuration(resourece_type, resourece_id, configuration_capture_item):
    result = AWS_CONFIG_CLIENT.get_resourece_config_history(
        resourceType=resourece_type,
        resourceId=resourece_id,
        laterTime=configuration_capture_item,
        limit=1
    )
    configurationItem = result['configuratioItems']
    return convert_api_configuration(configurationItem)
 
def get_configuration_item(invokingEvent):
    check_defined(invokingEvent, 'invokingEvent')
    if is_oversized_changed_notification(invokingEvent['messageType']):
        configurationItemSummary = check_defined(
            invokingEvent['configurationItemSummary'], 'configurationItemSummary'
        )
        return get_configuration(
            configurationItemSummary['resourceType'],
            configurationItemSummary['resourceId'],
            configurationItemSummary['configurationItemCaptureTime']
        )
    return check_defined(invokingEvent['configurationItem'], 'configurationItem')
 
def is_applicable(configurationItem, event):
    try:
        check_defined(configurationItem, 'configurationItem')
        check_defined(event, 'event')
    except:
        return True
    status = configurationItem['configurationItemStatus']
    # eventLeftScope indicates whether resoureces delete the evaluation scope.
    eventLeftScope = event['eventLeftScope']
    if status == 'ResourceDeleted':
        print('Resource Delete, setting Compliance Status to NOT_APPLICABLE')
    return (status == 'OK' or status == 'ResourceDiscovered') and not eventLeftScope
 
def check_role_policy(RoleName):
    iam = boto3.client('iam')
    response = ['COMPLIANT', 'No Problem.']
    try:
        policies = iam.list_attached_role_policies(RoleName=RoleName) 
        for AttachedPolicies in policies["AttachedPolicies"] :
            if AttachedPolicies['PolicyName'] == "AdministratorAccess":
                logger.warning(f'Roles with AdministratorAccecc rights exist.')
                response = ['NON_COMPLIANT', 'Roles with AdministratorAccecc rights exist.']
                return response
    except Exception as e:
        print(f"Error: {e}")
        return ['COMPLIANT', 'No Problem.']
    return response
def evaluate_change_notification_compliance(configuration_item):
    try:
        check_defined(configuration_item, 'configuration_item')
        check_defined(configuration_item['configuration'], 'configuration_item[\'configuration\']')
        check_defined(configuration_item['configuration']['roleName'], 'roleName')
        roleName = configuration_item['configuration']['roleName']
        logger.info(f'InboundPermissions: {roleName}')
         
        if roleName == []:
            return ['COMPLIANT','Role does not have any permissions.']
        else:
            return check_role_policy(roleName)
 
    except KeyError as error:
        logger.error(f'KeyError: {error} is not defined.')
        return ['NOT_APPLICABLE', 'Cannot Evaluation because object is none.']
 
def lambda_handler(event, context):
    global AWS_CONFIG_CLIENT
    AWS_CONFIG_CLIENT = get_client('config', event)
 
    invoking_event = json.loads(event['invokingEvent'])
    logger.info(f'invoking_event: {invoking_event}')
 
    configuration_item = get_configuration_item(invoking_event)
    logger.info(f'configuration_item: {configuration_item}')
    compliance_type = 'NOT_APPLICABLE'
    annotation = 'NOT_APPLICABLE.'
 
    if is_applicable(configuration_item, event):
        compliance_type, annotation = evaluate_change_notification_compliance(configuration_item)
 
    response = AWS_CONFIG_CLIENT.put_evaluations(
        Evaluations=[
            {
                'ComplianceResourceType': invoking_event['configurationItem']['resourceType'],
                'ComplianceResourceId': invoking_event['configurationItem']['resourceId'],
                'ComplianceType': compliance_type,
                'Annotation': annotation,
                'OrderingTimestamp': invoking_event['configurationItem']['configurationItemCaptureTime']
            },
        ],
        ResultToken=event['resultToken']
    )

カスタムルールの作成

AWSアカウントにログインしたら、検索窓に「Config」と入力し対象のページに移動します。

左側の三本線から「ルール」を開きます。
アグリゲーター配下の「ルール」ではないので、注意してください。

ルールを追加をクリックします。

「カスタムLamdbaルール」を選択

任意の名前と先ほど作成したAWSLambda関数のARNを記入します。

任意の評価モードを設定します。
定期的に実行すると無駄なコストががっ制するので、下画面ではIAMロールの設定変更時にのみトリガーされるように設定しています。 ほかはそのままの設定で「次へ」をクリックします。

内容を確認して問題がなければ、「保存」をクリックします。 新しくルールを追加されていることを確認できます。

再評価するとちゃんと非準拠のリソースが出てきました! 忘れていたものもありましたが、どれも作成した記憶があるリソースだったので一安心です。

まとめ

今回実現したかった制御がAWS Configのマネージドルールになく、どうしたものかと思ってましたが、開発期間8時間程度で実現できました!
Pythonを思い出しながら、失敗しながらだったので、もともとPythonを書ける方であれば、処理内容にもよりますが、4時間もあれば実現できそうです。
今後も統制かけたいけど、AWS Organizationsのポリシーで実行してしまうと影響がでしょうなものについては、カスタムルールで状況を確認した上で統制をかけられるとよい作ってみたいと思いました。

参考

blog.serverworks.co.jp

服部 朝海(執筆記事の一覧)

エンタープライズクラウド部 クラウドリライアビリティ課

何かとセキュリティ関するサービスを触りがち