👊
S3への通信を許可するネットワークACLを動的に更新する
どうしてもミニマムな通信に絞りたい時の技
背景
社内の古き良き審査ルールではネットワークACLに 0.0.0.0/0 のAllowがあると無条件に怒られる。
セキュリティグループやその他諸々の手段で制御していても……
クローズドなVPCのCodeBuildからS3にログを出力させる必要があるが、CodeBuildではS3のインターフェースVPCエンドポイントを指定する方法がない。
ゲートウェイVPCエンドポイント経由でS3に接続させたいが、S3のIPアドレスのCIDRが時々変わる……
実現すること
- 社内ルールでギルティ扱いされるネットワークACLの 0.0.0.0/0 のAllowを削除する
- S3のIPアドレスのCIDR情報が載っている ip-ranges.json を定期的にチェックして、ここをAllowする設定をNACLに追加する!
- アウトバウンドはTCPの443、インバウンドはTCPの1024~65535の通信を通す必要あり
事前準備
- 2022/9/15時点でS3用の東京リージョン(ap-northeast-1)のCIDRが11行あるため、Service QuotasでNACLのルール上限緩和を申請しておく
Lambda (Python)
- 対象のネットワークACLを調べておき、環境変数 network_acl_id に設定
- S3許可用のネットワークACLのルール番号を何番から始めるかを環境変数 rule_number_start に設定
- NACLの情報と ip-ranges.json の情報を取得し、比較して不足があればルールを追加する
- NACLの CidrBlock と ip-ranges.json の ip_prefix を比較
- Lambdaには以下の権限を付与しておく
- ec2:DescribeNetworkAcls
- ec2:CreateNetworkAclEntry
- ec2:DeleteNetworkAclEntry
import boto3
import json
import os
import urllib.request
client = boto3.client('ec2')
url = 'https://ip-ranges.amazonaws.com/ip-ranges.json'
req = urllib.request.Request(url)
# get network_adl_id from environment variables
network_acl_id = os.environ.get('network_acl_id')
# get rule number start from environment variables
rule_number_start = os.environ.get('rule_number_start')
def lambda_handler(event, context):
global rule_number_start
# get current network acl settings
response = client.describe_network_acls(
NetworkAclIds = [ network_acl_id ]
)
nacl_entries = response['NetworkAcls'][0]['Entries']
nacl_ip_ciders = [d.get('CidrBlock') for d in nacl_entries]
nacl_rule_numbers = [d.get('RuleNumber') for d in nacl_entries]
# remove max rule number which is shown as "*" in console
max_n = max(nacl_rule_numbers)
nacl_rule_numbers = [ n for n in nacl_rule_numbers if n != max_n ]
# adjust rule number start
if rule_number_start in nacl_rule_numbers:
rule_number_start = max(nacl_rule_numbers) + 1
# retrieve information from "ip-ranges.json"
with urllib.request.urlopen(req) as res:
ip_ranges = json.loads(res.read())
s3_tokyo_ip_ranges = []
for i in ip_ranges['prefixes']:
if i['service'] == 'S3' and i['region'] == 'ap-northeast-1':
s3_tokyo_ip_ranges.append(i)
s3_tokyo_ip_ranges.sort(key=lambda x: x['ip_prefix'])
# create network acl entries
for s3_tokyo_ip_range in s3_tokyo_ip_ranges:
cider = s3_tokyo_ip_range['ip_prefix']
if cider not in nacl_ip_ciders:
# create network acl entry (Outbound)
response = client.create_network_acl_entry(
CidrBlock=cider,
Egress=True,
NetworkAclId=network_acl_id,
PortRange={
'From': 443,
'To': 443,
},
Protocol='6',
RuleAction='allow',
RuleNumber=rule_number_start,
)
# create network acl entry (Inbound)
response = client.create_network_acl_entry(
CidrBlock=cider,
Egress=False,
NetworkAclId=network_acl_id,
PortRange={
'From': 1024,
'To': 65535,
},
Protocol='6',
RuleAction='allow',
RuleNumber=rule_number_start,
)
# increment rule number
rule_number_start = rule_number_start + 1
あとは定期実行
詳細は省略。EventBridgeで週次ぐらいで実行したらいいのでは?と想定。
振り返り
実行時間はおよそ4秒ぐらい。
ちょいとした処理はLamdbaめちゃくちゃ安いよなー。といつも実感。
対象サービスや対象リージョンあたりも環境変数で指定しといた方が再利用性は高まるかも。
Discussion
よく見たら、変数名 cidr ってしたい気持ちだったのに cider ってなってた……
はずかし!