S3のバケットポリシーだけでウェブサイトにAPIキー認証をかける方法

# 概要

S3を使ったウェブサイトの開発で、開発途中の画面をクライアントに見せたい。

![website-to-client.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233180/8aa68026-dcf6-c993-cb0a-cf4b37621e0d.png)

でもCloudFrontやApiGatewayを使って真面目な認証を作るのは面倒。

そんな時に便利な、S3だけで簡単にAPIキー認証をかける方法があります。

## 構成図

必要な構成はS3だけです。S3のバケットポリシーだけでAPIキーを設定します。

![kousei.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233180/3f2087b0-ced2-1c37-3778-061d59e72d56.png)

## 使用感

ウェブページにアクセス制限をかけることができます。
Chromeの起動オプションで正しいAPIキーを設定したブラウザでだけ、閲覧することができます。

![a-b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233180/cadc7c18-5a49-2842-9f11-f601371d6ec9.png)


curlでファイルを見るだけなら、APIGatewayで`x-api-key`を指定するときと全く同じ感覚で使えます。

```bash:curlでAPIキーをつけてリクエストする
curl https://xxxxxxxxxxxxxxxxxxxxxxx.s3-ap-northeast-1.amazonaws.com/xxxxxx/xxxx.xxx \
-A "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

どちらの手順も、間違ったAPIキーだと403が返ります。

# 仕組み

ユーザーエージェントの偽装と、バケットポリシーの`aws:UserAgent`を使います。

```json:バケットポリシーの設定（抜粋）
{
    "Effect": "Deny",
    "Action": "s3:*",
    "Condition": {
        "StringNotEquals": {
            "aws:UserAgent": "設定したいAPIキー"
        }
    }
}
```

ブラウザから送った値がバケットポリシーの設定値と一致していればOK、そうでなければ制限します。
認証情報をヘッダに入れて送りますので、静的ウェブサイトホスティングでの公開は避けてください。ACLを使って、HTTPSのエンドポイントのままウェブサイトを公開するようにしてください。

参考：AWS グローバル条件コンテキストキー
https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/reference_policies_condition-keys.html


# 手順

詳しい手順を順に説明します。所要時間は数分だと思います。

1. S3にデータを入れる
2. S3のバケット設定とACLを編集
3. バケットポリシーにAPIキーを設定する

## データの準備（S3にデータを用意しておく）

### S3にウェブサイトのファイルを配置します。

今回はテストのために、シンプルなウェブサイトをS3に置きました。

- バケット名：20210219-testbucket-protect
- ファイル：
    - index.html（メインのHTML）
    - bootstrap.min.css（CSS）
    - kokodayo.png（HTMLに表示する画像）

![bucket-image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233180/dacd4fa3-c26f-e044-ae52-93c50480fb3c.png)

### アクセスコントロールリスト（ACL）の設定

ファイルをアップロードするときに、「追加のアップロードオプション」を開いて、アクセスコントロールリスト（ACL）を「全員（パブリックアクセス）」にチェックを入れておいてください。

![acl.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233180/9107e215-414d-9717-ce6a-5627672c0559.png)

### バケット設定

以下のように、バケット設定はブロックがすべて外れた状態にしておきます。

![bucket-public.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233180/31cb18ec-2601-7763-ad9a-9ae0ba14ccce.png)

### オブジェクトURLを確認する

index.htmlを開くと、オブジェクトURLが表示されています。

![index_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233180/07d18249-291e-6d61-3022-cccadc2590e4.png)

このオブジェクトURLをブラウザで開くと、アップロードしたウェブサイトが開きます。
まだセキュリティをかける前ですから、URLさえ知っていれば誰でも閲覧できる状態です。

## セキュリティを設定する

### APIキーに設定する文字列を決める

英数字の適当な文字列を決めます。

今回は以下のUUIDを使うことにしました。

```bash
a0168318-7f15-4660-97e0-568e96b664a0
```

### バケットポリシーを設定する

バケットポリシーに、「条件に一致する一切のアクセスを禁止する」ポリシーを設定します。

- Effect : Deny
- Action : S3:*
- Resource : arn:aws:s3:::${バケットポリシーを設定するS3のバケット}/*
- Principal : *
- Conditions : StringNotEquals : { "aws:UserAgent" : ${決めておいたAPIキー} }

Jsonではこのようになります。
※バケット名の「20210219-testbucket-protect」と、APIキーの「a0168318-7f15-4660-97e0-568e96b664a0」は自身の環境に合わせてください。

```json:バケットポリシー
{
    "Version": "2012-10-17",
    "Id": "S3PolicyId1",
    "Statement": [
        {
            "Sid": "KeyDeny",
            "Effect": "Deny",
            "Principal": "*",
            "Action": "s3:*",
            "Resource": "arn:aws:s3:::20210219-testbucket-protect/*",
            "Condition": {
                "StringNotEquals": {
                    "aws:UserAgent": "a0168318-7f15-4660-97e0-568e96b664a0"
                }
            }
        }
    ]
}
```

### 確認

ブラウザ、curl、どちらでも認証を利用できます。

#### curlで一つのファイルを取得する場合

以下のように、`-A`オプションでAPIキーをを送信すればファイルを取得できます。

```bash:GitBashで実行
curl https://20210219-testbucket-protect.s3-ap-northeast-1.amazonaws.com/index.html \
-A "a0168318-7f15-4660-97e0-568e96b664a0"
```

`-A`の代わりに、直接User-Agentヘッダを指定してもOKです。

```bash:GitBashで実行
curl https://20210219-testbucket-protect.s3-ap-northeast-1.amazonaws.com/index.html \
-H "User-Agent:a0168318-7f15-4660-97e0-568e96b664a0"
```

#### ブラウザでファイルを見る場合

以下のようなコマンドを実行すると、新しくChromeが立ち上がります。
※chromeのインストール先は環境によって異なる場合があります。

```text:コマンドプロンプトで実行
"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" \
--user-agent=APIキー \
--user-data-dir="C:\temp"
```

```text:入力例
"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" \
--user-agent=a0168318-7f15-4660-97e0-568e96b664a0 \
--user-data-dir="C:\temp"
```

立ち上がったブラウザでURLを開くと、ウェブサイトを見ることができます。

![option.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233180/4344e15f-6a16-19b2-6219-10e8bfef6c75.png)

APIキーが間違っていると、以下のような表示になります。

![deny-browser.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233180/95bd62fb-7f82-4556-71ee-77bf8e4b6826.png)

#### ブラウザでファイルを見る場合（起動オプションを使わない場合）

ブラウザの開発者ツールで設定します。

1. 開発者ツール（F12キー押下、または画面を右クリックして「検証」を選択）を開きます。
2. 右端の「縦三つの点」をクリック
3. 「More tools」をクリック
4. 「Network Conditions」をクリックして開きます。

![setting-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233180/a634afc2-79ca-8b5a-572b-a356256ebfb3.png)

「User Agent」の設定を変更します。

![setting-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233180/02cd3478-27b8-2746-57e0-adf2d06f85c3.png)

「Select automatically」を外して、ユーザーエージェントの入力欄に、APIキーを入力します。

![setting-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233180/0daba9eb-1614-eb4e-9b3a-c3071601d495.png)

開発者ツールにAPIキーを入力した状態で、開発者ツールを閉じないままリロード（F5キーを押下、またはブラウザ左上の再読み込みボタンを押下）をすれば、ウェブサイトが見えるようになります。

![result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/233180/ab355b53-1774-8397-7f1e-4061c0c4f2b0.png)

この場合は、開発者ツールを表示している間だけ、画面遷移やリロードを繰り返しても認証が通ります。

#### 記事中の画像リソースについて

記事中の画像は規約の範囲内で利用しています。

ファンキット利用規約
https://arknights.jp/fankit/precautions