Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

AWS Config Rules カスタムルールを作成してみよう

More than 1 year has passed since last update.

目的

AWS Config Rulesのカスタムルールは Lambda の作成によって行われます。この記事ではサンプルを元に、実際にカスタムルールの作成を行うことで、今後のカスタムルールを作成する際の開発をスムーズにします。

教材

awslabs の aws-config-rules を用います。

ここでは、Lambda (Python) で次のルールを作成します。
- 0.0.0.0/0のインバウンドルールを含むセキュリティグループの検出
- 実際には使用されていないIAMユーザの検出

仕組み

ドキュメントに書かれているように、AWS Config Rulesには2つのトリガータイプがあります。
今回の例では、両方とも「変更時に検出」を使います。「変更時に検出」の場合は対象のリソースがゼロではない限り、リソースの情報がLambdaファンクションにイベントとして渡されます。また、「再評価」をすることですべてのリソースを評価できます。「定期的な実行」の場合は、リソース情報は渡されませんので、リソース情報の抽出自体をコード内ですることが必要となります。

AWS Configの設定

デフォルトではIAMなどグローバルリソースは対象とならず、ルールを作成しても評価されません。
AWS Config の設定で、グローバルリソース (AWS IAM リソースなど) を含めるチェックボックスをオンにしておきます。

ソース

ソースの入手

作業用ディレクトリを作り、GitHubからCloneします。

$ mkdir Config
$ cd Config/
$ git clone https://github.com/awslabs/aws-config-rules.git
Cloning into 'aws-config-rules'...
remote: Counting objects: 575, done.
remote: Compressing objects: 100% (10/10), done.
remote: Total 575 (delta 2), reused 3 (delta 0), pack-reused 565
Receiving objects: 100% (575/575), 157.63 KiB | 140.00 KiB/s, done.
Resolving deltas: 100% (331/331), done.
$ 

渡されるイベントの構造

AWS Config ルールのイベントの例が参考になります。ただすべてのイベント仕様が記載されてはいないので、リソース毎にconfigurationItemを出力して内容を確認することをお勧めします。

今回扱うのは EC2 と IAM ユーザです。これらのリソースのinvocationEventの例を以下に記載しておきます。なお、ScheduledEventでの呼び出しの場合や、呼び出されたものの評価対象のリソースがない場合にはconfigurationItem自体が存在しません。

EC2
{
    "recordVersion": "1.3",
    "configurationItem": {
        "relationships": [],
        "configurationItemCaptureTime": "2018-07-04T01:54:02.810Z",
        "availabilityZone": null,
        "configurationStateMd5Hash": "",
        "tags": {},
        "resourceType": "AWS::EC2::Instance",
        "configurationItemVersion": "1.3",
        "configurationStateId": 1530669242810,
        "relatedEvents": [],
        "awsRegion": "ap-northeast-1",
        "ARN": "arn:aws:ec2:ap-northeast-1:xxxxxxxxxxxxxx:instance/i-05888074e384774ef",
        "supplementaryConfiguration": {},
        "resourceName": null,
        "configuration": null,
        "resourceId": "i-05888074e384774ef",
        "resourceCreationTime": null,
        "configurationItemStatus": "ResourceDeleted",
        "awsAccountId": "xxxxxxxxxxxxxxx"
    },
    "notificationCreationTime": "2018-07-05T14:40:13.752Z",
    "messageType": "ConfigurationItemChangeNotification",
    "configurationItemDiff": null
}
IAMユーザ
{
    "recordVersion": "1.3",
    "configurationItem": {
        "relationships": [
            {
                "resourceType": "AWS::IAM::Policy",
                "resourceId": "ANPAJUAZCQRMVSYFQNJPI",
                "name": "Is attached to CustomerManagedPolicy",
                "resourceName": "trainexpenser-codecommit-readonly"
            }
        ],
        "configurationItemCaptureTime": "2018-07-05T15:22:21.203Z",
        "availabilityZone": "Not Applicable",
        "configurationStateMd5Hash": "",
        "tags": {},
        "resourceType": "AWS::IAM::User",
        "configurationItemVersion": "1.3",
        "configurationStateId": 1530804141203,
        "relatedEvents": [],
        "awsRegion": "global",
        "ARN": "arn:aws:iam::xxxxxxxxxxxx:user/trainexpenser-codecommit-readonly",
        "supplementaryConfiguration": {},
        "resourceName": "trainexpenser-codecommit-readonly",
        "configuration": {
            "userName": "trainexpenser-codecommit-readonly",
            "groupList": [],
            "createDate": "2017-12-01T05:01:21.000Z",
            "userId": "AIDAIKBNZFI2LJRYEOI3G",
            "userPolicyList": null,
            "path": "/",
            "attachedManagedPolicies": [
                {
                    "policyName": "trainexpenser-codecommit-readonly",
                    "policyArn": "arn:aws:iam::xxxxxxxxxxxxx:policy/trainexpenser-codecommit-readonly"
                }
            ],
            "arn": "arn:aws:iam::xxxxxxxxxxxxx:user/trainexpenser-codecommit-readonly"
        },
        "resourceId": "AIDAIKBNZFI2LJRYEOI3G",
        "resourceCreationTime": "2017-12-01T05:01:21.000Z",
        "configurationItemStatus": "ResourceDiscovered",
        "awsAccountId": "xxxxxxxxxxxx"
    },
    "notificationCreationTime": "2018-07-05T15:22:49.724Z",
    "messageType": "ConfigurationItemChangeNotification",
    "configurationItemDiff": null
}

