S3のファイルへのアクセスをRefererで制御する

無敵のオンラインストーレッジ、アマゾンのS3は画像置き場として重宝する。しかし、いくら安価といっても画像などを無関係のサイトに埋め込まれ転送量を課金されるのはつまらない。そこで、Refererによってアクセスを制御したい。しかしS3のアクセス制御は通常のウェブサーバの設定とはちょっと違う。

ここではRefererにより特定のサイトからの参照を拒む設定をメモする。注意: Refererによる制御はホットリンクを防ぐだけで、ブラウザを直接向けたら誰でも見れる。プライバシーを守るアクセス制御としては全く機能しない。

S3のBucketレベルのアクセス制御ポリシーの概要

  • jsonで記述されたポリシーをアップロード
  • 基本的な述語を限られたブール理論で組み合わせる
  • これをPOSTするとポリシーが有効になる

JSONによるポリシー定義の実例

注意: C言語風コメント /* */ は解説のためで合法のJSONではないので使うときは削除した方がいい。 我輩はコメントしたJSONを保存しcppでコメントを除いて使っている。


{
/* S3ポシシー機構(?)のバーション。 */
/* 新しいバージョンが出るまでこのままでいい */
"Version": "2008-10-17",
/* このポリシーのID。ユニークな名前を入れればいいようだ。 */
"Id": "example.com-referer-filter",
/* 宣言のリスト */
"Statement": [
{
/* arn:aws:s3::: */
/* Unixパス名パターン展開 (glob)の「?」と「*」が使える */
"Resource": "arn:aws:s3:::example.com/*",
/* Effect: Allow|Deny */
/* 否定か肯定か。ここでは以下の条件にあてはまるリクエストを却下する。 */
"Effect": "Deny",
/* Statement ID. この宣言のID。*/
/* このポリシーの中でユニークにした方がいいみたい。 */
"Sid": "deny-referer",
/* どのオペレーションを制御するか。*/
/* ここではアクセスを制御するからGetObjectとなる。*とかリストも可のよう。 */
"Action": "s3:GetObject",
/* 条件 */
"Condition": {
/* 文字列パターンマッチ。正規表現じゃなくてglob(Unixパス)スタイル */
"StringLike": {
/* 比較する対象。Refererヘッダー値。 */
"aws:Referer": [
/* 比較するパターン glob風?と*の使用が可 */
"http://*.example.com/*"
]
}
},
/* 許可を与える対象となるリクエスター。AWS IDとかを陳列できるようだ。 */
/* しかし、ここでは全員に適用 */
"Principal": {
"AWS": "*"
}
}
]
}

注意

上記のポシリーは各リソース毎に公開アクセス(policy='public-read')を与えることを前提としている。そのまま使うと常にアクセス拒否されると思う。上記のポリシーに全てのリソースにアクセスをAllowするStatementを加えるのがもっと一般的かと思う。

ポリシーの構造

アクセス制御ポリシーは誰(Principal)が何(Resource)をどうしたい(Action)というリクエストに対し、条件(Condition)を考慮の上、結論(Effect)を出す。

上記のポリシーを訳すと:
bucketexample.com」下のリソースは誰がリクエストしてもリクエストのRefererヘッダー値が「http://*.example.com/*」にマッチしたら拒否。

このポリシーは単一のStatementしか持たないが、複数ある場合はORで評価される。

ルールの優先順位

弱い順に:
- Default Deny: 何も指定しないとアクセス拒否
- Allow: {Effect: Allow}の指定があればアクセス可
- Explicit Deny: ただし、明示的な否定{Effect: Deny}があれば不可
詳しくは: http://docs.amazonwebservices.com/AmazonS3/latest/dev/index.html?UsingBucketPolicies.html

コードで書くとこんな感じだと思う:


if there is an explicit deny:
return deny
if there is an allow:
return allow
return deny # default deny

テスト

example.com.s3.amazonaws.com/oh-haiはlolspeak語の挨拶、「OH HAI」を含む。

