Securing Padrino apps with https and ssl on Heroku
Let’s Encrypt has done an amazing job of making
https the new normal for web sites and helping create a more secure and private internet by giving away free, automatic ssl certificates to domain owners.
You should be encrypting your web traffic (and need to, to take advantage of new protocols like
http/2. Some implementations have stated they will only support http/2 over an encrypted connection. And currently, no browser supports http/2 unencrypted afaik.). Add to this the fact Google will start penalizing non-secured sites in search results, and
https is fast becoming the de facto standard.
What does this mean if you’re running a Padrino application?
It turns what used to be a difficult, complex process of certificate management, configuration, and Rack middleware chains (which were quite old), and that could not use padrino’s native Sinatra-based sessions management, into dead-easy ssl and https provision. Since I hadn’t seen a post detailing this, and most posts dealing with getting padrino and ssl going together were quite old, I wanted to write something google-able to save other people the effort of puzzling this out.
You can do everything I've described below from Heroku's well-designed web
interface. However, I tend to favour and drive most things off the excellent
Heroku CLI. If you haven’t got it installed, and are on OSX, use
homebrew and simply
brew install heroku. With the heroku
cli you can do everything from the command line (as well as script), which is
handy if you’re big on the terminal. If you're on linux or windows, alternative installation instructions are here. Both homebrew and the heroku cli are going to make your dev life easier if you're not already using them.
Setting up Padrino
At time of writing, I was using Padrino v. 0.14.0.1.
In general, in your app(s), you just need to set
# app.rb enable :sessions
You probably already have this set if you’ve used the
padrino g admin
generator for creating an admin panel, or are using any sort of functionality
which requires logging in or somehow managing session state. This turns on Padrino's native sessions management, which uses Sinatra's sessions and is handy since you can use the session hash between routes and views. It stores all the data in the cookie. Also, make sure to check that your app has a random
session secret set in
config/apps.rb, which should be a random 64 character hexadecimal string already created by Padrino for you in the
Padrino.configure_apps block (nb: you can also
enable: sessions there for all apps in the Padrino application if you prefer rather than setting it in
There are other ways to get sessions in Padrino, including using
Rack::Session::Pool. The former is useful if you
need to add extra parameters to the cookies (for example, expiry) and the latter
is faster than pure cookie-based sessions as it only stores an ID in cookie but
maintains the rest of the session hash in an instance variable called
that's available to you (but the session data does not persist across app
restarts). My use case meant Padrino's native sessions simplified a lot. YMMV.
Setting up DNS
https working, you need to have a qualified domain name for your
application. This is fairly simple on Heroku, and just a matter of making sure
you have a
CNAME record in your DNS provider records (I personally use the
excellent DNSimple) pointing at your
app on heroku and making sure you’ve let your Heroku app know it's the endpoint
for that CNAME by having set the qualified domain in the app’s
Settings | Domains and certificates section in the web interface and explicitly hitting the
Add domain button and adding it in.
You can also perform this from the cli with
🚀 ~ heroku domains:add cool.example.com --app your_app_name
One gotcha on this: You need to make sure you have your DNS pointing at the
herokudns.com domain in your application (an issue may be older apps
that may be pointing at
herokuapp.com or similar) If this is you, it might
make sense to create and redeploy the app on the newer heroku stack to support
this rather than workarounds, and repoint your DNS. If you do not have your DNS configured and ready when you upgrade the dyno and trigger ACM, certificate verification will fail.
You can verify that you’re actually pointed at
herokudns.com by running
🚀 ~ heroku domains --app your_app_name
If it comes up looking something like this with the herokudns.com,
=== your_app_name Custom Domains Domain Name DNS Target ───────────────────────── ─────────────────────────────────────── cool.example.com cool.example.com.herokudns.com
you’re good to go.
Setting up the SSL Certificate and https
This is usually the most painful part of getting ssl going on any system. I have to say Heroku has done an amazing job of making this utterly painless through their Automatic Certificate Management. Big fan.
Just upgrade your dyno from Free to Hobby (or more). At time of writing, a Hobby dyno costs $7 USD/month/dyno.
🚀 ~ heroku ps:resize hobby --app your_app_name
When you upgrade your Heroku dyno(s) from the Free tier to Hobby, the ACM will automatically start generating you a cert. It does take about 45-60 minutes, but you can obsessively check on its progress with:
🚀 ~ heroku certs:auto --app your_app_name
As near as I can tell it’s fairly automatic and foolproof as long as you have your DNS configured and pointing correctly. Heroku did a great job with this.
When the response to the above command comes through with the Domain and an OK in the status,
=== Automatic Certificate Management is enabled on your_app_name Certificate details: Common Name(s): cool.example.com Expires At: 2017-07-19 15:03 UTC Issuer: /C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3 Starts At: 2017-04-20 15:03 UTC Subject: /CN=cool.example.com SSL certificate is verified by a root authority. Domain Status ───────────────────────── ────── cool.example.com OK
you're ready to move on to restricting the app to just
Setting up the app for https only
Now, what if you want to ensure that all traffic that gets sent to your
app, even if someone is using
http goes through
https to the app?
You could use some of the aforementioned middlewares: the
Rack::SSL and the
Rack SSL Enforcer gems seem popular. However, they break cookies if
you don’t disable native padrino/sinatra sessions, meaning you can’t use
enabled: sessions . Since I didn’t want to mess with the
existing sessions functionality I already had built, tested, and working (and, as some of those middlewares had not been updated in years), I went a simpler route.
An easier solution is to simply use a
before filter on the app(s):
# app.rb unless RACK_ENV == 'development' before do redirect request.url.sub('http', 'https') unless request.secure? end end
This seemed simple, explicit, code-transparent, and meant I didn’t have to worry
about the extra complexities of a Rack middleware chain breaking padrino’s
Sinatra-based session management. All this does is test to see if the request is
secure, and if it isn’t, redirects it to the
https URL before processing. It ignores the directive in your development environment (I should note here I have ssl set up in both my production and staging environments. If you only need https in production, change the first line to
if RACK_ENV == 'production'.).
Really, the overall process is quite logical. It’s a matter of making sure your DNS is set up properly so the cert request can be verified, paying for a dyno, triggering the ACM request, it generating a cert for you, and then providing a redirect to https for all http requests.
In any case, that’s all there is to it. Once your cert verifies, you should be able to push your app to heroku via
git push heroku master and see it working in secure
https glory at the domain you've configured.