Proper way to set up Rails 6 on AWS ECS - ruby-on-rails

I have Rails 6 (running with Puma) deployed on AWS ECS. There's an ECS Service and an ECS Task, and the Task spins up EC2 instances that host my app. I also created an Application Load Balancer with my ECS Service. I added an HTTPS listener to the load balancer. My listener points to a Target Group that accepts traffic specifically via the HTTP protocol.
My understanding of the request flow:
HTTPS request from internet:
--> hits AWS load balancer
--> hits HTTPS listener
--> passes traffic using HTTP to Target Group
--> request finally reaches Rails app on Target Group EC2 targets *over HTTP*
Is this a valid setup? I read this StackOverflow answer and my interpretation is that that we only need HTTPS for our load balancer and not for Puma and thus not for the Target Group.
I also set up a health check over HTTP for my Target Group that expects a 301 status code response as a healthy target (because I have config.force_ssl on in my Rails config). The thing is, why does traffic from the load balancer not get redirected? Why does traffic from the health check get redirected? Aren't they both hitting the same Target Group? Why does one request result in a 200 while the other results in a 301?
I've made a drawing to try to capture my question/confusion/current understanding:
Here's my load balancer and target group setup:
Not sure if this is an AWS issue or if it's a Puma issue or something else. Taking all ideas! Thank you!!

Answering your 2 questions.
Why does traffic from the load balancer not get redirected?
This answer explains it as well, but I'll dig a little bit deeper: when the load balancer receives a HTTPS connection and forwards it via HTTP to your Rails server, it will set a X-FORWARDED-PROTO=https header to the request, and Rails understands that as enough for the force_ssl config.
The code path is that Rails
https://github.com/rails/rails/blob/main/actionpack/lib/action_dispatch/middleware/ssl.rb (which, as per the comments in that file, is included in the request chain when config.force_ssl is true) will call request.ssl?
request in that context is an instance of https://github.com/rails/rails/blob/main/actionpack/lib/action_dispatch/http/request.rb, but you won't find the ssl? method defined there; however, that class includes include Rack::Request::Helpers.
If you go to Rack's source code, you'll find the ssl? method defined in https://github.com/rack/rack/blob/master/lib/rack/request.rb, where it calls scheme method, and the X-Forwarded-Proto header will be checked in the elsif forwarded_scheme part. And there you go :) Even tough it's a HTTP request, the presence of that header will make the request.ssl? return true.
Why does traffic from the health check get redirected?
In this case, it's a HTTP request from the load balancer to your application that doesn't carry that X-Forwarded-For or X-Forwarded-Proto header, so Rail's config.force_ssl is doing what it's supposed to do and redirecting.
Lastly, check out the documentation on config.force_ssl. You actually can set it to a hash of options, and exclude your healthcheck urls from the force_ssl behavior!
config.ssl_options = { redirect: { exclude: -> request { request.path =~ /healthcheck/ } } }

Related

Spring Security Azure AD redirect url issue