Referer無しのリクエス

$ curl -s http://example.com.s3.amazonaws.com/oh-hai | grep 'OH HAI'
OH HAI
これは通る。ユーザからすると、ブラウザを直接向けた場合、見れるということになる。

除外対象でないURLからの参照

$ curl -s -H "Referer: http://good.example.org/hoge" http://example.com.s3.amazonaws.com/oh-hai | grep 'OH HAI'
OH HAI

http://good.example.org/hogeというページはhttp://example.com.s3.amazonaws.com/oh-haiという
リソースを埋め込めることになる。

除外対象URLからの参照

Bucketレベルでhttp://*.example.com/*からの参照を拒否しているので、これは弾かれる。
curl -s -D/dev/stdout -H "Referer: http://auction.example.com/hoge" http://example.com.s3.amazonaws.com/oh-hai | tee x.response | grep -o AccessDenied
AccessDenied

リスポンスはこんな感じだ


HTTP/1.1 403 Forbidden
x-amz-request-id: XXXX
x-amz-id-2: XXXXXXXXXXXXXXXX
Content-Type: application/xml
Transfer-Encoding: chunked
Date: Sun, 13 Nov 2011 06:02:59 GMT
Server: AmazonS3


AccessDeniedAccess DeniedXXXXXXXX

boto(python api)によるアクセス・ポリシー操作

S3の操作はPythonのS3 APIであるbotoを使いパイソンシェル内でやった。もっと親切なツールもあるんだろうが、これで十分だ。あと今後自動化していく基礎もできたことになる。


>>> from boto.s3.connection import S3Connection
>>> from boto.s3.key import Key
>>> from boto.s3.bucket import Bucket
# ログイン
>>> conn=S3Connection(cfg['s3_access_key'], cfg['s3_secret_key'])
# バケツを見る
>>> buckets=conn.get_all_buckets()
>>> buckets
[, ... ]
>>> bucket=buckets[0]
# 既存のポリシーはあるか?
>>> bucket.get_policy()
...
boto.exception.S3ResponseError: S3ResponseError: 404 Not Found
# ポリシーをファイルから読み込む
>>> policy_json=file('example.com-referer-filter-policy.json').read()
>>> import json
# JSONエラーがないか確認
>>> json.loads(policy_json)
# dictにする
>>> policy_dict=json.loads(policy_json)
# ポリシーをあげる。
# ここでS3との対話となる。
# ルール違反があると叱られ、それをpolicy_dictで編集し
# また上げる作業を繰替える。
>>> bucket.set_policy(json.dumps(policy_dict))
boto.exception.S3ResponseError: S3ResponseError: 400 Bad Request

MalformedPolicyPolicy has invalid resourceexample.comXXXXXXXX
# リソースが駄目だと。
# どこかのブログで見たようにarn:aws:s3:::を追加。
# ちゃんと定義していないんだよな、オフィシャル・サイトで
>>> policy_dict['Statement'][0]['Resource']='arn:aws:s3:::example.com'
>>> bucket.set_policy(json.dumps(policy_dict))
boto.exception.S3ResponseError: S3ResponseError: 400 Bad Request

MalformedPolicyAction does not apply to any resource(s) in statementAction "s3:GetObject" in Statement "deny-referer"XXXXXXXX
# リソースは合法だが対象となるリソースがないと。
# バケツの指定はできないみたいだ。
>>> policy_dict['Statement'][0]['Resource']='arn:aws:s3:::example.com/*'
>>> bucket.set_policy(json.dumps(policy_dict))
True
# これでやっと通った

グリーンスパンの第10法則

アマゾンS3のアクセスコントロール「グリーンスパンの第10法則」を実証する。こんな難解な限られたpredicate logic言語をJSONで実装するなんて…

特定のサイトからのリンクだけを許したい場合

上のポリシーを引っくり返せば、特定のサイトからのリンクだけを許すことができると思う。つまり、StringNotLikeをStringLikeにして、自分のサイトを"aws:Referer"にリストすればいい。