0.0.0.0/0 のインバウンドを含むセキュリティグループの検出

このルールについては既にサンプルに用意されています(ec2-exposed-instance.py)。以下ソースを解説します。

ec2-exposed-instance.py
#
# This file made available under CC0 1.0 Universal (https://creativecommons.org/publicdomain/zero/1.0/legalcode)
#
# Ensure that no EC2 instances allow public access to the specified ports.
# Description: Checks that all instances block access to the specified ports.
#
# Trigger Type: Change Triggered
# Scope of Changes: EC2:Instance
# Accepted Parameters: examplePort1, exampleRange1, examplePort2, ...
# Example Values: 8080, 1-1024, 2375, ...


import json
import boto3

# ルールを適用する対象となるリソースの配列です。evaluate_compliance()で使用します。
APPLICABLE_RESOURCES = ["AWS::EC2::Instance"]

# 隠されているべきポート番号定義(forbidden ports)をrangeに変換します。1つの場合はそのポートのみです。
def expand_range(ports):
    if "-" in ports:
        return range(int(ports.split("-")[0]), int(ports.split("-")[1])+1)
    else:
        return [int(ports)]

# 0.0.0.0/0が含まれるIpRanges設定を探し、収集します。
def find_exposed_ports(ip_permissions):
    exposed_ports = []
    for permission in ip_permissions:
        if next((r for r in permission["IpRanges"]
                if "0.0.0.0/0" in r["CidrIp"]), None):
                    exposed_ports.extend(range(permission["FromPort"],
                                               permission["ToPort"]+1))
    return exposed_ports

# exporsed_portsに、forbidden_portsが含まれていた場合にルール違反を返します。最初の1つが見つかった時点で探索は終了します。
# exporsed_ports * forbidden_ports の計算量となります。
# forbidden_portsの例: {"examplePort1":"8080", "exampleRange1":"1-1024", "examplePort2":"2375"}
def find_violation(ip_permissions, forbidden_ports):
    exposed_ports = find_exposed_ports(ip_permissions)
    for forbidden in forbidden_ports:
        ports = expand_range(forbidden_ports[forbidden])
        for port in ports:
            if port in exposed_ports:
                return "A forbidden port is exposed to the internet."

    return None


