この記事は、AWS Lambda Advent Calendar 2017 の5日目となります。

今回は S3 にある画像ファイルを CloudFront から配信をしつつ、
CloudFront のアクセスログを Lambda で解析を行って、WAF の IPリストへ追加を行って、セキュリティの担保を行ってみようと思います。

 

S3 を CloudFront から配信する理由

前提条件に、弊社の S3 バケットにはユーザー個人が撮影した画像がアップロードされます。
なのでセキュリティには気を置いてしっかり運用していく必要性があります。

AWS SDK を使用すれば S3 から署名付きURLが吐き出され、これにアクセスすれば画像を取得できます。が、この署名付きURLには有効期限が設定されており、アプリから使うにはちょっと面倒です。

S3 を CloudFront から配信をすると、WAF が使えるようになるので固定URLで配信しつつ、セキュリティも担保できます。

 

構成


①ユーザーが CloudFront にアクセスを行う
②リクエストヘッダー等を WAF へ送る
③WAF にあるルールと照合し、結果を返す
④③がOKなら S3 から画像を持ってくる(NG なら 403 を返す)
⑤アクセスログを S3 へ保存する
⑥⑤のタイミングで、Lambda が発火する
⑦アクセスログに HTTPステータスコードが 200 以外のものがあったら、IPを WAF の IPリストに追加する

Lambda 関連のアドベントカレンダーなのに、Lambda の部分を特筆するところがねぇ…

 

CloudFront ディストリビューションの作成

オリジンに、対象の S3 バケットを。
ログの設定も何となく分かると思います。


S3 は CloudFront からのみアクセスできるようにしておきます。
ブルートフォースアタック等で、キー(/user_id/hoge_id/file_name_.jpg みたいな)がバレないように、こうします。CloudFront + WAF を組み合わせればキーがバレる前にブラックリストに入れて、アクセスを拒否できます。

 

WAF web ACL の作成


AWS resource to associate には先程作成した CloudFront のディストリビューションを設定します。

ルールの作成部分では IP address で作成する。
文字列判定を使えば特定の文字列をヘッダーに載せるとアクセスできるなんて仕組みもできます。

 


こんな感じです。
最初に IPアドレス判定を行ってから、文字列判定のルールでヘッダー認証をします。順番が大事です。今回はなんちゃってヘッダー認証があるので、デフォルトアクションは全て 403 を返すようにしています。

 

Lambda スクリプトを作る

言語は好きなものを選んでください。
今回僕が書いて運用しているコードを貼っておきます。


IP_SET_ID には WAF -> IP addresses の自分で作成した空のルールにアクセスすると、URLにある /ipsets/<ここ> を指定します。

 


トリガーに S3 として、オブジェクト作成時に発火されるようにしておきます。

 

動作確認

①正常なアクセス

ヘッダーに特定の文字列があり、正常なアクセスな場合

$ curl -I -L -H 'token:token' 'https://hoge.cloudfront.net/user/akfja/1A1C275B-BE3D-4DFC-9008-B3C375682F41/IMG_3022.JPG' -s
HTTP/2 200
content-type: application/octet-stream
content-length: 228486
date: Sat, 02 Dec 2017 12:59:15 GMT
last-modified: Sat, 16 Sep 2017 10:46:06 GMT
etag: "e70ff2a523be03ffa29ab17964345ba1"
x-amz-storage-class: STANDARD_IA
accept-ranges: bytes
server: AmazonS3
age: 252
x-cache: Hit from cloudfront
via: 1.1 Bcra7f856e226a0db7cefa4bac222.cloudfront.net (CloudFront)
x-amz-cf-id: hoge_c9CYphpJ2mJzLGvGOVn_b3Rouzkr9cfEtajQ==

 

②不正なアクセス(ヘッダー無し)

このアクセスをすると、WAF の IPリストに載り、以降アクセスできなくなれば成功

$ curl -I -L 'https://hoge.cloudfront.net/user/akfja/1A1C275B-BE3D-4DFC-9008-B3C375682F41/IMG_3022.JPG' -s
HTTP/2 403
server: CloudFront
date: Sat, 02 Dec 2017 13:10:14 GMT
content-type: text/html
content-length: 555
x-cache: Error from cloudfront
via: 1.1 6eaa7f856e226a0db7cef6201d3b8393.cloudfront.net (CloudFront)
x-amz-cf-id: hoge_KUn-jh3jcV_KOTMqlJs8WJhQw==

 

WAF の IPリストを確認する


期待通り、IPリストに追加されていました?
あとは、正しいヘッダーを入れても 403 が返ってくれればOK

 

③IPリストに追加されたIPで、①をやってみる

$ curl -I -L -H 'token:token' 'https://hoge.cloudfront.net/user/akfja/1A1C275B-BE3D-4DFC-9008-B3C375682F41/IMG_3022.JPG' -s
HTTP/2 403
server: CloudFront
date: Sat, 02 Dec 2017 15:45:45 GMT
content-type: text/html
content-length: 555
x-cache: Error from cloudfront
via: 1.1 a3c7cc30af6c8465e695a3c0d44793e0.cloudfront.net (CloudFront)
x-amz-cf-id: hogenv8bPhaW6Qz1k6qUZajIWCwWw9RDuc6CKmTJvy5rg==

素晴らしい…

 

まとめ

ということで、S3 を CloudFront で配信し、WAF と Lambda を組み合わせれば
固定URLで配信しつつ、セキュリティの確保もすることができました。

ちなみに、Lambda 側は実行時間とか、設定したメモリ使用量で課金がされるので
dev, stg, prd で3つの関数を用意しても、1つの関数で賄ってもどちらでも良いと思います。
管理のしやすさで言えば、3つに分けたほうが良いのかな?

Lambda の料金も、低スペックで長時間やるより、ちょっと良いスペックで短時間で処理を終わらせたほうが安くなったりするのでどうやって Lambda を運用するか難しいですね。