🤖

S3で静的サイトを公開するときのアクセス制御について。referer制限

2024/12/27に公開

概要

やりたいこと

  • 情報共有のための静的サイトを限定公開したい。
  • サイトは特定のサービスからのみページを開いて閲覧できるようにしたい。(そのサービスのユーザにだけサイトを見せたい)

サービスのユーザはauth0で認証済み。
最初はgithub pagesでの公開が便利そうだったので良いなと思ったが、アクセス制限はできなさそう。(完全公開かprivateのみ)

やり方の調査

ベースとなる方法としては「Cloud front + S3 rest apiエンドポイント」を使った公開になる。
S3の静的サイト公開は「Block public access (bucket settings)」がデフォルトでONになっている現状では選択肢にならなそう。

公開の限定方法については2つありそう。

  1. refererによる制限
  2. lambda@edgeを使ったjwt検証

1の場合はrefererをつければ見られるので、悪意があるユーザに対しては防御にならない。
プライバシー関係の情報は置けない。例えば、特定のお客さんに見せたい社内の製品資料のようなもの(漏れても大きな問題にはならないもの)の用途に使える。

それぞれ試してみる。

準備

cdkをインストールしておく

cdkのバージョンは

"aws-cdk": "2.173.2",

1. cloud frontとs3でreferer設定をして制限する

cloud frontにOACでS3 bucketとの連携設定をする。
s3 bucketではcloud frontからforwardされるrefererを参照するpolicyをつける。

cdkについて

2022年からOAI(Origin Access Identity)ではなくOAC(Origin Access Control)の利用が推奨されていた。
しかしCDKのL2コンストラクタでOACは扱うことができず、L1コンストラクタを使ったカスタマイズが必要だった。
しかし2024/10/31のブログで解説されているように、OACがL2コンストラクトでサポートされるようになった!

cdkの実装
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class S3StaticSiteStack extends cdk.Stack {
    constructor(scope: Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props);

        /* publicサイト */
        const bucketPublic = new cdk.aws_s3.Bucket(this, 'BucketPublic', {
            removalPolicy: cdk.RemovalPolicy.DESTROY, // テスト用なので削除可能とする
            autoDeleteObjects: true, // テスト用なので削除可能とする
        });
        const distributionPublic = new cdk.aws_cloudfront.Distribution(this, 'DistributionPublic', {
            defaultRootObject: 'index_public.html',
            defaultBehavior: {
                origin: cdk.aws_cloudfront_origins.S3BucketOrigin.withOriginAccessControl(bucketPublic),
                cachePolicy: cdk.aws_cloudfront.CachePolicy.CACHING_OPTIMIZED,
            }
        });

        /* 制限をつけたいサイト */
        const bucket = new cdk.aws_s3.Bucket(this, 'BucketRestricted', {
            removalPolicy: cdk.RemovalPolicy.DESTROY, // テスト用なので削除可能とする
            autoDeleteObjects: true, // テスト用なので削除可能とする
            cors: [{
                allowedMethods: [cdk.aws_s3.HttpMethods.GET],
                allowedOrigins: ['*'],
                allowedHeaders: ['*']
            }]
        });
        // バケットポリシーでRefererをチェックする
        const bucketPolicy = new cdk.aws_s3.BucketPolicy(this, 'BucketPolicyRestricted', {
            bucket: bucket
        });
        bucketPolicy.document.addStatements(
            new cdk.aws_iam.PolicyStatement({
                actions: ['s3:GetObject'],
                resources: [bucket.bucketArn + '/*'],
                // cloud frontからのアクセスに制限
                principals: [new cdk.aws_iam.ServicePrincipal('cloudfront.amazonaws.com')],
                // refererの設定を追加
                conditions: {
                    'StringLike': {
                        'aws:Referer': `https://${distributionPublic.distributionDomainName}/*`
                    }
                },
            })
        );
        // cloud front distributionを追加
        new cdk.aws_cloudfront.Distribution(this, 'DistributionRestricted', {
            defaultRootObject: 'index.html',
            defaultBehavior: {
                origin: cdk.aws_cloudfront_origins.S3BucketOrigin.withOriginAccessControl(bucket),
                // originRequestPolicy: cdk.aws_cloudfront.OriginRequestPolicy.USER_AGENT_REFERER_HEADERS,
                cachePolicy: new cdk.aws_cloudfront.CachePolicy(this, 'CachePolicyAllowReferer', {
                    headerBehavior: cdk.aws_cloudfront.CacheHeaderBehavior.allowList('Referer'),
                }),
                viewerProtocolPolicy: cdk.aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
            }
        });
    }
}

公開用ファイル配置

以下のファイルをpublicの方のs3 bucket直下にindex_public.htmlとして配置。

<!DOCTYPE html>
<html>
    <head><title>Referer test</title></head>
    <body>
        <a href="制限したいサイトのエンドポイント(distribution domain name)">refer</a>
    </body>
</html>

以下の内容のファイルを制限をかけたいほうのs3 bucket直下にindex.htmlとして配置。

<!DOCTYPE html>
<html>
    <head><title>Hello, World!</title></head>
    <body>Hello, World!</body>
</html>

publicの方はdistribution domain nameでサイトにアクセスできる。

referをクリックすると制限されたページにアクセスできる

restrictedの方はdistribution domain nameでサイトにアクセスできない

しかしrefererを追加してやればアクセスできる

注意点

  1. オリジンリクエストポリシーは「originRequestPolicy」ではなく「cachePolicy」を使って設定する
    originRequestPolicyに設定してしまうと、refererがキャッシュ再取得の条件とならず、refererなしでもキャッシュに存在するサイト参照ができてしまう。
    cachePolicyに設定したヘッダーはキャッシュミス発生時にオリジンリクエストに含まれる。(cachePolicyに設定すればoriginRequestPolicyに設定したのと同等になる。originRequestPolicyはcachePolicyに含めたくないヘッダーを設定する)

CloudFront はこのヘッダーを使用してキャッシュヒットを特定し、オリジンリクエスト (キャッシュミスが発生したときにオリジン CloudFront に送信するリクエスト) にヘッダーを含めます。

  1. ALL_VIEWERを使ってはいけない
    originRequestPolicyではALL_VIEWERと言う設定がありますが、これをやってもreferer制限がかかったサイトを見ることはできません。hostヘッダーもforwardされてしまい、s3にアクセスしているのがpublic用のcloud frontだと判断されてしまうからのようです。

最後に

けっこう大変だった。。。
簡単だろうと高を括っていたが、意外と難しかった。
cloud frontのキャッシュとブラウザのキャッシュを消しながら動作確認するのが面倒だった。
2のlambda authorizerを使った方法は別途!

Discussion