はじめに
バケットポリシーで制御している 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 バケット
対象バケットのアクセス設定、および、対応するドメイン名の関連は下記とします。
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 から取って、整形します。
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 や証明書も自動でやってくれるのがいいですね。
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 で作成されています。
「user-agent を S3 のバケットポリシーと同じにもの上書きし、S3 を fetch する」スクリプトを当てます。
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 でも成功します。
~ $ 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 ユーザーのアクセスキーとシークレットキーなどを環境変数に入れます。
接続テスト
直接接続は 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 経由では許可されます。
~ $ curl https://s3-v4.oyama.cf/hw.txt
Hello World
Workers でログを確認すると、Worker が 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 書き換え
- AWS SigV4 認証リクエスト
あとは関連しそうな事項を載せておきます。
S3 仮想ホスティング
仮想ホスティングを使う場合を確認します。
S3 が Eyeball のリクエストと同じドメイン名(仮想ホスト)を受け入れてくれます。
そのため、hostヘッダー・SNI の変換が不要になり、そこを Workers で補う必要がなくなります。
仮想ホスティングを有効にする S3 バケット名
バケット名の先頭にドメイン名をつければ、そのドメイン名へのリクエストを受け付けてくれる模様です。
Cloudflare DNS
Cloudflare DNS で CNAME を設定します。
まずは Proxy せず、DNS として CNAME を返すようにします。S3 のユーザーガイドに記載の方法です。
TLS 証明書
仮想ホストで S3 に HTTPS でアクセスすると、証明書エラーが出ます。
~ $ 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 エンドポイントにアクセスしても同じです。
~ $ 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 にして証明書の検証エラーを回避します。
アクセス可能となりました。
~ $ 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 が不要になります)
接続テスト
Eyeball から任意の user-agent でアクセスができます。
~ $ curl https://s3-v.oyama.cf/hw.txt -A "hogee"
Hello World
仮想ホスティングなしで単に Proxy するとどうなるか
仮想ホスティングしていない S3 バケットを CNAME で指定して Proxy します。
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 で実施します。
~ $ curl https://s3-o.oyama.cf/hw.txt
Hello World
S3 静的ウェブサイトホスティング
S3 で静的ウェブサイトホスティングにしている場合は HTTPS が使えないとのことで、HTTP ヘッダーやボディを使う認証は止めたほうがよさそうです。
送信元 IP アドレスを Cloudflare に制限するくらいでしょうか。
DNS は CNAME で設定します。
仮想ホスティングのように受け付けてくれます。
S3 との通信を HTTP にするには、対象の静的ウェブホスティングのみ Page Rules または Configuration Rules で SSL モードを Flexible にします。
Cloudflare からオリジンへの接続が HTTP になります。
これだけです。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 のアクセス制御まわりはドキュメント読んでみましたが複雑で、追いつけてませんので、記載間違えや他の方法、アイデアなどあれば指摘いただけるとありがたいです。