Let's Encrypt with a Rails app on Heroku

Padlock by Moyan Brenn is licensed under CC BY 2.0

Updated Aug 30 2016: Changed renew command as the old one wasn’t working.

Updated June 3 2016: Let’s Encrypt command-line client is now Certbot

Updated April 14 2016: Added info about renewing based on Jenny’s comment and added notes about the homebrew package.

I needed to renew an SSL certificate today, so I used it as an excuse to try Let’s Encrypt for a free certificate.

I’m on a Mac and I have homebrew already installed. I’m using inchworm.io as my example, so use your own domain where appropriate.

First, grab the Let’s Encrypt client, certbot:

On a mac, simply run:

brew install certbot

On other platforms install and run the certbot-auto command everywhere I use certbot.

Let’s Encrypt’s ideal workflow involves running it on your server. Since we’re not “on” the Heroku server, we have to do it locally and in a few steps.

sudo certbot certonly --manual

You’ll be prompted to enter the domain you want a certificate for, and another prompt or two.

Screen shot from Terminal during the Let's Encrypt process

Then you’ll see a message about the next steps. Don’t continue on yet! Read the message that begins like this:

Make sure your web server displays the following content at
http://inchworm.io/.well-known/acme-challenge/ya6k1ed-SOME-LONG-URL before continuing:

ya6k1edW38z-your-value-here

We need the server to serve this code at that particular URL to verify that it is indeed ours.

In Rails, we first add a route to config/routes.rb:

get '/.well-known/acme-challenge/:id' => 'pages#letsencrypt'

I used PagesController since I already had one for our static pages, but you can use any controller (or a new one)[1].

Then we’ll add the actual controller code. Remember to substitute your long code here[2]:


  class PagesController < ApplicationController
    def letsencrypt
      # use your code here, not mine
      render text: "ya6k1edW38z-your-value-here"
    end
  end

Once this is in your app, deploy it to Heroku.

Then, and only then, do you continue the Let’s Encrypt process.

If you did it right, you’ll see a message like this:

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at
   /etc/letsencrypt/live/inchworm.io/fullchain.pem. Your cert will
   expire on 2016-04-11. To obtain a new version of the certificate in
   the future, simply run Let's Encrypt again.

That means it worked and you have your certificate!

The final step is to upload the certificate to Heroku.

If you are replacing an existing certificate[3]:

sudo heroku certs:add /etc/letsencrypt/live/inchworm.io/fullchain.pem /etc/letsencrypt/live/inchworm.io/privkey.pem

If this is your first time adding a certificate to your Heroku app, you need to enable the SSL Endpoint ($20/month) and add the cert:

heroku addons:create ssl:endpoint
sudo heroku certs:update /etc/letsencrypt/live/inchworm.io/fullchain.pem /etc/letsencrypt/live/inchworm.io/privkey.pem

Adding the SSL Endpoint changes the CNAME you need to point your custom domain to. You’ll need to edit your DNS using the new value listed in your Heroku dashboard for it to work correctly.

Done!

Screen shot of Safari showing the certificate is installed.

Ready to upgrade your certificate? Run this command to renew:

sudo certbot certonly --manual -d inchworm.io

Then go back up to set the new challenge key and upload the certificate when complete.

There are a few things I’d like to see improved, and I expect they will be soon.

  1. A homebrew package for certbot. I expect we'll see this after the bugs are worked out.

    This exists now!

  2. Don’t require sudo. I expect this will come, but most use cases today will need sudo, so I understand why they require it.
  3. Heroku should provide this as an automated service. I hope they do before my 90-day certificate expires, but otherwise I’ll do this process again.

[1] A static file would work too and skip the route.

[2] You can also use an ENV variable for this, but I’m hoping this is automated by Heroku soon.

[3] Thanks to Jenny’s comment for pointing out the syntax for first-timers and Olivier Lacan for prodding me to edit the post to include it.

Photo of Daniel Morrison

Among colleagues, Daniel is known for rambling about algorithm design or obscure academic terms, which he blames on his classical training in Computer Science. Among clients, Daniel is known for being honest and direct when trying to get clients to focus on value and efficiency, which he blames on his unwavering quest for…value and efficiency.

Daniel founded Collective Idea in 2005 to put a name to his growing and already full-time freelance work. He works hard writing code, teaching, mentoring, and speaking about the company’s processes and tools.

