3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

アクセス制御した S3 に Cloudflare から接続する

Last updated at Posted at 2023-09-27

はじめに

バケットポリシーで制御している Amazon S3 に Cloudflare Proxy で接続する方法を確認します。

以下の二つのパターンを試します。

  • 送信元 IP と HTTP user-agent ヘッダーのペア
  • AWS Signature Version 4
S3 バケット S3 条件 何を見るか Cloudflare プロダクト 他の Cloudflare Account から接続できてしまう
公開 aws:SourceIP 送信元 IP Workers

* S3 で仮想ホスティングを使えば Worker 不要
aws:SourceIP

aws:UserAgent
送信元 IP

HTTP user-agent ヘッダー
Workers

* Enterprise プランなら Worker の代わりに Rules でも対応可能
非公開 AWS SigV4 認証リクエスト HTTP Authorization ヘッダーまたはクエリーパラメーター Workers

他にも使われる制御方法があれば、教えてください。

S3 バケット

対象バケットのアクセス設定、および、対応するドメイン名の関連は下記とします。

alt
Eyeball Host S3 Host
s3-ua.oyama.cf pub-ua.s3.ap-northeast-1.amazonaws.com
s3-v4.oyama.cf pub-v4.s3.ap-northeast-1.amazonaws.com

送信元 IP と user-agent ヘッダーでの制御

リクエストフロー
  • 任意の Eyeball からリクエスト
  • Cloudflare Proxy が特定の user-agent に変更(hostヘッダー・SNIも S3 に変更)
  • S3 が接続を許可

今回 Cloudflare − S3 の通信に焦点当ててるので、Eyeball の認証はしていません。

Cloudflare で user-agent を上書きせず、Eyeball から来たものをパススルーして、Eyeball 自身を S3 に認証させることも可能です。

また、Eyeball - Cloudflare に Cloudflare のセキュリティ機能を実装すれば、より柔軟な S3 のアクセス制御を追加できます。

S3 バケットポリシー
  • 送信元 IP を定義
    送信元 IP のリストを Cloudflare の API から取って、整形します。
IP リスト作成
curl -s "https://api.cloudflare.com/client/v4/ips" | jq '.result|.ipv4_cidrs+.ipv6_cidrs'
  • user-agent を定義
    特定の user-agent ヘッダーを持つものだけ受け付けるようにします。
    任意のヘッダーと言うことで、uuidgen で生成した文字列を使いました。

これらをバケットポリシーに適用します。
Condition 内の二つの条件が AND で評価されていました。

バケットポリシー
{
    "Version": "2012-10-17",
    "Id": "s3-1",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:*",
            "Resource": "arn:aws:s3:::pub-ua/*",
            "Condition": {
                "StringEquals": {
                    "aws:UserAgent": "02F2F1C1-5C6F-410D-A08F-BD2D014FD3F4"
                },
                "IpAddress": {
                    "aws:SourceIP": [
                        "173.245.48.0/20",
                        :中略:
                        "2c0f:f248::/32"
                    ]
                }
            }
        }
    ]
}
Cloudflare Proxy

Cloudflare は Workers (カスタムドメイン)を利用しています。
DNS や証明書も自動でやってくれるのがいいですね。

wrangler.toml
name = "fetch-s3-ua"
main = "src/index.js"
compatibility_date = "2023-09-21"

route = { pattern = "s3-ua.oyama.cf", custom_domain = true }

DNS の設定を見ると Type が Worker で作成されています。
alt

「user-agent を S3 のバケットポリシーと同じにもの上書きし、S3 を fetch する」スクリプトを当てます。

index.js
export default {
	async fetch(request) {
         let url = new URL(request.url);
         url.hostname = "pub-ua.s3.ap-northeast-1.amazonaws.com"
         let req = new Request(url,request);
         req.headers.set("user-agent", "02F2F1C1-5C6F-410D-A08F-BD2D014FD3F4");
         return fetch(req);
         /*
         return fetch(req, {
           cf: { cacheEverything: true,
                 cacheTtlByStatus: {"200": 10, "404": 0},
                 cacheKey: request.url,
                 cacheTags: ["s3ua"] }
           });
         */
     },
};

コメントアウトしている cf プロパティはキャッシュの制御などで利用できます。

接続テスト

直接接続は正しい user-agent でも AccessDenied で拒否されます。

直接接続
~ $ curl https://pub-ua.s3.ap-northeast-1.amazonaws.com/hw.txt -A "02F2F1C1-5C6F-410D-A08F-BD2D014FD3F4"
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>AccessDenied</Code><Message>Access Denied</Message><RequestId>ABM4J83FHPW064CM</RequestId><HostId>IrZWQtlNc2fRvMBTpZavVVNV33cF63b9hvd5wZL4g3izfo6p3XsNUwO2pqVOOUQ9RSbRG9HA4ik=</HostId></Error>%