When I try running in localhost, it works fine. But when I try running the same behind a load balancer, it gives the following error:
AADSTS50011: The reply URL specified in the request does not match the reply URLs configured for the application: '<clien-id>'.
I have registered the application at AzureAD with the load balancer URL. But when I send my request, the redirect URL is still localhost as shown below.
https://login.microsoftonline.com/common/oauth2/authorize?response_type=code&client_id=XXX&...**redirect_uri=localhost:8080/login/oauth2/code/azure**&nonce=...
I want my application to insert the load balancer URL as the value of redirect_url (instead of localhost).
I tried the solutions suggested below and still not successful:
Redirect URL for Spring OAuth2 app on Azure with Active Directory: Invalid Redirect URI Parameter
Spring Boot using Azure OAuth2 - reply URL does not match error
Thanks in advance.
When you use a load balancer/proxy, you need to add some extra configuration to make it possible to resolve the redirect URL correctly.
A load balancer usually applies the standard RFC7239 "Forwarded Headers" like X-Forwarded-Proto and X-Forwarded-Host. In that case, the redirect url should be correctly computed after applying the following two configurations. (Example for the Tomcat scenario)
server.forward-headers-strategy=NATIVE
"If the proxy adds the commonly used X-Forwarded-For and
X-Forwarded-Proto headers, setting server.forward-headers-strategy to
NATIVE is enough to support those."
server.tomcat.redirect-context-root=false
If you are using Tomcat and terminating SSL at the proxy,
server.tomcat.redirect-context-root should be set to false. This
allows the X-Forwarded-Proto header to be honored before any redirects
are performed.
The above configuration works if you use a placeholder for the base URL in your client configuration in Spring Security, for example {baseUrl}/login/oauth2/code/{registrationId}. In this way, the {baseUrl} placeholder is dynamically resolved by Spring Security differently depending on whether it's behind a load balancer or not (https://your-lb-url.com vs http://localhost:8080).
More info in the official documentation:
Spring Boot - Running Behind a Front-end Proxy Server
Spring Security - Proxy Server Configuration

AWS Cloudfront + Load Balancer, try to change domain from load balancer subdomain

I face the same situation, except that my framework is Ruby on Rails 4.2.6 (Ruby version 2.2.4)
I have do exactly the solution told, but when I try to login, always redirect to root page.(still not logined)
And I checked server log, login status was 200 success.
another clue is that when I go to the page which not enable
before_action :authenticate_user!
everything works fine. (domain not redirect to elb domain)
I think the problem is in the login part, but still not find the exact bug and solution.
How to make ec2 catch the host we expected (example.com), not elb host (elb.example.com)
Configure the CloudFront Cache Behavior settings to whitelist the Host header for forwarding. You may also need to whitelist one or more cookies, and possibly query strings. CloudFront forwards minimal headers by default, and no query parameters or cookies.
As a rule, the more things you forward, the lower your cache hit ratio... but obviously certain things must be forwarded unless the site is entirely static.

Rails on Elastic Beanstalk, Can't do a load balancer health check

I am running a rails app on elastic beanstalk, my problem is that I cannot use the load balancer health check, as it checks via http, not https. I have config.force_ssl set to true, and don't really want to change it, since it means I have 100% ssl coverage, HSTS, and secure cookies.
I have setup a controller to check at /health and can access this via curl.
There seems to be a contradiction between what is shown in elastic beanstalk (EC2 Instance Health Check), where I can only change the path and some timings, and the health check in the load balancer in EC2, where I can change the ping protocol, port and path to what I want, but this seems to have no effect other than causing a 503 error.
Ping Protocol: https
Ping Port: 443
Ping Path: /health
The load balancer has ports 80 and 443 open but redirects from 80 to 443 due to force_ssl.
Does anyone know of the correct settings to get around this, as I won't be able to scale the application without it?
Thank you
Eamon
The way to do this is to add this to your config, like this.
config.force_ssl
config.ssl_options = { redirect: { exclude: -> request { request.path =~ /health/ } } }
This is as per the rails docs and is valid for Rails 5:
http://api.rubyonrails.org/classes/ActionDispatch/SSL.html
Small hint for the accepted answer:
In case you have updated from Rails 4 to Rails 5 and have therefore a new_frameworks_default.rb in your initializers directory. You need to remove (or out-comment) the following line:
Rails.application.config.ssl_options = { hsts: { subdomains: true } }
Otherwise ssl_options is already set.
The accepted answer kept not working for me, but after hours of debugging I found this gem:
https://github.com/lserman/aws-healthcheck
It returns 200 on /healthcheck which works like a charm.
If, like me you are not using rails 5, the other way you can do this is to disable force SSL, and use NGINX to force all traffic onto https... You can use an ebextension file to do this. The exact version you need comes direct from AWS:
https://github.com/awslabs/elastic-beanstalk-docs/blob/master/.ebextensions/aws_provided/security%20configuration/https-redirect-ruby-puma.config
If you need more information on ebextensions, you can find them in the docs http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/ebextensions.html
EDIT: The original link no longer works...
Try this:
https://github.com/awsdocs/elastic-beanstalk-samples/blob/master/configuration-files/aws-provided/security-configuration/https-redirect/ruby-puma/https-redirect-ruby-puma.config