# 評価の主ファンクションです。
def evaluate_compliance(configuration_item, rule_parameters):
    # 対象のリソースタイプではない場合は NOT_APPLICABLE として返します。
    if configuration_item["resourceType"] not in APPLICABLE_RESOURCES:
        return {
            "compliance_type": "NOT_APPLICABLE",
            "annotation": "The rule doesn't apply to resources of type " +
            configuration_item["resourceType"] + "."
        }

    # リソースが既に削除されている場合は NOT_APPLICABLE として返します。
    if configuration_item['configurationItemStatus'] == "ResourceDeleted":
        return {
            "compliance_type": "NOT_APPLICABLE",
            "annotation": "The configurationItem was deleted and therefore cannot be validated"
        }

    security_groups = configuration_item["configuration"].get("securityGroups")

    # そもそもセキュリティグループがアタッチされていない場合は NON_COMPLIANT として返します。
    if security_groups is None:
        return {
            "compliance_type": "NON_COMPLIANT",
            "annotation": "The instance doesn't pertain to any security groups."
        }

    ec2 = boto3.resource("ec2")
    # すべてのセキュリティグループについて検査します。
    for security_group in security_groups:
        # IPパーミッションを取り出します。
        ip_permissions = ec2.SecurityGroup(
                                           security_group["groupId"]
                                          ).ip_permissions
        # 違反がないかを検査します。
        violation = find_violation(
            ip_permissions,
            rule_parameters
        )

        # None以外が返却された場合は違反として NON_COMPLIANT とその内容を返します。
        if violation:
            return {
                "compliance_type": "NON_COMPLIANT",
                "annotation": violation
            }

    # 検査に合格したので COMPLIANT を返します。
    return {
        "compliance_type": "COMPLIANT",
        "annotation": "This resource is compliant with the rule."
    }


# Lambdaの主関数です。検査を行う対象のリソース毎に呼び出されます。
# eventの以下3つを使用します。
# invokingEvent/configurationItem: リソースの設定内容
# ruleParameters: AWS Config Rules におけるルールのパラメータ  -> (key, value)の配列
# resultToken: 結果をAWS Configにputする時に使用するトークン
def lambda_handler(event, context):

    # 各データ/パラメータ/トークンの取り出し
    invoking_event = json.loads(event["invokingEvent"])
    configuration_item = invoking_event["configurationItem"]
    rule_parameters = json.loads(event["ruleParameters"])

    result_token = "No token found."
    if "resultToken" in event:
        result_token = event["resultToken"]

    # 評価
    evaluation = evaluate_compliance(configuration_item, rule_parameters)

    # boto3を使用して AWS Config に結果を put
    config = boto3.client("config")
    config.put_evaluations(
        Evaluations=[
            {
                # 通常はinputのリソースタイプ/リソースIDを使用する
                "ComplianceResourceType":
                    configuration_item["resourceType"],
                "ComplianceResourceId":
                    configuration_item["resourceId"],

                # 以下2つが評価結果
                # COMPLIANT, NON_COMPLIANT, NOT_APPLICABLE
                "ComplianceType":
                    evaluation["compliance_type"],
                # 評価結果の文字列
                "Annotation":
                    evaluation["annotation"],

                # 並べ替えに使うタイムスタンプ
                # キャプチャ時のタイムスタンプを用いれば良い
                "OrderingTimestamp":
                    configuration_item["configurationItemCaptureTime"]
            },
        ],
        ResultToken=result_token
    )

実際には使用されていないIAMユーザの検出

このルールについても既にサンプルに用意されています(iam-inactive-user.py)。以下ソースを解説します。前節と重複する部分については省略しています。

iam-inactive-user.py
#
# This file made available under CC0 1.0 Universal (https://creativecommons.org/publicdomain/zero/1.0/legalcode)
#
# Ensure that no users have been inactive for a period longer than specified.
# Description: Checks that all users have been active for earlier than specified.
#
# Trigger Type: Change Triggered
# Scope of Changes: IAM:User
# Required Parameters: maxInactiveDays
# Example Value: 90


import json
import boto3
import datetime


APPLICABLE_RESOURCES = ["AWS::IAM::User"]

# 与えられた日付と現在の差分の日数を計算します
def calculate_age(date):
    now = datetime.datetime.utcnow().date()
    then = date.date()
    age = now - then

    return age.days


