CloudFront で使用する IP アドレスレンジを動的にポリシーへ設定する

Posted on 2019-10-26 in zakki

AWS S3 の静的ホスティングを CloudFront で CDN 配信した時の S3 バケットのアクセス性について。

経緯

S3 静的ホスティングを CloudFront で CDN 配信したとき、CloudFront の生成ドメイン (xxxxxx.cloudfront.net) でユーザにアクセスいただきたいところを、オリジンである静的ホスティングエンドポイント ({bucketname}.s3-website-{region}.amazonaws.com) でアクセスされてしまう可能性があり、これを防ぎたいねという話です。あと、ホスティングエンドポイントにアクセスできるのは、CloudFront だけとしたいです。

前提

そもそも、この構成を取る場合はオリジンの設定方法が複数あるようです。

  • バケットそのもの
  • ホスティングエンドポイント

今回は、S3 バケットのサブディレクトリに index.html があり URL では解決したくなかった (URL にindex.htmlを付与したくなかった ) ことから、オリジンにホスティングエンドポイントを指定しました。

方法

いくつか方法はあるようなのですが、パっと見で実装できそうだった SNS + Lambda + S3 バケットポリシー で実装してみました。バケットポリシーで CloudFront が使用する IP アドレスレンジのみを許可する方針です。

ip-ranges

AWS さんはサービスで使用する IP アドレスレンジが公開されているのでここの IP アドレスレンジを拾って、ポリシーに組み込みます。

このアドレスレンジが変更することもあります。ただ、そのときは SNS による受信が可能なので受信できるようにして、受信をトリガーとして Lambda を起動させバケットポリシーを書き換えるようにしました。

AWS の IP アドレス範囲の通知

Lambda 用スクリプトファイル

  • lambda_function.py
import boto3
import json
import urllib.request

def lambda_handler(event, context):
    addrs = []

    req = urllib.request.Request("https://ip-ranges.amazonaws.com/ip-ranges.json")
    with urllib.request.urlopen(req) as res:
        body = json.load(res)

    for i, prefix in enumerate(body["prefixes"]):
        if (prefix["service"] == "CLOUDFRONT"):
            addrs.append(prefix["ip_prefix"])

    for i, prefix in enumerate(body["ipv6_prefixes"]):
        if (prefix["service"] == "CLOUDFRONT"):
            addrs.append(prefix["ipv6_prefix"])

    client = boto3.client("s3")

    bucket_policy = {
        'Version': '2012-10-17',
        'Statement': [{
            'Sid': 'AllowS3GetObjectAccess',
            'Effect': 'Allow',
            'Principal': '*',
            'Action': ['s3:GetObject'],
            'Resource': "arn:aws:s3:::( バケット名 )/*"
        },
        {
            'Sid': 'DenyS3Access',
            'Effect': 'Deny',
            'Principal': '*',
            'Action': ['s3:*'],
            'Resource': "arn:aws:s3:::( バケット名 )/*",
            'Condition': {
                'NotIpAddress': {
                    'aws:SourceIp': addrs
                }
            }
        }]
    }

    bucket_policy = json.dumps(bucket_policy)

    client.put_bucket_policy(
        Bucket="( バケット名 )",
        Policy=bucket_policy
    )

lambda のロールは S3 へのアクセスをフル許可していますが、実際にポリシーを適用したいバケットのみの許可でも問題ないと思います。

ポリシーの内容ですがAllowS3GetObjectAccess が無くても OK かと思っていたのだけどダメだった。Lambda がバケット ( とオブジェクト ) の情報を得るために必要な許可なのか。

結果

403 になりました。よかった。

AWS Lambda CloudFront