👊

S3への通信を許可するネットワークACLを動的に更新する

2022/09/15に公開1

どうしてもミニマムな通信に絞りたい時の技

背景

社内の古き良き審査ルールではネットワークACLに 0.0.0.0/0 のAllowがあると無条件に怒られる。
セキュリティグループやその他諸々の手段で制御していても……
クローズドなVPCのCodeBuildからS3にログを出力させる必要があるが、CodeBuildではS3のインターフェースVPCエンドポイントを指定する方法がない。
ゲートウェイVPCエンドポイント経由でS3に接続させたいが、S3のIPアドレスのCIDRが時々変わる……

実現すること

事前準備

  • 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

jkobaxjkobax

よく見たら、変数名 cidr ってしたい気持ちだったのに cider ってなってた……
はずかし!