def evaluate_compliance(configuration_item, rule_parameters):
    if configuration_item["resourceType"] not in APPLICABLE_RESOURCES:
        return "NOT_APPLICABLE"

    config = boto3.client("config")
    # 与えられているイベントから変更があるかもしれないので、念のためAWS Configの最新情報からリソースIDをキーとしてユーザ名を取り出します
    resource_information = config.get_resource_config_history(
        resourceType=configuration_item["resourceType"],
        resourceId=configuration_item["resourceId"]
    )
    user_name = resource_information["configurationItems"][0]["resourceName"]

    # IAM の API を用いて当該IAMユーザの PasswordLastUsed を取り出します
    iam = boto3.client("iam")
    user = iam.get_user(UserName=user_name)
    last_used = user["User"].get("PasswordLastUsed")

    # パラメータから未使用期間の閾値を取り出します
    max_inactive_days = int(rule_parameters["maxInactiveDays"])

    # 未使用期間がパラメータで与えられた閾値を超えていた場合は NON_COMPIANT として返します
    if last_used is not None and calculate_age(last_used) > max_inactive_days:
        return "NON_COMPLIANT"

    return "COMPLIANT"


def lambda_handler(event, context):
    invoking_event = json.loads(event["invokingEvent"])
    configuration_item = invoking_event["configurationItem"]
    rule_parameters = json.loads(event["ruleParameters"])

    result_token = "No token found."
    if "resultToken" in event:
        result_token = event["resultToken"]

    config = boto3.client("config")
    config.put_evaluations(
        Evaluations=[
            {
                "ComplianceResourceType":
                    configuration_item["resourceType"],
                "ComplianceResourceId":
                    configuration_item["resourceId"],
                "ComplianceType":
                    evaluate_compliance(configuration_item, rule_parameters),
                "Annotation":
                    "The user has never logged in.", # COMPIANTの場合も同じ Annotation となっています
                "OrderingTimestamp":
                    configuration_item["configurationItemCaptureTime"]
            },
        ],
        ResultToken=result_token
    )

Lambdaファンクションの作成

AWS Config Rules のための Lambda ファンクションは以下で構成されます。
- AWS Config Rules にアクセス可能な Lambda ファンクション用のロール
- ソースコード・パッケージ (zip)

AWS Config Rules のカスタムルール用ロールの作成

まずは、ロールを作成します。ロールは1つ作っておけば複数のルールで再利用できます。このロールは Lambda ファンクション用のサービスロールであり、ログ出力のために CloudWatch Logs への権限と、AWS Config へのアクセス権限が必要となります。次のように作成します。

  1. Lamdda ファンクション用のサービスロール作成に必要なポリシーファイルを作成します。
lambda-exec-role-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
     {
       "Action": "sts:AssumeRole",
       "Principal": {
         "Service": "lambda.amazonaws.com"
        },
        "Effect": "Allow",
        "Sid": ""
     }
  ]
}
  1. create-roleでロールを作成します。ロール名は lambda-config-rules-role とします。作成後 ARN を得ておきます。get-roleを実行すればいつでも得られます。
$ ls
aws-config-rules        lambda-exec-role-policy.json
※git cloneを実行したディレクトリ
$ aws iam create-role --role-name lambda-config-rules-role --assume-role-policy-document file://lambda-exec-role-policy.json
{
    "Role": {
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17", 
            "Statement": [
                {
                    "Action": "sts:AssumeRole", 
                    "Sid": "", 
                    "Effect": "Allow", 
                    "Principal": {
                        "Service": "lambda.amazonaws.com"
                    }
                }
            ]
        }, 
        "RoleId": "AROAJSIB5RAT7Y2JOAEUA", 
        "CreateDate": "2018-07-05T12:38:22.114Z", 
        "RoleName": "lambda-config-rules-role", 
        "Path": "/", 
        "Arn": "arn:aws:iam::xxxxxxxxxxxxxx:role/lambda-config-rules-role"
    }
}

$ aws iam get-role --role-name lambda-config-rules-role
※create-role時と同じ結果が返される
  1. ロールに必要なポリシーを追加します。既存のポリシーの利用をする場合、CloudWatchLogsFullAccess と AWSConfigRulesExecutionRole となります。今回はEC2とIAMのリソース情報を取得しますので、AmazonEC2ReadOnlyAccessとIAMReadOnlyAccessも追加しています。
