I skipped the request with Proxy ELB-> Nginx-> ELB-> Taget Group-> ECS on AWS and ran the Rails service, but I got an error with CSRF token countermeasures, so I got a way to debug and solve it.
The error that was occurring was ʻActionController :: InvalidAuthenticityToken`.
https://railsguides.jp/security.html#クロスサイトリクエストフォージェリ-csrf This is a security measure that Rails comes standard with. Verify that the token stored in the session and ʻauthencity_token` at POST match, and if they do not match, throw an error.
Add proxy_set_header X-Forwarded-SSL on;
to nginx.conf.
nginx.conf
#It's actually written, but omitted
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;
#Add this
proxy_set_header X-Forwarded-SSL on;
}
Verify that the token stored in the session matches ʻauthencity_token` at POST, and throw an error if they do not match
Then, I looked at the Rails code that is actually verified by wondering if the token stored in the session and ʻauthencity_token` are different, or if it may happen by normal operation.
rails/actionpack/lib/action_controller/metal/request_forgery_protection.rb
def verified_request? # :doc:
!protect_against_forgery? || request.get? || request.head? ||
(valid_request_origin? && any_authenticity_token_valid?)
end
https://github.com/rails/rails/blob/98a4c0c76938e46009cca668da9c3b584a9e9e74/actionpack/lib/action_controller/metal/request_forgery_protection.rb#L289-L292
When this verified_request?
is false, the error ʻInvalidAuthenticityTokenis thrown. If the tokens are different, it means that ʻany_authenticity_token_valid?
Is false, so I tried debugging with that expectation. However, ʻany_authenticity_token_valid?` was true.
valid_request_origin?
false?Looking at the above code, verified_request?
Can be false even when valid_request_origin?
is false, so I checked it.
Indeed, valid_request_origin?
Was false.
Take a look at the contents of valid_request_origin?
.
rails/actionpack/lib/action_controller/metal/request_forgery_protection.rb
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
https://github.com/rails/rails/blob/98a4c0c76938e46009cca668da9c3b584a9e9e74/actionpack/lib/action_controller/metal/request_forgery_protection.rb#L455-L463
To make valid_request_origin?
False, I tried to output it because it seems that the reason can be understood if the contents of request.origin
and request.base_url
are known.
Then, request.origin
washttps: // ~
, while request.base_url
was http: // ~
.
In other words, it turned out that the verification part of request.origin == request.base_url
in the above code is false.
When I investigated variously at this point, "When a request is passed from Nginx to Rails, it seems that even if you access Nginx with HTTPS, it will be passed to Rails as HTTP, and to prevent this, X-Forwarded-Proto
in the Nginx conf Use to let Rails know that it's HTTPS. " I tried it.
nginx.conf
#It's actually written, but omitted
server {
listen 80;
server_name hoge.jp;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
#Add this
proxy_set_header X-Forwarded-Proto https;
}
But it didn't work.
When I tried to output the request header with Rails, it was " X-Forwarded-Proto ":" http "
.
** That's right, this was due to the nature of ELB. ** ** https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/userguide/how-elastic-load-balancing-works.html
This time, a request is sent from the ELB that is the Proxy to the ELB associated with the Rails service that is running, but this is sent by HTTP.
Application Load Balancer and Classic Load Balancer prefer connection headers from client input requests after proxying replies to clients </ b>
As a result, HTTP communication between Nginx and ELB was prioritized, and the request headers X-Forwarded-For
, X-Forwarded-Proto
, and X-Forwarded-Port
were rewritten.
The request object seems to be made with Rack, so I took a look at the code there.
rack/lib/rack/request.rb
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
#abridgement
def base_url
"#{scheme}://#{host_with_port}"
end
https://github.com/rack/rack/blob/649c72bab9e7b50d657b5b432d0c205c95c2be07/lib/rack/request.rb
From the way base_url
is created, scheme
should be https
.
There are some conditions for schema
to be https
, but this time it seems that get_header (HTTP_X_FORWARDED_SSL) =='on'
should be set!
(HTTP_X_FORWARDED_SSL
does not have to be rewritten to ELB)
So I added X_Forwarded_SSL
to the Nginx request header.
nginx.conf
#It's actually written, but omitted
server {
listen 80;
server_name hoge.jp;
proxy_set_header Host $host;
#The bottom two will be rewritten to ELB
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
#Add this
proxy_set_header X-Forwarded-SSL on;
}
No more errors!
When I debugged, X_Forwarded_SSL
was added to the request header and scheme
became https
.