Cloudflare 経由で接続すると、任意の user-agent でも成功します。

Cloudflare経由
~ $ curl https://s3-ua.oyama.cf/hw.txt -A "hogee"
Hello World

AWS Signature Version 4 認証リクエスト

次は AWS SigV4 認証リクエスト を利用します。
非公開のバケットが、正常に署名されたリクエストのみ受け付けます。

S3 IAM ポリシー

AmazonS3ReadOnlyAccess を付けた IAM ユーザーのアクセスキーで試します。

Cloudflare Proxy

Workers スクリプト s3workers を使います。(利用方法は本家ページで)
IAM ユーザーのアクセスキーとシークレットキーなどを環境変数に入れます。
alt

こちらもカスタムホストを利用しました。
alt

接続テスト

直接接続は AccessDenied で拒否されます。

直接接続
~ $ curl https://pub-v4.s3.ap-northeast-1.amazonaws.com/hw.txt
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>AccessDenied</Code><Message>Access Denied</Message><RequestId>CCV2DQ63J708NY4F</RequestId><HostId>S+n0psm4czpaEZZdfNVS8HB9rj3bZM+6oIoZU6rVyW75kgR5mmtuFyRhh5jR15uVjoeIQW8wX4g=</HostId></Error>%

Cloudflare 経由では許可されます。

Cloudflare 経由
~ $ curl https://s3-v4.oyama.cf/hw.txt
Hello World

Workers でログを確認すると、Worker が Authorization ヘッダで署名情報を送っていることがわかります。

HTTP Authorization ヘッダー
# console.log(signedRequest.headers.get("Authorization"))

"AWS4-HMAC-SHA256 Credential=*, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=ed8d405f818994ebea51846f402819e74ffc4b26a6dbfcc86800eafce716a21f"

中締め

以上、バケットポリシーへの対応を確認しました。
また、各 Worker の指標はこんな感じでした。(短期間で参考にならないかもしれませんが)

  • user-agent 書き換え
alt
  • AWS SigV4 認証リクエスト
alt

あとは関連しそうな事項を載せておきます。

S3 仮想ホスティング

仮想ホスティングを使う場合を確認します。

S3 が Eyeball のリクエストと同じドメイン名(仮想ホスト)を受け入れてくれます。
そのため、hostヘッダー・SNI の変換が不要になり、そこを Workers で補う必要がなくなります。

仮想ホスティングを有効にする S3 バケット名

バケット名の先頭にドメイン名をつければ、そのドメイン名へのリクエストを受け付けてくれる模様です。

Cloudflare DNS

Cloudflare DNS で CNAME を設定します。
まずは Proxy せず、DNS として CNAME を返すようにします。S3 のユーザーガイドに記載の方法です。
alt

TLS 証明書

仮想ホストで S3 に HTTPS でアクセスすると、証明書エラーが出ます。

S3 仮想ホスト(S3 に直接接続)
~ $ curl https://s3-v.oyama.cf/hw.txt -A "02F2F1C1-5C6F-410D-A08F-BD2D014FD3F4"
curl: (60) SSL: no alternative certificate subject name matches target host name 's3-v.oyama.cf'

これは、S3 エンドポイントにアクセスしても同じです。

S3 エンドポイント(S3 に直接接続)
~ $ curl https://s3-v.oyama.cf.s3.ap-northeast-1.amazonaws.com/hw.txt -A "02F2F1C1-5C6F-410D-A08F-BD2D014FD3F4"
curl: (60) SSL: no alternative certificate subject name matches target host name 's3-v.oyama.cf.s3.ap-northeast-1.amazonaws.com'

どうやら、ドット付きのホストをカバーするような証明書が発行されないようです。

互換性を最も高くするには、静的ウェブサイトホスティング専用のバケットを除き、バケット名にドット (.) を使用しないことをお勧めします。バケット名にドットを含めると、証明書の検証を独自に実行しない限り、HTTPS 経由の仮想ホスト形式のアドレス指定は使用できません。これは、バケット名にドットが含まれていると、バケットの仮想ホスティング用のセキュリティ証明書は機能しないためです。

静的ウェブサイトホスティングは HTTP 経由でのみ使用されるため、この制限は静的ウェブサイトホスティング用のバケットには影響しません。

証明書を見てみると、仮想ホスト名にマッチする内容は CN にも SAN にも含まれていません。
仮想ホスト用の証明書が動的に作成されるわけではなさそうです…