$ aws iam attach-role-policy --role-name lambda-config-rules-role --policy-arn "arn:aws:iam::aws:policy/service-role/AWSConfigRulesExecutionRole"
$ aws iam attach-role-policy --role-name lambda-config-rules-role --policy-arn "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess"
$ aws iam attach-role-policy --role-name lambda-config-rules-role --policy-arn "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess"
$ aws iam attach-role-policy --role-name lambda-config-rules-role --policy-arn "arn:aws:iam::aws:policy/IAMReadOnlyAccess"
$ aws iam list-attached-role-policies --role-name lambda-config-rules-role
{
    "AttachedPolicies": [
        {
            "PolicyName": "AmazonEC2ReadOnlyAccess", 
            "PolicyArn": "arn:aws:iam::aws:policy/AmazonEC2ReadOnlyAccess"
        }, 
        {
            "PolicyName": "CloudWatchLogsFullAccess", 
            "PolicyArn": "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess"
        }, 
        {
            "PolicyName": "IAMReadOnlyAccess", 
            "PolicyArn": "arn:aws:iam::aws:policy/IAMReadOnlyAccess"
        }, 
        {
            "PolicyName": "AWSConfigRulesExecutionRole", 
            "PolicyArn": "arn:aws:iam::aws:policy/service-role/AWSConfigRulesExecutionRole"
        }
    ]
}

Lambda ファンクションの作成

ソースコードを zip にして前節のロールと共にファンクションを作成します。

  1. Zip ファイルを作成します。
$ zip -j ec2-exposed-instance.zip aws-config-rules/python/ec2-exposed-instance.py 
  adding: ec2-exposed-instance.py (deflated 68%)
$ zip -j iam-inactive-user.zip aws-config-rules/python/iam-inactive-user.py
  adding: iam-inactive-user.py (deflated 61%) 
  1. Lambdaファンクションを作成します。ロールのARNは実際のものに変更して下さい。
$ aws lambda create-function \
 --function-name awsconfig-ec2-exposed-instance \
 --runtime python2.7 \
 --role arn:aws:iam::xxxxxxxxxx:role/lambda-config-rules-role \
 --handler ec2-exposed-instance.lambda_handler \
 --timeout 300 \
 --zip-file fileb://ec2-exposed-instance.zip
{
    "TracingConfig": {
        "Mode": "PassThrough"
    }, 
    "CodeSha256": "f+ZThR3wLwsK9AHcupb7MNFLtKl3H5tnAxA77vg2ILo=", 
    "FunctionName": "awsconfig-ec2-exposed-instance", 
    "CodeSize": 1473, 
    "RevisionId": "ba1b0512-2646-4571-92e3-728fe627d8a3", 
    "MemorySize": 128, 
    "FunctionArn": "arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxxx:function:awsconfig-ec2-exposed-instance", 
    "Version": "$LATEST", 
    "Role": "arn:aws:iam::xxxxxxxxxxxxxxx:role/lambda-config-rules-role", 
    "Timeout": 300, 
    "LastModified": "2018-07-05T13:18:28.119+0000", 
    "Handler": "lambda_handler", 
    "Runtime": "python2.7", 
    "Description": ""
}
$ aws lambda create-function \
 --function-name awsconfig-iam-inactive-user \
 --runtime python2.7 \
 --role arn:aws:iam::xxxxxxxxxx:role/lambda-config-rules-role \
 --handler iam-inactive-user.lambda_handler \
 --timeout 300 \
 --zip-file fileb://iam-inactive-user.zip
{
    "TracingConfig": {
        "Mode": "PassThrough"
    }, 
    "CodeSha256": "yRuXariCEPmjSqZ0GKOHcVhzk2/1B14+C+9BpMQzkXs=", 
    "FunctionName": "awsconfig-iam-inactive-user", 
    "CodeSize": 1115, 
    "RevisionId": "ba7be6ef-5148-4790-80c2-73f232e5536d", 
    "MemorySize": 128, 
    "FunctionArn": "arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxx:function:awsconfig-iam-inactive-user", 
    "Version": "$LATEST", 
    "Role": "arn:aws:iam::xxxxxxxxxxxx:role/lambda-config-rules-role", 
    "Timeout": 300, 
    "LastModified": "2018-07-05T13:19:29.453+0000", 
    "Handler": "lambda_handler", 
    "Runtime": "python2.7", 
    "Description": ""
}
  1. AWS Config から Lambda ファンクションを呼び出す許可を与えます。source-accountについては自分のアカウント番号に変更して下さい。
