EC2 のWebサーバーへ CloudFront 経由で AWS WAF を導入

なぜ CloudFront か?

(リクエスト数次第だが、) ELBは時間課金なのでWAFだけのために使うのは高そう。

要件

  • CloudFront 経由で AWS WAF をEC2インスタンスのWebサーバーへ適用
  • サイトはすでに常時SSL (AOSSL) 設定済みの WordPress
    • サイト実装上の都合により、CloudFront はひとまずWAF用途のみで、CDN的な用途には使用しない
  • Zone Apex なDNS名のサイトを CloudFront 経由とするため、DNSサーバーは Route 53 へ移行

手順

AWS Certificate Manager (ACM) にSSL証明書を登録

CloudFront で使用する場合は、バージニア北部リージョン (us-east-1) のACMに登録。

手元の証明書をインポートする場合

証明書のインポート 画面に従って証明書をコピーアンドペーストして登録。

ACMの無料証明書を発行する場合

パブリック証明書のリクエスト 画面から証明書をAmazon先生に発行してもらう。 ドメイン検証方法は下記の2種類。

CloudFront 設定

CDNなんてクソ食らえ、とにかく俺はWAFが使いたいだけなんだ!!! という場合の設定。

Distribution Settings

・Alternative Domain Names: example.com (WAFを導入するサイトのドメインをすべて列挙、1行1ドメイン)
・SSL Certificate: Custom SSL Certificate (先程ACMに登録した証明書を選択)
・Custom SSL Client Suppoet: Only Clients that Support Server Name Indication (SNI)
・Logging: Off (必要ならOn、キャッシュ無効化するなら不要かと)

Origin Settings

・Origin Domain Name: sv.example.com
・Origin ID: ec2 (適当な識別子)
・Origin Protocol Policy: Match Viewer
・Origin Custom Headers: X-Cf-Secret=(ランダム生成した十分な長さのシークレットキー)

Default Cache Behavior Settings

ここが肝で、Cache Based On Selected Request HeaderForward CookiesQuery String Forwarding And Caching を 全部All祭りにしてキャッシュを無理やり握りつぶす。

・Origin: ec2 (前述のOrigin ID)
・Viewer Protocol Policy: Redirect HTTP to HTTPS (AOSSLなので)
・Allowed HTTP Method: GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE
・Cache Based On Selected Request Header: All
・Forward Cookies: All
・Query String Forwarding And Caching: Forward all, based on all
・Compress Objects Automatically: Yes

AWS WAF 設定

ぶっちゃけ、こちらのクラメソ様記事 のまんま。

AWS Marketplace でマネージドルールを購読

マーケットプレイスにて画面に従って希望するルールをSubscribe。

AWS Marketplace: Trend Micro Managed Rules for AWS WAF – Content Management System (CMS)

Web ACL作る

Step 1: Name web ACL
・Web ACL name: (適当にサイト名など)
・CloudWatch metric name: (適当にサイト名など)
・Region: Global (CloudFront)
・AWS resource to associate: (前述のCloudFrontディストリビューションを選択)
Step 2: Create conditions

マネージドルール使うので何も設定せずに放置。

Step 3: Create rules
・購読したマネージドルールを Add rule to web ACL
・Action
 ・No Override (攻撃検知時にブロックする)
 ・Override To Count (攻撃検知のみでブロックしない)
・Default action: Allow all requests that don't match any rules

動作確認 – その1

ここまでで、AWS WAF を通してサイトへアクセスできるようになったので、ひとまず動作確認しておくと良い。 CloudFront のエンドポイント (xxxxxxxxxxxx.cloudfront.netのようなDNS名) を名前解決して得られるIPアドレスをどれか一つ適当に選択し、自端末の /etc/hosts に下記のようなエントリを追記。

xxx.xxx.xxx.xxx example.com

ブラウザから軽く動作確認。

  • トップページを表示できること
  • 記事ページを表示できること
  • 記事検索できること
  • 管理画面ログインできること
  • 記事投稿できること
  • 画像を投稿できること
  • 試験的にExploitを叩き込んで、403 Forbidden されること

試験的に使ったExploitの掲載は差し控えるが、下記を参考にしてかの有名なWordPress REST APIの脆弱性 を突くと比較的簡単にテストできる。

※常識的なサーバーならすでに修正済みで、実際にはなにも起こらないはずだが、WAFは反応するので動作確認には使用できる

DNSサーバーを Route 53 へ移行

前述の通り、Zone Apex なDNS名のサイトを CloudFront 経由とするため、DNSサーバーは Route 53 へ移行してAliasレコードとして CloudFront のエンドポイントを指定。 詳しい事情は こちらのクラメソ様記事が詳しい。

移行前レコード例

exapmle.com. A xxx.xxx.xxx.xxx
example.com. MX 10 example.com
example.com. TXT v=spf1 a mx ~all

移行後レコード例

Route 53 に登録するレコードセット例

exapmle.com. ALIAS xxxxxxxxxxxxxxxx.cloudfront.net
example.com. MX 10 sv.example.com
example.com. TXT v=spf1 mx ~all
sv.example.com. A xxx.xxx.xxx.xxx

最後に、ドメインのレジストラにあるであろうコンパネ等で、ドメインのネームサーバーとして Route 53 のネームサーバーを指定して移行。

