S3の静的ウェブサイトをカスタムオリジンとしてCloudFront運用する場合のS3側接続制限について

## 最初に結論
SNS+Lambdaを使ってS3のバケットポリシーを動的に変更することで、IP制限だけでS3のCloudFront以外からのアクセスを遮断できそうです。
ただ実際に切り替わる様子を観察していないので、実際どうなのかは不安です。

## はじめに(インデックスドキュメント)
S3をオリジンにしたCloudFront運用の場合、一般的にはバケットポリシーをCloudFrontからのみアクセスするように設定することで、インターネット側からのS3アクセスを遮断できます。

しかし公開するサイトがサーバのインデックスドキュメント設定を期待した設計の場合、CloudFrontだけではインデックスドキュメント設定による解決ができません(CloudFrontはルートのみ設定できる)。
そこでS3側で静的ウェブサイトとして公開してそこでインデックスドキュメントを設定し、これをカスタムオリジンとして使用する方法を取ります。

しかしそうすると今度は、静的ウェブサイトとして公開したS3のURLを運悪く発見された場合、オリジンに直接アクセスが来ることも考える必要があります。アクセス＝課金対象なので念のためここも遮断しておきたい。しかし静的ウェブサイトとして公開したS3の接続を制限する方法はsource IPを元にした接続制限しかできません。

CloudFrontのIPアドレスは一応以下のように公開されています。ではこの情報を使いCloudFrontのsource IPをバケットポリシーに列挙して制限をかければ…と思いますが、当然リストが意図しないタイミングで変わることは念頭に入れないといけません。いつの間にかリストが更新されていて、いつの間にか数％の確率で403エラーが発生する、なんて悲しいことが起こる可能性があるわけです。

http://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/LocationsOfEdgeServers.html
http://docs.aws.amazon.com/ja_jp/general/latest/gr/aws-ip-ranges.html

どうしたものか…と思って色々質問したり調べたりしていると、先程のリンクに以下のような情報を見つけました。
> AWS の IP アドレス範囲に変更があるたびに通知を受け取るには、次のように Amazon SNS を使用して通知を受け取るように受信登録できます。

これは…いけるぞ！すなわち
SNS→Lambda→S3バケットポリシー変更
すごく遠いですが、試してみました。

## 目的
表題の制限のために、SNS+Lambdaを利用してS3のバケットポリシーを変更する仕組みを作る。

## 手法
### SNS設定
※ここで選択するLambdaFunctionがないといけないので、先にLambdaFunctionを用意したほうが良いのですが、便宜上先に説明します。

SNSへの設定は超かんたんです。先程のリンクの「AWS の IP アドレス範囲の通知を受信登録するには」通りに設定の手順を進めます。
http://docs.aws.amazon.com/ja_jp/general/latest/gr/aws-ip-ranges.html

ただし5.b.以降

- ProtocolはAWSLambda
- Endpointは呼び出すLambdaFunctionのARNを選択
- Version or aliasの選択が増えているので、ここはdefaultを選択($LATESTがNGな理由がわからない…)

という設定をします。

### LambdaFunction設定

個人的趣味でnode.jsで作成します。

- Configure triggersはblankのままNext
- Configure function設定を入れる
 - Nameは任意(update_webs3origin_policyとか適当に)
 - Lambda function handler and roleで「CreateCustomRole」を選択。IAMの管理画面に飛ぶのでロールを作成する
 - IAMロールは「新しいIAMロールを作成」を選択、ロール名は「update_web_s3origin_policy」とか適当に。
 - ポリシーを編集しS3へのフルアクセスを許容する設定を追加する(特定バケットに対するputBacketPolicyだとダメでした…なぜ？)。結果ポリシーは以下のようになる。

```
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:*"
            ],
            "Resource": "*"
        }
    ]
}
```

 - 一番下の「許可」を押して設定。元のLambdaのコンソールに戻る。
 - LambdaFunctionを実装。まだ実験なので以下のような雑なコードを書いた。

```
'use strict';

const aws = require('aws-sdk');
const https = require('https');

exports.handler = (event, context, callback) => {

    // IPリストをawsから取ってくる
    var prefixes_json = "";
    https.get("https://ip-ranges.amazonaws.com/ip-ranges.json", function(res) {
        res.on('data', function (chunk) {
            prefixes_json += chunk.toString();
        });
        res.on('end', function () {
            // IPリストを配列に入れ直し
            const prefixes = JSON.parse(prefixes_json);
            var addrs = [];
            prefixes.prefixes.forEach(function(prefix) {
                if (prefix.service === "CLOUDFRONT") {
                    addrs.push(prefix.ip_prefix);
                }
            });
            prefixes.ipv6_prefixes.forEach(function(prefix) {
                if (prefix.service === "CLOUDFRONT") {
                    addrs.push(prefix.ipv6_prefix);
                }
            });
        
            // S3のバケットポリシーを変更する
            const bucket = "web-s3origin";
            var s3 = new aws.S3();
            var policy = {
                Version : "2012-10-17",
                Statement : [
            		{
            			Sid: "publishobject_basic",
            			Effect: "Allow",
            			Principal: {
            				AWS:"*"
                		},
            			Action: "s3:GetObject",
            			Resource: "arn:aws:s3:::"+ bucket + "/*"
                    },
                    {
                        Sid : "allow_cloudfront",
                        Effect : "Deny",
                        Principal : {
                            AWS:"*"
                        },
                        Action : "s3:*",
                        Resource : "arn:aws:s3:::" + bucket+ "/*",
                        Condition : {
                            NotIpAddress : {
                                "aws:SourceIp": addrs
                            }
                        }
                    } 
                ]
            };
            var param = {
                Bucket : bucket,
                Policy : JSON.stringify(policy)
            };
            console.log(param);
        
            s3.putBucketPolicy(param, function(err, data) {
                if (err) {
                    console.log("###error###")
                    callback(err);
                } else {
                    console.log("###success###")
                    context.done(null, "done");
                }
            });


        });
    }).on('error', function(e) {
        console.error("error##########");
        callback(e);
    });
    
};
```


## 結果
Lambda Functionのテストを行い、実際にS3のバケットポリシーに制限外のIPアドレスが列挙されることを確認しました。
IPアドレスリストが実際に変わった瞬間というのを見届けていませんが、SNSに適切なタイミングで更新のお知らせが届くなら、適切なタイミングでS3のバケットポリシーが更新されて、CloudFrontで突然数%の確率で403エラーが発生するなんてことはなく配信されることでしょう。適切なタイミングで更新されるなら…

## 個人的には
- オリジンのバケット名を十分複雑にする
- 最悪発見された場合に備えてIP制限の仕込みを入れる
- ただし仕込みを入れたなら動的に変えられるようにする

これくらいでも充分対策になると思います。或いは

- そんな悪さをする人は全体の数％にも満たないんだから、無視する。

という判断もありかと思います。厳密な制限を求められる場合、IPアドレスを使ったそういう対策もあるということで、軽い調査レポートでした。