はじめに
AWSのSGルール自動削除は、AWSマネージドだとsshやRDPのフルオープン以外はないと思います。
ちょうどEventBridgeやLambdaを触ってみたかったので、いろいろ調べつつ実装してみました。
構成
SG作成 → EventBridgeで検出 → Lambdaで自動削除
要件定義
- 削除対象はSGのインバウンドルールで、許可されている送信元IPアドレスが
0.0.0.0/0
,::/0
となっているもの - 新規作成されたルールが対象
- 送信元IPアドレスがフルオープンになっているルールと、そうでないルールが一緒に作成された場合、フルオープンのルールのみを削除する
EventBridgeルールの作成
EventBridgeは、ルールにより一致した受信イベントを検出し、ターゲットとして登録したAWSリソースを呼び出してイベントを渡します。
呼び出されたAWSリソースは渡されたイベントを使用したりして、決められた処理を実行します。
今回、EventBridgeルールは二つ作成します。
どうにかして一つのルールで実装できないか検討しましたが、現状のイベントパターンで使用できるフィルタリングでは実装できないと思います。
もし実装できたらコメント欄で教えていただけると幸いです。
イベントパターン
フルオープンSGを検出するためのイベントパターンを作成するので、検出対象のイベントがどのような形式なのかを確認します。
AWS公式ドキュメント
サポートされている AWS サービスからの EventBridge イベントの例
https://docs.aws.amazon.com/ja_jp/eventbridge/latest/userguide/event-types.html
上記公式ドキュメントを確認すると、SGのイベントは見当たらないので、CloudTrail 経由で配信されたイベントが該当します。
書式は以下のとおりでした。
{
"version": "0",
"id": "36eb8523-97d0-4518-b33d-ee3579ff19f0",
"detail-type": "AWS API Call via CloudTrail",
"source": "aws.s3",
"account": "123456789012",
"time": "2016-02-20T01:09:13Z",
"region": "us-east-1",
"resources": [],
"detail": {
"eventVersion": "1.03",
"userIdentity": {
"type": "Root",
"principalId": "123456789012",
"arn": "arn:aws:iam::123456789012:root",
"accountId": "123456789012",
"sessionContext": {
"attributes": {
"mfaAuthenticated": "false",
"creationDate": "2016-02-20T01:05:59Z"
}
}
},
"eventTime": "2016-02-20T01:09:13Z",
"eventSource": "s3.amazonaws.com",
"eventName": "CreateBucket",
"awsRegion": "us-east-1",
"sourceIPAddress": "100.100.100.100",
"userAgent": "[S3Console/0.4]",
"requestParameters": {
"bucketName": "bucket-test-iad"
},
"responseElements": null,
"requestID": "9D767BCC3B4E7487",
"eventID": "24ba271e-d595-4e66-a7fd-9c16cbf8abae",
"eventType": "AwsApiCall"
}
}
例ではS3のため、これをSGルール作成に置き換えます。
detail
の中身はCloudTrailログでそのまま置き換えられる思いますので、実際にフルオープンSGを作成してCloudTrailログを確認し、その内容に置き換えます(後ほどイベントパターンで使うプロパティに星マークを付けています。)。
これで、検出対象のイベントが分かりました。
{
"version": "0",
"id": "36eb8523-97d0-4518-b33d-ee3579ff19f0",
★"detail-type": "AWS API Call via CloudTrail",
★"source": "aws.ec2",
"account": "123456789012",
"time": "2016-02-20T01:09:13Z",
"region": "us-east-1",
"resources": [],
"detail": {
"eventVersion": "1.08",
"userIdentity": {
略
},
"eventTime": "2021-01-09T04:08:28Z",
★ "eventSource": "ec2.amazonaws.com",
★ "eventName": "AuthorizeSecurityGroupIngress",
"awsRegion": "us-east-1",
"sourceIPAddress": "192.168.1.1",
"userAgent": "console.ec2.amazonaws.com",
"requestParameters": {
"groupId": "sg-000000000000",
"ipPermissions": {
"items": [
{
"ipProtocol": "-1",
"groups": {},
"ipRanges": {
"items": [
{
★ "cidrIp": "0.0.0.0/0"
}
]
},
"ipv6Ranges": {
"items": [
{
★ "cidrIpv6": "::/0"
}
]
},
"prefixListIds": {}
}
]
}
},
以下略
これからは、イベントパターンを作成します。
今回のイベントはAWSで定義したイベントパターンには該当しないので、自作します。
検出しなければならないイベントは、三つあり、
・IPv4のみフルオープン
・IPv6のみフルオープン
・IPv4・IPv6両方フルオープン
のSGルールです。
ルールを二つ作成するので、まず一つ目から。
完成したイベントパターンがこちらです。
{
"source": ["aws.ec2"],
"detail-type": ["AWS API Call via CloudTrail"],
"detail": {
"eventSource": ["ec2.amazonaws.com"],
"eventName": ["AuthorizeSecurityGroupIngress"],
"requestParameters": {
"ipPermissions": {
"items": {
"ipRanges": {
"items": {
"cidrIp": [ { "cidr": "0.0.0.0/0" } ]
}
}
}
}
}
}
}
このイベントパターンが検出する対象は
・IPv4のみフルオープン
・IPv4・IPv6両方フルオープン
のSGルールであり、IPv6のみフルオープンのSGルールは検出できません。
IPv6のみフルオープンのSGルールだけを検出するため、もう一つのEventBridgeルールは以下のイベントパターンにします。
{
"source": ["aws.ec2"],
"detail-type": ["AWS API Call via CloudTrail"],
"detail": {
"eventSource": ["ec2.amazonaws.com"],
"eventName": ["AuthorizeSecurityGroupIngress"],
"requestParameters": {
"ipPermissions": {
"items": {
"ipRanges": {
"items": {
"cidrIp": [ { "anything-but" : "0.0.0.0/0" }, { "exists": false } ]
}
},
"ipv6Ranges": {
"items": {
"cidrIpv6": [ "::/0" ]
}
}
}
}
}
}
}
イベントパターン①より少し複雑になっているのは、二つのEventBridgeルールが一つのイベントを二重に検出しないようにするためです。
イベントパターン②にipRanges
プロパティの記載がない場合、IPv4・IPv6両方フルオープン
のSGルール作成イベントが、イベントパターン①、②の両方で一致してしまい、二つのEventBridgeルールに検出されてしまいます。
その結果、同じLambdaで処理されることになり(Lambdaは二つに分けません。)、どちらかが必ずエラーとなります。
それを防ぐために、
・IPv6フルオープン かつ IPv4が指定されている または IPv4を許可していない
イベントに一致するものをイベントパターン②として作成しています。
これにより、IPv6のみフルオープンのSGルール作成イベントに限り、EventBridgeルールがイベントを検出します。
対応関係をまとめると以下の表のとおりです。
パターン | 検出 |
---|---|
イベントパターン① | IPv4のみフルオープン IPv4・IPv6両方フルオープン |
イベントパターン② | IPv6のみフルオープン |
また、それぞれのイベントパターンでコンテンツフィルタリングを使用しています。
詳細を知りたい方は以下のドキュメントをご覧ください。
なお、イベントパターン②の{ "anything-but" : "0.0.0.0/0" }
で、 イベントパターン①のようにIPアドレスマッチングを使用していないのは、anything-but
内でのIPアドレスマッチングの使用がサポートされていなかったからです。
AWS公式ドキュメント
イベントパターンを使用したコンテンツベースのフィルタリング
https://docs.aws.amazon.com/ja_jp/eventbridge/latest/userguide/content-filtering-with-event-patterns.html
ターゲットに渡すイベント
EventBridgeはターゲットに渡すイベント内容をカスタマイズしたり、イベントの代わりにイベントと関係のないJSONを自作して渡すことができます。
今回は、Lambdaで必要な部分のみ渡すことにします。
マネージメントコンソール上の「入力の設定」→ 「一致したイベントの一部」を選択し、$.detail.requestParameters
を入力します。
$.detail.requestParameters
をJSONにすると以下のとおりです。
このJSONからグループIDなどを必要な情報を使用します。
{
"groupId": "sg-000000000000",
"ipPermissions": {
"items": [
{
"ipProtocol": "-1",
"groups": {},
"ipRanges": {
"items": [
{
"cidrIp": "0.0.0.0/0"
}
]
},
"ipv6Ranges": {
"items": [
{
"cidrIpv6": "::/0"
}
]
},
"prefixListIds": {}
}
]
}
}
Lambda作成
Python3.6
で作成しました。
SGルールを削除するrevoke_security_group_ingress()
メソッドを使用しています。
完成したプログラムがこちらです。
コメントでも記載していますが、ec2.revoke_security_group_ingress()
でポート番号の設定をしていないルールを削除する際、適当な数字をfromPort
とtoPort
に入れてec2.revoke_security_group_ingress()
を実行してもエラーにならず削除できました。
最初は各ポートにNone
を入れて対応できないか試しましたが、int型でなければダメだと怒られました。
import json
import boto3
ec2 = boto3.client("ec2")
def lambda_handler(event, context):
ip_permissions_items = event["ipPermissions"]["items"]
SGID = event["groupId"]
for ip_permissions_item in ip_permissions_items:
ip_protocol = ip_permissions_item["ipProtocol"]
ip_ranges = ip_permissions_item["ipRanges"]
ipv6_ranges = ip_permissions_item["ipv6Ranges"]
# ipRangesのcidripが存在しフルオープンであればそのまま変数に格納し、それ以外はNoneを格納する
if ip_ranges == {}:
cidrip = None
else:
# 許可するIPアドレスが複数存在する場合もあるので順次IPアドレスを調べ、フルオープンがでた時点で変数に格納し抜け出す
for ip_ranges_item in ip_ranges["items"]:
if ip_ranges_item["cidrIp"] == "0.0.0.0/0":
cidrip = ip_ranges_item["cidrIp"]
break
else:
cidrip = None
if ipv6_ranges == {}:
cidripv6 = None
else:
for ipv6_ranges_item in ipv6_ranges["items"]:
if ipv6_ranges_item["cidrIpv6"] == "::/0":
cidripv6 = ipv6_ranges_item["cidrIpv6"]
break
else:
cidripv6 = None
# ポート設定はルールによっては存在しないため、あればそのまま変数に格納し、なければ適当な数字を格納する
if "fromPort" and "toPort" in ip_permissions_item:
from_port = ip_permissions_item["fromPort"]
to_port = ip_permissions_item["toPort"]
else:
# int型でなければ削除時エラーが発生するため数値を格納
# ポートが存在しない場合の処理なので、どの数値でも問題ないと思われる(0,70000は支障なし)
from_port = 0
to_port = 0
if cidrip == "0.0.0.0/0" and cidripv6 == "::/0":
ec2.revoke_security_group_ingress(
GroupId=SGID,
IpPermissions=[
{
"IpProtocol": ip_protocol,
"FromPort": from_port,
"ToPort": to_port,
"IpRanges":[
{
"CidrIp": cidrip
}
],
"Ipv6Ranges":[
{
"CidrIpv6": cidripv6
}
]
}
]
)
elif cidrip == "0.0.0.0/0":
ec2.revoke_security_group_ingress(
GroupId=SGID,
IpPermissions=[
{
"IpProtocol": ip_protocol,
"FromPort": from_port,
"ToPort": to_port,
"IpRanges":[
{
"CidrIp": cidrip
}
]
}
]
)
elif cidripv6 == "::/0":
ec2.revoke_security_group_ingress(
GroupId=SGID,
IpPermissions=[
{
"IpProtocol": ip_protocol,
"FromPort": from_port,
"ToPort": to_port,
"Ipv6Ranges":[
{
"CidrIpv6": cidripv6
}
]
}
]
)
これでSGフルオープンルールの自動削除実装は終わりです。
私が試してみた限りはうまく動いているように見えましたが、欠陥があった場合はコメント欄から教えていただけると嬉しいです。
主に私個人の勉強のために記事を執筆しましたが、誰かのお役に立てれば幸いです。