~ $ openssl s_client -connect s3-v.oyama.cf.s3.ap-northeast-1.amazonaws.com:443 -servername s3-v.oyama.cf.s3.ap-northeast-1.amazonaws.com </dev/null 2>&1|awk '/BEGIN CERTIFICAT/,/END CERTIFICAT/' | openssl x509 -noout -text| awk /s3.ap-northeast-1.amazonaws.com/
        Subject: CN = *.s3-ap-northeast-1.amazonaws.com
                DNS:s3-ap-northeast-1.amazonaws.com, DNS:*.s3-ap-northeast-1.amazonaws.com, DNS:s3.ap-northeast-1.amazonaws.com, DNS:*.s3.ap-northeast-1.amazonaws.com, DNS:s3.dualstack.ap-northeast-1.amazonaws.com, DNS:*.s3.dualstack.ap-northeast-1.amazonaws.com, DNS:*.s3.amazonaws.com, DNS:*.s3-control.ap-northeast-1.amazonaws.com, DNS:s3-control.ap-northeast-1.amazonaws.com, DNS:*.s3-control.dualstack.ap-northeast-1.amazonaws.com, DNS:s3-control.dualstack.ap-northeast-1.amazonaws.com, DNS:*.s3-accesspoint.ap-northeast-1.amazonaws.com, DNS:*.s3-accesspoint.dualstack.ap-northeast-1.amazonaws.com, DNS:*.s3-deprecated.ap-northeast-1.amazonaws.com, DNS:s3-deprecated.ap-northeast-1.amazonaws.com
Cloudflare Proxy

証明書を正しいものにする(仮想ホスト名で発行する)方法が分からないので、Cloudflare Proxy を有効にし、 SSL/TLS モードを Full にして証明書の検証エラーを回避します。

alt

アクセス可能となりました。

~ $ curl https://s3-v.oyama.cf/hw.txt -A "02F2F1C1-5C6F-410D-A08F-BD2D014FD3F4"
Hello World
user-agent 変更も Workers なしで

S3 で仮想ホスティングを使うことで Workers なしで S3 にアクセスできるようになりました。次に、user-agent の変更も Workers を使わず、 Transfor Rules で試してみます。(これで Workers が不要になります)

alt
接続テスト

Eyeball から任意の user-agent でアクセスができます。

~ $ curl https://s3-v.oyama.cf/hw.txt -A "hogee"
Hello World

仮想ホスティングなしで単に Proxy するとどうなるか

仮想ホスティングしていない S3 バケットを CNAME で指定して Proxy します。

alt

NoSuchBucket と言われます。

エラー表示
~ $ curl https://s3-o.oyama.cf/hw.txt  -A "02F2F1C1-5C6F-410D-A08F-BD2D014FD3F4"
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>NoSuchBucket</Code><Message>The specified bucket does not exist</Message><BucketName>s3-o.oyama.cf</BucketName><RequestId>M0SEFQBYAMT3SWS9</RequestId><HostId>C0ZRfDUnuGBXZwzn6VyIkde1B/10RD1GlxdzJrBnZDouI03OcPU6LPOzaTY3Sk7v2NtqZRivduw=</HostId></Error>%

指摘の通り、その名前のバケットはありません。

これを回避するのに Workers fetch あるいは S3 仮想ホスティングを使ったということになります。

Enterprise プランでの機能(Page Rules または Origin Rules)

Enterprise プランの場合、host ヘッダー・SNI を Workers 以外の方法で上書き可能です。
Page Rules の Host Header Override または Origin Rules で Host Header で実施します。

Origin Rules の場合
alt

~ $ curl https://s3-o.oyama.cf/hw.txt
Hello World

S3 静的ウェブサイトホスティング

S3 で静的ウェブサイトホスティングにしている場合は HTTPS が使えないとのことで、HTTP ヘッダーやボディを使う認証は止めたほうがよさそうです。
送信元 IP アドレスを Cloudflare に制限するくらいでしょうか。

DNS は CNAME で設定します。
仮想ホスティングのように受け付けてくれます。

alt

S3 との通信を HTTP にするには、対象の静的ウェブホスティングのみ Page Rules または Configuration Rules で SSL モードを Flexible にします。
Cloudflare からオリジンへの接続が HTTP になります。
alt

これだけです。Rules や Workers も不要です。
確認すると CDN も動いています。style.css の方がキャッシュされています。(拡張子 CSS はデフォルトでキャッシュ対象

~ $ curl https://s3-www.oyama.cf/ -sv 2>&1 | grep cf-cache
< cf-cache-status: DYNAMIC

~ $ curl https://s3-www.oyama.cf/style.css -sv 2>&1 | grep cf-cache
< cf-cache-status: HIT

まとめ

S3 のアクセス制限に Cloudflare Proxy で追随する方法を試しました。

S3 のアクセス制御まわりはドキュメント読んでみましたが複雑で、追いつけてませんので、記載間違えや他の方法、アイデアなどあれば指摘いただけるとありがたいです。

3
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?