CloudFront 経由以外のアクセスを拒否

ここまでの状態だと、せっかくWAFを導入したにもかかわらず CloudFront を迂回するアクセス (IP直アクセスなど) に対してはWAFが効かず、攻撃に対して無力なままである。 よって、例外的なアクセスを抑止するために、オリジンサーバー側で CloudFront 以外からのアクセスを制限する必要がある。

方法としては、下記の2点が考えられる。

  1. CloudFront のエッジロケーションで使用されるIP一覧を使用してIPベースでアクセス制限する
  2. CloudFront → オリジンサーバー アクセス時にカスタムヘッダー を付与して、自前で共通鍵認証を実装する

1. の方法は、継続的にAWSから提供されるIPリストの変更を監視する仕組みが必要になり設定・管理コストがかなり高い。 (というか、このIPリストがどれくらい真面目に管理されているのかよくわからないので、あんまり使いたくない……)

2. の方法も同様に、(例によって、クラメソ様記事が大変詳しい のだが、) 自前で共通鍵認証によるアクセス制限を実装しないといけないため、やはり簡単ではないが、1. の方法よりはシンプルな仕組みで済む。

こちらの場合、しかし、CloudFront – オリジンサーバー間の通信経路上において件の共通鍵認証が盗聴される可能性があるので、サイトのAOSSL化が必須となる (厳密には、CloudFront が付加する CloudFront-Forwarded-Proto をアプリケーション側でうまいこと処理すればクライアント – CloudFront 間の通信はどうでも良いと思うが、いまさらそんな次元の低いところで頑張らなくてもいいでしょ……)。

カスタムヘッダーを利用した共通鍵認証 (NGINXの場合)

設定する場合、前述のDNSサーバー移行が完全に浸透するまでの十分な時間をおいてからWebサーバーに適用しないと、正常なトラフィックが弾かれてしまうので要注意

nginx.conf

http {
    .
    .
    .

  log_format main '$remote_addr ... $from_cf $from_local';
    .
    .
    .
}

conf.d/includes/allow_only_from_cf.conf

※念の為、ローカルからの接続も許可

    set $from_cf 0;
    if ($http_x_cf_secret = 'iloverandompasswordbutthiswilldo') {
      set $from_cf 1;
    }

    set $from_local 0;
    if ($remote_addr = '127.0.0.1') {
      set $from_local 1;
    }

    set $test $from_cf$from_local;
    if ($test = '00') {
      return 403;
    }

最後に、こちらのconfファイルを適当に include するように変更してNGINXをリロード。

動作確認 – その2

その1で設定した /etc/hosts エントリを消して確認。

  • トップページを表示できること
  • 記事ページを表示できること
  • 記事検索できること
  • 管理画面ログインできること
  • 記事投稿できること
  • 画像を投稿できること
  • Exploitブチ込んだ場合に、403:forbidden されること
  • IP直打ちで外部からアクセスした場合に 403:forbidden されること
  • サーバーローカルから正常にアクセスできること

( ´ー`)y-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

料金

※アホがテキトーに試算した結果です。責任は取れません。正確な計算は各自でお願いいたします。

月額料金 (Trend Micro Managed Rules for AWS WAF – Content Management System (CMS) のみの場合)

■ウェブ ACL の料金 (すべての利用可能なリージョン)
1 * $5

■ルールの料金 (すべての利用可能なリージョン)
1 * $1

■リクエストの料金 (すべての利用可能なリージョン)
$0.60 (100万リクエストあたり)

■[Managed Rule] Charge per month in each available region
1 * $5

■[Managed Rule] Charge per million requests in each available region
$0.20  (100万リクエストあたり)

=> 約 $12

参考: 料金 – AWS WAF(Web アプリケーションファイアーウォール)| AWS

その他、CloudFront、EC2、VPC通信料金など

所感

  • おそらく、実際に運用する際は「カスタムヘッダーを利用した共通鍵認証」方式が必須になると思われるので、結果的にAOSSLなサイトじゃないと厳しい
    • このWAFで守るべきサイトは果たして……
  • CDN導入は難しい……
    • サイトとCDNの仕様を完全に把握してないと非常に残念な結果になるので
    • まぁでも少なくとも、画像やらCSSやらのアセット周りくらいはシュッとキャッシュしてあげてもいいのでは?
  • SSL証明書周りやHTTPリクエストのHostヘッダ周りも難しいね (小並感)
  • ログが貧弱すぎませんか……?

.
.
.
ウェブ ACL 用に CloudFront または Application Load Balancer が AWS WAF に転送するすべてのリクエストに対して、CloudWatch では以下のことが可能です。
・1 時間前または 3 時間前のデータを表示する
.
.
.
リクエストのサンプルには、各ルールのすべての条件に一致した最大 100 件のリクエストと、デフォルトアクションが適用された別の 100 件のリクエストが含まれます。デフォルトアクションは、すべてのルールのすべての条件に一致しなかったリクエストに適用されます。サンプルのリクエストは、過去 15 分間にコンテンツに対するリクエストを受信したのすべての CloudFront エッジロケーションまたは Application Load Balancer からのものです。
ウェブ ACL のテスト

  • クラメソの先生方のおかげで飯が食える、圧倒的感謝