🖼️

WAFを使わずにS3の画像アクセス制限をCloudFront Functionsで実装する

に公開

ソニックムーブのエンジニアの福田です。

画像などの静的コンテンツをS3に置いて、配信する構成はよくあります。
ただし、誰でもアクセスできる状態だと、外部サイトからの直リンクや不正利用が気になりますよね。

そこで「特定のURL(自社サービスなど)からのアクセスだけを許可したい」と思ったのですが、
AWS WAFを使うのはややコストが高く、個人開発や小規模プロジェクトには少しオーバースペックです。。。
参考:AWS WAFの料金形態
https://aws.amazon.com/jp/waf/pricing/

そこで今回は、WAFを使わずにCloudFront Functionsを利用してアクセス制限を実現する方法を試してみました。
軽量・低コストで手軽に導入できるので、「ちょっとした制御をかけたい」ケースにぴったりです。

そもそもS3に制限はかけれないの?

そもそもS3にはバケットポリシーという機能があります。
バケット自体にアクセス制限をかけたりできる優れものですね!

んじゃ、そもそもこれでやっちゃえばいいのではという声も出てくるかと思います。

でも結論から言うと、S3で特定のURLからのアクセス制限はできなさそうでした。

実際、特定のIPからのアクセス制限はできそうでした!

{
  "Version": "2012-10-17",
  "Id": "PolicyForSpecificIP",
  "Statement": [
    {
      "Sid": "AllowAccessFromSpecificIP",
      "Effect": "Allow",
      "Principal": "*",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::origin.bucket.name/*",
      "Condition": {
        "IpAddress": {"aws:SourceIp": "127.0.0.1/32"}
      }
    }
  ]
}

ただ、今回はURLでの設定です。色々やってみましたが、これだとできなさそうでした。。。残念。。。

CloudFrontを利用する

まぁ制限できるとは言っても、そもそもS3へのアクセスについてはCloud Frontを利用することが推奨されてます。
理由は以下の感じですね。

セキュリティ向上

S3バケットをプライベートにして、CloudFront経由のみアクセスを許可することで、諸々のセキュリティ設定ができるのでセキュリティの向上が期待できます。

柔軟なアクセス制御

CloudFront FunctionsやLambda@EdgeでRefererチェックや署名付きURLの検証が可能になります。

高速配信とキャッシュ

高速に画像を届けられるようになり、S3への負荷を軽減できます。

独自ドメインやHTTPS対応

安全で見栄えの良いURLで公開することができます。

つまり、S3単体での公開は「小規模かつ信頼できるユーザー向け」に限定されますが、CloudFrontを経由すれば安全性と利便性の両立が可能になります。
そもそもS3に保存してる画像を利用する際はCloudFrontを経由してアクセスするようにしましょう!

S3とCloudFrontを結びつける

実際にCloudFront経由でアクセスできるようにしましょう。
まず、S3のバケットポリシーに以下のように書いていきます。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowGetFromSpecificIP",
            "Effect": "Allow",
            "Principal": {
                "Service": "cloudfront.amazonaws.com"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::origin.bucket.name/*"
        }
    ]
}

CloudFrontからのアクセスを許可する感じですね。

あとはCloudFront側で新規のディストリビューションを作成して、オリジンで作成したバケットを設定してやればOKです!

CloudFront Functionを利用してアクセス制限をかける

では実際にCloudFront Functionにどんな感じで書いていけばいいでしょうか?
今回は以下のように書いてみました!

function handler(event) {
    var request = event.request;
    var headers = request.headers;

    // 許可するドメイン
    var allowedDomains = [
        【自分が許可したいドメイン】
    ];

    // Referer ヘッダーがあるか
    if (headers.referer) {
        var referer = headers.referer.value;
        
        var allowed = allowedDomains.some(function(domain) {
            return referer.startsWith(domain);
        });
        if (!allowed) {
            return { statusCode: 403, statusDescription: "Forbidden" };
        }
    } else {
        // Referer がない場合も拒否
        return { statusCode: 403, statusDescription: "Forbidden" };
    }

    // 許可された場合はリクエストを通す
    return request;
}

リファラでアクセス先のURLを判別して許可をもらう感じですね。
あとはこれをビヘイビアの関数の関連付けで紐づけてやるだけです!
ビューワーリクエストに設定してやる感じですね!

構成上の注意

今回の構成上、リファラを確認をしてURLを判別する形式にしてます。それ以外で関数部分で判別する方法はないのかなと考えてます。
リファラで確認すると言うことはバレてしまえば不正することが可能ではあります。予算よりセキュリティを重要視したい場合はやはりWAFを利用する方が望ましいと思います。

株式会社ソニックムーブ

Discussion