廃墟

本ブログは更新を終了しました。 技術的な記事のみ、有用性を鑑みて残しておきます。

CloudFront配下にRailsを置いたときにSSL対応するMiddleware

最近CloudFront配下にアプリケーションサーバーを置くのが徐々に流行ってる気がします。

想定する構成としては、ブラウザが直接叩くのがCloudFrontで、その直下にELB、そのさらに下に直接pumaというような形式。 nginx使わないパターンですね。 assets以下はどのみちCloudFrontでキャッシュされるから、nginxで高速化するまでもないっていうのがこの設計の思想かなと思ってます。 pumaが重くならない程度に同時接続数を制御する役割もELBがになってくれますし。

さて、ともかくそんな構成にすると、SSL対応で手間取ったのでメモ。

バージョンは以下の想定です。

  • Rails 5.1.2 (4系でも変わらないとは思います)
  • Puma 3.7

困ること

何が困るかというと、CloudFrontでSSL→HTTPに変換して、ELB以降はずっとHTTPで取り回すと、X-Forwarded-Protoヘッダが渡ってこない事です。CloudFrontは何故か独自のCloudFront-Forwarded-Protoというヘッダを渡してきます。

これによって、 force_ssl! も動かないし、そもそも post,put,patchリクエストはCSRFをブロックする機構によって弾かれてしまいます。

フロントがELBだったら問題ないんですが、ここでは最前面にCoudFrontを置く想定…。

ここで対策は2つあります。

  • 1: ELBにもSSL鍵を指定して、CloudFrontからELBへの通信路をSSLにする
    • AWS内しか通らない暗号化/解除の処理がムダでは?
    • ↑ 「AWS内しか通らない」保証はない気がするのでホントに高いレベルのセキュリティを意識するなら怖いかも(2019/5/30追記)
  • 2: どうにかしてCloudFront-Forwarded-ProtoをRailsから認識させる。

ここでは後者を採用しました。

やったこと

以下のRack Middlewareを用意します。

# lib/cloud-front-header.rb
class CloudFrontHeader
  def initialize(app)
    @app = app 
  end 

  def call(env) 
    cf_proto = env['HTTP_CLOUDFRONT_FORWARDED_PROTO'].to_s
    if cf_proto && cf_proto.length > 0
      env['HTTP_X_FORWARDED_PROTO'] = cf_proto
    end

    @app.call(env)
  end 
end

そして、 application.rb に以下の内容を足します。 他にmiddleware使ってない場合は、どこに書いても問題ないと思います。

# config/application.rb
module MyApp
  class Application < Rails::Application
    # 前略
    require './lib/cloud-front-header.rb'
    config.middleware.use CloudFrontHeader
    # 後略
  end
end

と、強引な方法ではありますが、 CloudFront-Forwarded-Proto を勝手に X-Forwarded-Proto に上書きするという荒技でどうにかしたのでした。

Rack Middleware がバグるのは鬱陶しいので、to_s等を使った防衛的なコードになってますが、気に入らない人は適宜書き換えると良いです。

あと、置き場所を考え直すと require は要らないかも知れないです。 もし良い方法あったら誰かコメントとかで教えてください。

こういう事が出来るのはRackの優れたレイヤアーキテクチャのたまものですね。