🔐

Cloudflare Tunnel 経由で Rails force_ssl を有効にする

2024/07/11に公開

はじめに

Cloudflare の DNS Proxy や Tunnel を利用して、Rails アプリケーションを https で公開しています。
Rails 7.1 でデフォルトで有効になった force_ssl を利用した場合、 https でリクエストされていることが正しく検知できず、リダイレクトループになってしまいました。
そのため、 force_ssl を有効にしつつリダイレクトループにならないような設定を行います。

結論

  • リクエストヘッダー CF-Visitor の scheme の値で、 X-Forwarded-Proto を上書き
  • force_ssl が有効な場合、 middleware ActionDispatch::SSL より前で行う

構成図

Cloudflare 向けの Rack Middleware を追加

force_ssl に関係なく Rails アプリを Cloudflare 経由で公開すると、以下のエラーが発生することがあります。

ActionController::InvalidAuthenticityToken (HTTP Origin header didn't match request.base_url)

その解決方法として、以下を参考に Cloudflare 向けの Rack Middleware を追加します。

コード

config/initializers/cloudflare.rb

require 'json'

module Middleware
  class CloudflareProxy
    def initialize(app)
      @app = app
    end

    def call(env)
      return @app.call(env) unless env['HTTP_CF_VISITOR']

      env['HTTP_X_FORWARDED_PROTO'] = JSON.parse(env['HTTP_CF_VISITOR'])['scheme']
      @app.call(env)
    end
  end
end

Rails.application.config.middleware.use Middleware::CloudflareProxy

なぜエラーの解決として Rack Middleware を追加するのか?

例えば https://example.com で Cloudflare 経由にて Rails アプリを公開しているとします。
ブラウザで https://example.com を開いた場合、以下のように判定されます。

  • ブラウザから Cloudflare: https://example.com
  • Cloudflare から Rails アプリ: http://example.com

この違いによりエラーが発生します。

Rails アプリでも https://example.com として判定されるようにするため、 Rack Middleware を追加し、リクエストヘッダーを書き換えを行っています。

Cloudflare で追加されるリクエストヘッダー

Cloudflare を経由することで追加されるリクエストヘッダーは以下の URL で確認できます。

https://developers.cloudflare.com/fundamentals/reference/http-request-headers/

今回の場合、 ​​CF-Visitor が判断として適しているため、それを利用しています。

Cf-Visitor: {"scheme":"https"}

Cloudflare からリクエストヘッダー  X-Forwarded-Proto は設定されてこないのか?

Cloudflare の SSL/TLS の設定で、Full を設定すれば、DNS Proxy では X-Forwarded-Proto に https が設定されているようです。

ただ、Tunnel では SSL/TLS の設定を Full にしても X-Forwarded-Proto は http のため、​​CF-Visitor を利用するようにしました。

Rails force_ssl を有効にする

Rails の設定を変更

config/environments/production.rb

  # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
  config.force_ssl = true

この状態だと、リダイレクトループになります

Cloudflare 向けの Rack Middleware の位置を変更

config/initializers/cloudflare.rb

- Rails.application.config.middleware.use Middleware::CloudflareProxy
+ # force_sslが有効な場合はActionDispatch::SSLの前に挿入する
+ if Rails.application.config.force_ssl
+   Rails.application.config.middleware.insert_before ActionDispatch::SSL, Middleware::CloudflareProxy
+ else
+   Rails.application.config.middleware.use Middleware::CloudflareProxy
+ end

この状態で、リダイレクトループは解消されます

なぜ Rack Middleware の位置を変更が必要なのか?

force_ssl を有効にした場合、 ActionDispatch::SSL が Middleware として追加されます。
そこで、https か判断し、https でない場合は https へリダイレクトされます。

https://github.com/rails/rails/blob/v7.1.0/railties/lib/rails/application/default_middleware_stack.rb#L24-L27

https://github.com/rails/rails/blob/v7.1.0/actionpack/lib/action_dispatch/middleware/ssl.rb

その中でリクエストヘッダー X-Forwarded-Proto を参照して https か判断しています。
ただ、 Cloudflare 向けの Rack Middleware はActionDispatch::SSLよりあとに実行されるため、リクエストヘッダー ​​CF-Visitor を参照し、X-Forwarded-Proto を上書きはまだ行われていません。

そのため、 Rails.application.config.middleware.insert_before ActionDispatch::SSL, Middleware::CloudflareProxy で先に処理されるようにしています。

Rails.application.config.middleware.use Middleware::CloudflareProxy

rails middleware
use ActionDispatch::SSL
use Rack::Sendfile
use ActionDispatch::Static
use ActionDispatch::Executor
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use ActionDispatch::RemoteIp
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use Sentry::Rails::CaptureExceptions
use ActionDispatch::DebugExceptions
use Sentry::Rails::RescuedExceptionInterceptor
use ActionDispatch::Callbacks
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use ActionDispatch::PermissionsPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
use Warden::Manager
use Middleware::CloudflareProxy
run SampleApp::Application.routes

Rails.application.config.middleware.insert_before ActionDispatch::SSL, Middleware::CloudflareProxy

rails middleware
use Middleware::CloudflareProxy
use ActionDispatch::SSL
use Rack::Sendfile
use ActionDispatch::Static
use ActionDispatch::Executor
use ActiveSupport::Cache::Strategy::LocalCache::Middleware
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use ActionDispatch::RemoteIp
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use Sentry::Rails::CaptureExceptions
use ActionDispatch::DebugExceptions
use Sentry::Rails::RescuedExceptionInterceptor
use ActionDispatch::Callbacks
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ContentSecurityPolicy::Middleware
use ActionDispatch::PermissionsPolicy::Middleware
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
use Rack::TempfileReaper
use Warden::Manager
run SampleApp::Application.routes

まとめ

Cloudflare Tunnel 経由で Rails force_ssl を有効にすることができました。
リダイレクトループの回避として force_ssl を無効にしている場合は、リクエストヘッダーや Middleware の順番を確認してみてはいかがでしょう。

Discussion