Skip to content

Instantly share code, notes, and snippets.

@tadast
Forked from trcarden/gist:3295935
Last active June 19, 2025 23:35
Show Gist options
  • Save tadast/9932075 to your computer and use it in GitHub Desktop.
Save tadast/9932075 to your computer and use it in GitHub Desktop.
localhost SSL with puma
# 1) Create your private key (any password will do, we remove it below)
$ cd ~/.ssh
$ openssl genrsa -des3 -out server.orig.key 2048
# 2) Remove the password
$ openssl rsa -in server.orig.key -out server.key
# 3) Generate the csr (Certificate signing request) (Details are important!)
$ openssl req -new -key server.key -out server.csr
# IMPORTANT
# MUST have localhost.ssl as the common name to keep browsers happy
# (has to do with non internal domain names ... which sadly can be
# avoided with a domain name with a "." in the middle of it somewhere)
Country Name (2 letter code) [AU]:
...
Common Name: localhost.ssl
...
# 4) Generate self signed ssl certificate
$ openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
# 5) Finally Add localhost.ssl to your hosts file
$ echo "127.0.0.1 localhost.ssl" | sudo tee -a /private/etc/hosts
# 6) Boot puma
$ puma -b 'ssl://127.0.0.1:3000?key=/Users/tadas/.ssh/server.key&cert=/Users/tadas/.ssh/server.crt'
# 7) Add server.crt as trusted !!SYSTEM!! (not login) cert in the mac osx keychain
# Open keychain tool, drag .crt file to system, and trust everything.
# Notes:
# 1) Https traffic and http traffic can't be served from the same process. If you want
# both you need to start two instances on different ports.
#
#
@webdevotion
Copy link

@stiller-leser's solution worked for me.

Thanks everyone who chimed in with their solutions and feedback.

  • puma (3.11.4)
  • rails 5.2.0
  • ruby 2.5
  • dockerized environment

My puma.rb:

# Puma can serve each request in a thread from an internal thread pool.
# The `threads` method setting takes two numbers: a minimum and maximum.
# Any libraries that use thread pools should be configured to match
# the maximum value specified for Puma. Default is set to 5 threads for minimum
# and maximum; this matches the default thread size of Active Record.
#
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
threads threads_count, threads_count

# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
#
port        ENV.fetch("PORT") { 3000 }

# Specifies the `environment` that Puma will run in.
#
environment ENV.fetch("RAILS_ENV") { "development" }

# Specifies the number of `workers` to boot in clustered mode.
# Workers are forked webserver processes. If using threads and workers together
# the concurrency of the application would be max `threads` * `workers`.
# Workers do not work on JRuby or Windows (both of which do not support
# processes).
#
# workers ENV.fetch("WEB_CONCURRENCY") { 2 }

# Use the `preload_app!` method when specifying a `workers` number.
# This directive tells Puma to first boot the application and load code
# before forking the application. This takes advantage of Copy On Write
# process behavior so workers use less memory.
#
# preload_app!

# Allow puma to be restarted by `rails restart` command.
plugin :tmp_restart


if Rails.env.development?

  localhost_key = "#{Dir.pwd}/#{File.join('config', 'certs', 'localhost.key')}"
  localhost_cert = "#{Dir.pwd}/#{File.join('config', 'certs', 'localhost.crt')}"

  unless File.exist?(localhost_key)
    def generate_root_cert(root_key)
      root_ca = OpenSSL::X509::Certificate.new
      root_ca.version = 2 # cf. RFC 5280 - to make it a "v3" certificate
      root_ca.serial = 0x0
      root_ca.subject = OpenSSL::X509::Name.parse "/C=BE/O=A1/OU=A/CN=localhost"
      root_ca.issuer = root_ca.subject # root CA's are "self-signed"
      root_ca.public_key = root_key.public_key
      root_ca.not_before = Time.now
      root_ca.not_after = root_ca.not_before + 2 * 365 * 24 * 60 * 60 # 2 years validity
      root_ca.sign(root_key, OpenSSL::Digest::SHA256.new)
      root_ca
    end

    root_key = OpenSSL::PKey::RSA.new(2048)
    file = File.new( localhost_key, "wb")
    file.write(root_key)
    file.close

    root_cert = generate_root_cert(root_key)
    file = File.new( localhost_cert, "wb")
    file.write(root_cert)
    file.close
  end

  ssl_bind '0.0.0.0', '8443', {
    key: localhost_key,
    cert: localhost_cert
  }
