Search code examples
ruby-on-railssslhttpsconfigurationpuma

How do I configure my Rails app (using Puma) to serve over HTTPS?


I'm setting up a Rails application with Puma, I can't seem to make it serve over HTTPS. I've tried maybe fifteen different guides and stack over flow answers, but neither of them have worked for me. Now, I can't serve regular HTTP either, even when reverting the configuration.

What I've tried:

config/puma.rb

port ENV.fetch("PORT") { 3000 }

my_key = "#{File.join(Rails.root, 'public', '.well-known', 'privkey.pem')}"
my_crt = "#{File.join(Rails.root, 'public', '.well-known', 'cert.pem')}"

ssl_bind(
  'mydomain.com',
  3000,
  key: my_key,
  cert: my_crt,
  verify_mode: 'peer'
)

mydomain.com is of course replaced by the proper domain name.

config/application.rb

config.force_ssl = true

I've put my certificate and private key in public/.well-known. The files were generated using Certbot. Folder contents:

cert.pem
chain.pem
fullchain.pem
privkey.pem

The files were generated using Certbot.

The resulting error message is as follows: HTTP parse error, malformed request: #<Puma::HttpParserError: Invalid HTTP format, parsing fails. Are you trying to open an SSL connection to a non-SSL Puma?>

What am I doing wrong?

-

Also, when resetting my configuration and testing out the site, I get the same error message. What can be done about that?

SOLUTION: Check out the comments in the reply by Jan Vítek to see how this was solved.


Solution

  • I see a few problems here:

    1. This is not why it is not working, but please move your private key out of the /public path. It is not much private there.
    2. You are binding the SSL to the same port as the HTTP server
    3. You are binding the SSL to whatever.domain. It can bring many problems. If there is a NAT the app has no interface to bind to. If there is the server's FQDN in /etc/hosts it would bind to the localhost interface a you wouldn't be able to access it from the internet. If it actually bound to the interface with IP resolved from that domain, you might have problems accessing the app locally. Just change it to 0.0.0.0 for now and when you have it working you can experiment with this attribute.
    4. SSL verify mode peer would verify client certificates. I'm not sure if it is your intention but it would require :ca option to be passed to ssl_bind() with a path to your CA certificate
    5. You don't say if it is for development or production purposes. For production you might want to consider Apache + Passenger or Nginx + Passenger where the HTTPs would have been handled by the web server (Apache or Nginx)

    Anyway I created a new Rails 7 application, placed my crt and key to config/ssl/ and modified only the config/puma.rb

    #config/puma.rb
    max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
    min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
    threads min_threads_count, max_threads_count
    worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development"
    
    port ENV.fetch("PORT") { 3000 }
    
    ssl_bind '0.0.0.0', 3001, {
      key: 'config/ssl/dev.mydomain.com.key',
      cert: 'config/ssl/fullchain.cer',
      verify_mode: 'none'
    }
    
    environment ENV.fetch("RAILS_ENV") { "development" }
    pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }
    plugin :tmp_restart
    

    Then simple rails s and I am able to connect to https://dev.mydomain.com:3001. Make sure to explicitly call https://.