Comments:


Post a Comment

(optional)
(optional — will be included as a link.)
  1. Dan thanks for the great article, I’ve been trying to get this working with Heroku since the Beta opened with no luck. Just one note, this article details how to get Let’s Encrypt working if you already have SSL working on Heroku. If you don’t there are few extra steps. Instead of using Dan’s “final step” of

    sudo heroku certs:update /etc/letsencrypt/live/inchworm.io/fullchain.pem /etc/letsencrypt/live/inchworm.io/privkey.pem

    You need to first create the SSL endpoint on Heroku (which costs $20/month):

    sudo heroku addons:create ssl:endpoint

    Then you can add an SSL cert:

    sudo heroku certs:add /etc/letsencrypt/live/inchworm.io/fullchain.pem /etc/letsencrypt/live/inchworm.io/privkey.pem

    Once you’ve successufly added the SSL endpoint you need to update your DNS to match the end of the line starting with:

    inchworm.io now served by (has new domain to point your DNS to here)

    Thanks for the great article!

    jennykortina@gmail.com
    Jenny
    January 13, 2016 at 0:52 AM
  2. Thank you Dan for this article! I could easily setup my first Let’s Encrypt certificate with your help. I only encountered a single issue. Not because your article was not complete, but because my always-on “TLDR;” attitude made me skip this last-yet-critical step you mention at the end of the article: you need to update the DNS with the SSL-specific name Heroku has just gave you.

    If you’re willing to update your article, I suggest to add a screenshot. This is the kind of eye catching element that triggers a “Oh, this is another step I have to consider” in lazy reader’s mind like me :)

    You can find one here: http://stackoverflow.com/questions/31960487/ssl-invalid-cn-after-installing-certificate-getting-cn-herokuapp-com/36006740#36006740

    Thank you again!

    March 15, 2016 at 9:25 AM
  3. A tricky edge case we stumbled upon in our project was trying accessing the validating url through https. Since I couldn’t find out how to use SSL for the validating url I had to change `config.force_ssl=false`. Luckily we manage the SSL enforce through an ENV variable, so it didn’t require a dedicated deploy.

    March 16, 2016 at 11:09 AM
  4. Hi Dan,

    I just had to update my cert that I generated following your original tutorial. To update cert:

    1. Launch let’s encrypt:

    cd letsencrypt ./letsencrypt-auto renew –dry-run –agree-tos –manual-public-ip-logging-ok

    1. Update StaticPagesController to match new code provided
    2. Deploy to Heroku
    3. Continue command line (hit enter)
    4. It will prompt if successful. If so run for real:

    ./letsencrypt-auto renew –agree-tos –manual-public-ip-logging-ok

    jennykortina@gmail.com
    Jenny
    March 23, 2016 at 15:00 PM
  5. One relatively important detail to remember: if you want both your root domain (example.com) and your `www` subdomain to be supported under one certificate, I highly recommend submitting: example.com, www.example.com in the Let’s Encrypt client domain prompt.

    Otherwise, if you use a CNAME redirect to the Heroku endpoint for your `www` subdomain, you will see SSL errors because the certificate is only valid for the root (example.com) and not the subdomain (www.example.com).

    March 28, 2016 at 21:05 PM
  6. “If this is your first time adding a certificate” instructions have a typo — the command should be “certs:add”

    emirotin@gmail.com
    Eugene
    April 15, 2016 at 13:30 PM
  7. Let´s Encrypt is interesting and easy way to HTTPS protocol. But will the trustworthy also same in the future? Nowdays you can see some problems with phishing website with https becuse of any real validation. Maybe if I can choose, i will prefer free SSL certificate Basic DV https://magazin.sslmarket.de/inpage/lets-encrypt-vs-basic-dv-von-symantec-ein-vergleich/

    BTW: You have a bug in your comment form. If I make it longer more than first comment. The form is behind this comment and I can not give it back.

    July 04, 2016 at 7:52 AM
  8. LetsEncrypt is designed for automatic renewal. That’s why their certificates only last 90 days. I wrote a gem that encapsulates both provisioning & renewing a LE SSL certificate on Heroku. At the moment it only supports their new free SNI-based SSL, but I’d like to add support for their paid-for SSL endpoint too.

    Comments & feedback appreciated: https://github.com/pixielabs/letsencrypt-rails-heroku

    August 03, 2016 at 7:43 AM
  9. @David Somers: Cool idea, we’ll check it out!

    You should really hook up Dead Man’s Snitch (Heroku Addon or deadmanssnitch.com) to that Heroku Scheduler job! ;)

    August 03, 2016 at 14:14 PM
  10. Great article!

    There’s a small typo on the “If this is your first time adding a certificate to your Heroku app” part. You give this command:

    sudo heroku certs:update /etc/letsencrypt/live/inchworm.io/fullchain.pem /etc/letsencrypt/live/inchworm.io/privkey.pem

    But, instead, it should be:

    sudo heroku certs:add /etc/letsencrypt/live/inchworm.io/fullchain.pem /etc/letsencrypt/live/inchworm.io/privkey.pem

    August 17, 2016 at 14:28 PM
  11. @Johann Pardanaud: Thank you for catching that! We’ll get that changed. :)

    August 18, 2016 at 13:42 PM
  12. Thanks for the great article!

    I wanted to secure a few subdomains + my root.

    I modified your code slightly like this to handle it through a config var lets encrypt content looks like this in secrets.yml: lets_encrypt_content: and then on Heroku it looks like challenge.response,challenge2.response2,challenge3.response3, etc.

    Then I modified the controller code like this:

    def letsencrypt id = params[:id] values = Rails.application.secrets.lets_encrypt_content

    lookup = Hash.new values.split(‘,’).each do |value| vals = value.split(‘.’) lookup[vals[0]] = vals[1] end render text: “#{id}.#{lookup[id]}” end

    Now I can add multiple domains by changing the heroku config var LETS_ENCRYPT_CONTENT.

    August 30, 2016 at 2:35 AM
  13. David Smith: Very nice!

    August 30, 2016 at 12:35 PM
  14. Does it work with Ruby 5.0? 1. Expected behavior

    Visiting /.well-known/acme-challenge/y7HlW6w…… renders a challenge token

    1. Actual behavior

    Visiting /.well-known/acme-challenge/y7HlW…….. renders a not found message

    rails 5.0.0 ruby 2.3.0

    NB: if i remove the . (in front of .well-known in the browser), it seems to work.

    annie@agentbright.com
    annie
    August 30, 2016 at 16:23 PM
  15. @annie: Yes, I have this working with Rails 4 and 5. The period is required, so maybe something is up with your routes?

    August 30, 2016 at 19:04 PM
  16. Hi Dan, Put the code above in the routes (at the very top).

    I am wondering if maybe one of our gems is causing the problem? http://stackoverflow.com/questions/39250336/cant-create-a-route-that-has-a-segment-with-a-leading-dot-in-rails-to-verify-l

    annie@agentbright.com
    annie
    August 31, 2016 at 13:17 PM
  17. +1 for Heroku supporting this automagically in the future!

    I’ve created some brief instructions on how to do this with Django apps. Pretty close to how you’ve done it with rails: https://github.com/mattharley/pdpdmeetup/wiki/SSL-Lets-Encrypt

    September 28, 2016 at 4:25 AM
  18. There’s no need for a controller, you can just handle it in your routes file itself using a proc

    get ‘/.well-known/acme-challenge/:id’, to: proc { env [200, {‘Content-Type’ => ‘text/plain’}, [‘ya6k1edW38z-your-value-here’]] }
    October 25, 2016 at 22:51 PM
  19. Hey Daniel, thanks for this post, it helped us a lot. Just a quick FYI: Sometime in the last week or so the source code formatting broke—Lots of tags appearing in the source rather than syntax highlighting.

    smeves@gmail.com
    Scott M
    November 08, 2016 at 18:14 PM
  20. Thanks Daniel for such clear instructions! Straight-forward and worked like a charm! Also, should point out that Heroku now has free SSL, so no more $20/month for SSL EndPoint add-on. =)

    Jeff
    January 05, 2017 at 1:36 AM
  21. Jeff: Great point. We’ve been using free SSL too, and love it!

    January 05, 2017 at 14:11 PM
  22. Thank you for this tutorial!

    stefan.huska@gmail.com
    Stefan Huska
    January 12, 2017 at 11:04 AM
  23. thanks, it was helpful. twice.

    andrei
    March 02, 2017 at 16:00 PM
  24. Thanks a lot! Worked like a charm.

    March 04, 2017 at 0:22 AM