end

@gugat
Copy link

gugat commented Sep 20, 2018

To fix "Your connection is not private" for Google Chrome, allow invalid certificates for resources loaded from localhost:

chrome://flags/#allow-insecure-localhost

image

@scottjacobsen
Copy link

There is a fantastic tool called mkcert which eliminates most of the pain of generating self signed certs and installing them as trusted certs on your machine - https://github.com/FiloSottile/mkcert. Way easier than trying wrangle OpenSSL commands and APIs.

@anon987654321
Copy link

For what it's worth, here's a simpler and more secure alternative to Linux for hosting your static sites and Rails apps on the same server: https://gist.github.com/anon987654321/4532cf8d6c59c1f43ec8973faa031103

@TheNotary
Copy link

I'm seeing this error on modern versions of Chrome.

2020-06-26 09:23:21 -0500: SSL error, peer: 127.0.0.1, peer cert: , #<Puma::MiniSSL::SSLError: OpenSSL error: error:141F7065:SSL routines:final_key_share:no suitable key share - 337604709>

According to google, this indicates that the cert generated in this guide is insecure and not supported in SSL 1.3

@anon987654321
Copy link

OpenSSL is fundamentally insecure. I'd recommend checking out my gist above which is based on OpenBSD's OpenSSL rewrite LibreSSL:

https://www.libressl.org/

OpenBSD's acme-client is also the most secure cert generator around. Check it out!

@TheNotary
Copy link

Can such cert be generated on a mac, @anon987654321? I'd love to get away from openssl, but it seems like it's the only TLS group that figured out how to do distribution of their software (which is clearly a bit less than ideal given their quality track record).

@liam-le-goldenowl
Copy link

thank you, it work for me

@etozzato
Copy link

etozzato commented Aug 6, 2021

Thank you for this great thread!

This is my adaptation: https://gist.github.com/etozzato/0ba2140ea3c6125d4839373309fe733a

  • Allows for a domain and wildcard subdomain;
  • Cleans up after itself in case of error;
  • Will still boot puma (no SSL) in case of error;

@basicfeatures
Copy link

@TheNotary thanks for getting back at me. You'd probably have to spawn a new server using OpenBSD, check out:

https://github.com/basicfeatures/openbsd-rails

Does SSL/TLS termination before Puma as Puma isn't really suited for this. Check out https://github.com/ErwinM/acts_as_tenant for multiple domains/subdomains, or message me.

@etozzato I might be wrong, but your gist looks over-engineered.

@etozzato
Copy link

@etozzato I might be wrong, but your gist looks over-engineered.

yes, it's plausible! 👍

@calebhaye
Copy link

calebhaye commented Sep 27, 2021

You can generate a trusted localhost cert by using letsencrypt and creating a certificate like localhost.domain.com (or *.localhost.domain.com for wildcards), verify that with a dns challenge, which usually involves creating an _acme_challenge TXT record. Then, once you have passed the challenges and have the cert, point localhost.domain.com to 127.0.0.1

If you have a multi-tenant app, you can create a wildcard cert also, but you'll have to go through the extra step of manually adding subdomains to localhost.domain.com to/etc/hosts and your config/enviroments/development.rb (assuming this is a rails app)

@allencch
Copy link

In order to run with Rails (version 7),

bin/rails s -u puma -b 'ssl://127.0.0.1:3000?key=server.key&cert=server.crt&verify_mode=peer&ca=server.crt'

@pirkka
Copy link

pirkka commented May 27, 2022

There is a fantastic tool called mkcert which eliminates most of the pain of generating self signed certs and installing them as trusted certs on your machine - https://github.com/FiloSottile/mkcert. Way easier than trying wrangle OpenSSL commands and APIs.

I would like to recommend this approach as well.

I am no SSL guru, so I had a long battle trying to get local SSL to work a my new computer (it works fine on my older one). At some point I even had subjectively non-deterministic results where my SSL would work for a minute or two and then stop working with no apparent change in anything.

Using the mkcert on my macOS computer via homebrew solved the problem very quickly and easily.

@chaffeqa
Copy link

I created a fork of @etozzato great sample, but added automatic local trusting per OS:

https://gist.github.com/chaffeqa/d6c6ac491d3e1824a2980607d796e4a8

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment