目的
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自体が存在しません。
{
"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
}
{
"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)。以下ソースを解説します。
#
# 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)。以下ソースを解説します。前節と重複する部分については省略しています。
#
# 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 へのアクセス権限が必要となります。次のように作成します。
- Lamdda ファンクション用のサービスロール作成に必要なポリシーファイルを作成します。
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "sts:AssumeRole",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Effect": "Allow",
"Sid": ""
}
]
}
- 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時と同じ結果が返される
- ロールに必要なポリシーを追加します。既存のポリシーの利用をする場合、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 にして前節のロールと共にファンクションを作成します。
- 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%)
- 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": ""
}
- 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を指定して下さい。
{
"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\"}"
}
{
"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のログを確認してみてください。