Is it necessary to force_ssl? Or should the SSL terminate at the load balancer?

On AWS OpsWorks. I'm using an ELB, which has my CA's SSL certificate.
The first point of access is always the load balancer (ELB). The ELB directs traffic to the instances. The instances each have a copy of the Rails app, Unicorn, etc.
One thing to note. The instances behind the ELB cannot be accessed directly.
At this point, do I need to force_ssl in Rails? I hear it's common enough to terminate SSL at the border (ELB).
As far as I've read, force_ssl gives the following:
Automatic redirect traffic from http to https.
Flagging cookies as secure and some added protection (i.e. against MITM attacks).
http://api.rubyonrails.org/classes/ActionController/ForceSSL/ClassMethods.html only indicates http to https redirection.
What does force_ssl do in Rails? second answer suggests that force_ssl does more than redirection.
If I decide not to use force_ssl, I can manage redirects by writing Nginx definitions.
Given the scenario, it feel like forcing SSL via Rails is obsolete, since the SSL negotiation is already happening in the ELB. Is it still necessary to force_ssl? Are there any added benefits?
if you're terminating SSL at the ELB level you don't want it. (you want to take http traffic and not be redirected).
bear in mind that in this case the traffic between the ELB and your backend instances will be over HTTP (i.e. not encrypted). this is fine for most cases.

Elastic Load Balancer on port 443 works for forced SSL Ruby On Rails application, but why?

My ruby on Rails application is configured with the following:
config.force_ssl = true
And I set up the following elastic load balancer:
With this configuration everything works, but I do not understand why? with the code above, my application instance will return a 301 redirect in response to HTTP request. When the HTTP request is handled by the load balancer, it is forwarded on to to the instance as a HTTP request. Shouldn't this result in another 301, and therefore an endless loop?
EDIT
I thought a bit about my answer and decided to get in to some more detail with it.
Network communication is usually composed of several layers, among which are the physical layer, which is the cable/radio channel where information travels through, the transport layer which is often TCP/IP, the protocol layer which in our case is usually HTTP or HTTPS and finally the application layer which is what our rails app handles.
Rails usually never gets in touch with the actual HTTPS data stream, as this is handled by your webserver. So how does force_ssl work at all?
The protocol layer is handled by the webserver (nginx, mongrel...) and this is who could care first about forcing ssl. When the webserver hands over a request to the application layer (hence, the rails app), it also provides a lot of meta data, which includes requester IP, request path, request format, a lot of header variables and also information about the used protocol.
When a request arrives at your webserver on port 443 (and uses HTTPS protocol), the webserver sets the header flag SERVER_PROTOCOL to https.
If a proxy server (like load balancer is) receives a request on 443 and forwards it to 80, it adds the X-FORWARDED-PROTO=https header to the request, which is made available for your rails app by the webserver.
Now, long story short: config.force_ssl requires SERVER_PROTOCOL OR X-FORWARDED-PROTO to denote https.
ORIGINAL ANSWER
The rails force_ssl method does not really force a request to arrive on port 443 on your server, it is satisfied when the original (client) request was sent over ssl through the internet. The load balancer (as a proxy) sets the header X-FORWARDED-PROTO to "https". rails trusts that information and that is why this is working.
More info on that can be found in the elastic load balancer docs: http://docs.aws.amazon.com/ElasticLoadBalancing/latest/DeveloperGuide/TerminologyandKeyConcepts.html#x-forwarded-for

Resources