Cloudflare Tunnel 経由で Rails force_ssl を有効にする
はじめに
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 で確認できます。
今回の場合、 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 へリダイレクトされます。
その中でリクエストヘッダー 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