背景
AWSで Proxy ELB -> Nginx -> ELB -> Taget Group -> ECS でリクエストを飛ばしてRailsのサービスを動かしたところ、 CSRFトークン対策でエラーになったのでそのデバッグと解決策までの道のり。
CSRFトークン対策でエラーになる
エラー概要
起こっていたエラーはActionController::InvalidAuthenticityToken
。
CSRFトークン対策とは
https://railsguides.jp/security.html#クロスサイトリクエストフォージェリ-csrf
Railsが標準搭載しているセキュリティ対策です。
セッションに保存されてるtokenとPOST時の authencity_token
が一致しているかを検証し、一致していない場合にエラーを吐く。
解決策
nginx.confにproxy_set_header X-Forwarded-SSL on;
を追加する。
# もっと本当は書いてあるけど省略
server {
listen 80;
server_name hoge.jp;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
# これを追加する
proxy_set_header X-Forwarded-SSL on;
}
エラー検証
tokenが異なっている?
セッションに保存されてるtokenとPOST時の
authencity_token
が一致しているかを検証し、一致していない場合にエラーを吐く
ならセッションに保存されているtokenとauthencity_token
が異なっているのか、通常の操作でそうなることがあるのだろうか、ということで実際に検証しているRailsのコードをみてみた。
def verified_request? # :doc:
!protect_against_forgery? || request.get? || request.head? ||
(valid_request_origin? && any_authenticity_token_valid?)
end
この verified_request?
がfalseの時に InvalidAuthenticityToken
のエラーが投げられる。
tokenが異なるということは any_authenticity_token_valid?
がfalseということになるので、その予想でデバッグをしてみた。が、any_authenticity_token_valid?
はtrueだった。
valid_request_origin?
がfalseになっている?
上のコードを見ると、valid_request_origin?
がfalseの時にもverified_request?
がfalseになる可能性があるので、確認してみた。
すると確かに、valid_request_origin?
がfalseだった。
valid_request_origin?
の中身を見てみる。
def valid_request_origin? # :doc:
if forgery_protection_origin_check
# We accept blank origin headers because some user agents don't send it.
raise InvalidAuthenticityToken, NULL_ORIGIN_MESSAGE if request.origin == "null"
request.origin.nil? || request.origin == request.base_url
else
true
end
end
valid_request_origin?
がfalseになるには request.origin
と request.base_url
の中身がわかれば理由が分りそうなので出力してみた。
するとrequest.origin
は https://〜
なのに対し request.base_url
が http://〜
となっていた。
つまり上のコードの request.origin == request.base_url
の検証部分でfalseになっていることがわかった。
NginxのconfでX-Forwarded-Protoを設定する?
この時点でいろいろ調べると、「NginxからRailsにリクエストが渡される時にHTTPSでNginxにアクセスしてもHTTPとしてRailsに渡されてしまうらしく、これを防ぐために Nginxのconfで X-Forwarded-Proto
を使ってRailsにHTTPSであることを知らせる」、という方法がすぐ出てくる。やってみた。
# もっと本当は書いてあるけど省略
server {
listen 80;
server_name hoge.jp;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# これを追加
proxy_set_header X-Forwarded-Proto https;
}
けどダメだった。
試しにRailsでリクエストヘッダを出力してみると "X-Forwarded-Proto": "http"
となっていた。
どこかでhttpsからhttpに上書きされている?
その通りで、これはELBの性質上でした。
https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/userguide/how-elastic-load-balancing-works.html
今回、ProxyとなるELBから動かしているRailsのサービスに紐づくELBに対してリクエストが送られてくるが、ここはHTTPで送られてくる。
Application Load Balancer および クラシックロードバランサー は、クライアントに返信する応答のプロキシの後のクライアントの入力リクエストからの接続ヘッダーを優先します
とのことで、NginxからELB間のHTTP通信が優先されてリクエストヘッダの X-Forwarded-For
、 X-Forwarded-Proto
、 X-Forwarded-Port
が書き換えられてしまっていた。
じゃあどうする
requestオブジェクトはRackで作られているらしいのでそこのコードを見てみた。
def scheme
if get_header(HTTPS) == 'on'
'https'
elsif get_header(HTTP_X_FORWARDED_SSL) == 'on'
'https'
elsif forwarded_scheme
forwarded_scheme
else
get_header(RACK_URL_SCHEME)
end
end
# 省略
def base_url
"#{scheme}://#{host_with_port}"
end
base_url
の作られ方から、 scheme
が https
になれば良い。
schema
が https
になるにはいくつか条件があるけれど 、今回はget_header(HTTP_X_FORWARDED_SSL) == 'on'
になるようにすればいけそう!
(HTTP_X_FORWARDED_SSL
はELBに書き換えられる心配もない)
ということで、Nginxのリクエストヘッダに X_Forwarded_SSL
を追加してみた。
# もっと本当は書いてあるけど省略
server {
listen 80;
server_name hoge.jp;
proxy_set_header Host $host;
# この下2つはELBに書き換えられちゃう
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
# これを追加する
proxy_set_header X-Forwarded-SSL on;
}
結果
エラーが出なくなった!
デバッグしてみたらちゃんとリクエストヘッダにX_Forwarded_SSL
が追加されて scheme
はhttps
になっていた。