$ aws lambda add-permission \
> --function-name awsconfig-ec2-exposed-instance \
> --statement-id 1 \
> --principal config.amazonaws.com \
> --action lambda:InvokeFunction \
> --source-account xxxxxxxxxxxxxx
{
    "Statement": "{\"Sid\":\"1\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"config.amazonaws.com\"},\"Action\":\"lambda:InvokeFunction\",\"Resource\":\"arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxxx:function:awsconfig-ec2-exposed-instance\",\"Condition\":{\"StringEquals\":{\"AWS:SourceAccount\":\"xxxxxxxxxxxxx\"}}}"
} 
$ aws lambda add-permission \
> --function-name awsconfig-iam-inactive-user \
> --statement-id 1 \
> --principal config.amazonaws.com \
> --action lambda:InvokeFunction \
> --source-account xxxxxxxxxxxxx
{
    "Statement": "{\"Sid\":\"1\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"config.amazonaws.com\"},\"Action\":\"lambda:InvokeFunction\",\"Resource\":\"arn:aws:lambda:ap-northeast-1:xxxxxxxxxxxxxxx:function:awsconfig-iam-inactive-user\",\"Condition\":{\"StringEquals\":{\"AWS:SourceAccount\":\"xxxxxxxxxxxxx\"}}}"
}
$ 

AWS Config Rules の作成

ルール定義のための json ファイルを作成し、それを用いてルールを作成します。SourceIdentifierには。実際の Lambda ファンクションのARNを指定して下さい。

ec2-exposed-instance-rule.json
{
    "ConfigRuleName": "EC2-Exposed-Instance",
    "Description": "Evaluates EC2 instances which expose port(s).",
    "Scope": {
        "ComplianceResourceTypes": [
            "AWS::EC2::Instance"
        ]
    },
    "Source": {
        "Owner": "CUSTOM_LAMBDA",
        "SourceIdentifier": "arn:aws:lambda:ap-northeast-1:xxxxxxxxxx:function:awsconfig-ec2-exposed-instance",
        "SourceDetails": [{
            "EventSource": "aws.config",
            "MessageType": "ConfigurationItemChangeNotification"
        }]
    },
    "InputParameters": "{\"examplePort1\":\"8080\", \"exampleRange1\":\"1-1024\", \"examplePort2\":\"2375\"}"
}
iam-inactive-user-rule.json
{
    "ConfigRuleName": "IAM-Inactive-Users",
    "Description": "Evaluates inactive IAM users.",
    "Scope": {
        "ComplianceResourceTypes": [
            "AWS::IAM::User"
        ]
    },
    "Source": {
        "Owner": "CUSTOM_LAMBDA",
        "SourceIdentifier": "arn:aws:lambda:ap-northeast-1:xxxxxxxxxxx:function:awsconfig-iam-inactive-user",
        "SourceDetails": [{
            "EventSource": "aws.config",
            "MessageType": "ConfigurationItemChangeNotification"
        }]
    },
    "InputParameters": "{\"maxInactiveDays\":\"90\"}"
}
$ aws configservice put-config-rule --config-rule file://ec2-exposed-instance-rule.json 
$ aws configservice put-config-rule --config-rule file://iam-inactive-user-rule.json 

ここまでできたら、AWS Configのコンソールから「再評価」を押すことにより全リソースの評価を実行できます。評価結果やLambdaのログを確認してみてください。

kempe
発言は個人の意見であり、所属団体を代表するものではありません。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away