Heroku + Cloudflare: The Right Way

You might be missing out on these security steps when using Cloudflare with Heroku.

There are dozens of articles on how to setup your Heroku app to use Cloudflare. They all leave out two crucial security steps.

Encrypt and authenticate communication between Cloudflare and Heroku.

The first issue pertains to selecting the SSL mode for your app in Cloudflare. The option I’ve observed most people choosing, “Flexible SSL”, means the communication between Cloudflare and Heroku is left completely insecure. Which means all those nice passwords and personal data just get to float through the internet. Securing this communication is super important (especially in this day of data breach outrage), and really easy.

Choosing “Full SSL” is a decent option as it encrypts communication, but it’s still less secure than “Full (Strict)”. It will encrypt communication between Cloudflare and Heroku, but won’t perform authentication to ensure that Cloudflare is actually talking to your app.

So I’d recommend always using “Full (Strict)”. Setting things up to work properly for this SSL mode is straightforward: 1) generate an origin certificate on Cloudflare, 2) install that certificate on Heroku.

On the Crypto page in your Cloudflare account, find the section “Origin Certificates”, and hit “Create Certificate”. Choose the hostnames of your website (not your herokuapp.com or herokudns.com hostname), and choose an expiry (up to 15 years). Then in your Heroku app’s Settings page, scroll down to domains and certificates and click “Configure SSL”. Click the “Manually” option, and proceed to install the generated cert. (If you run into an error, and were previously using Automated Certificate Management, you may need to remove SSL first before clicking again to manually install a cert.)

When the certificate is installed, return to Cloudflare and change your SSL setting to “Full (Strict)”. You're done! Wasn't that easy?

Note that if you have multiple apps running on subdomains, be careful to change you master SSL setting only after you’ve completed the steps above for each app.

Force all traffic through Cloudflare.

Cloudflare is an awesome security tool. Among other things, we use Cloudflare to lock down the admin sections of our apps. However with a default Cloudflare + Heroku setup, it’s trivial to get around all of the security mechanisms that Cloudflare provides just by visiting a site via its ___.herokuapp.com hostname. (And yes, people probably will be able to guess your Heroku hostname. Especially if you use the same productname-production.herokuapp.com convention everyone else does :-) Thus is it critical to reject non-Cloudflare-issued requests to your Heroku app.

On a normal server, this problem would be easy to solve using client SSL certificates, however Heroku’s routing infrastructure doesn’t support their use. Instead we have to resort to two hackier solutions for origin protection.

The first step is to use a truly unguessable Heroku subdomain. (Yes, security through obscurity.) This isn’t a great option by itself, and you need to be very careful in your DNS management not to expose your Heroku hostname as a true CNAME.

The second, more rigorous step is to reject requests in your application from non-Cloudflare operated IPs and/or requests using the Heroku hostname. This doesn’t afford you any real protection against denial of service attacks (hence the first step), but it will at least ensure that your other security rules in Cloudflare can’t be circumvented.

If you have a Rails app, rejecting non-Cloudflare requests could look something like this:

# In /config/initializers/rack_attack.rb
# Using the `rack-attack` and `cloudflare-rails` gems.
# Set ENV["REJECT_UNPROXIED_REQUESTS"] in protected environments.

class Rack::Attack
  if ENV["REJECT_UNPROXIED_REQUESTS"]
    blocklist("block non-proxied requests in production") do |request|
      raw_ip = request.get_header("HTTP_X_FORWARDED_FOR")
      ip_addresses = raw_ip ? raw_ip.strip.split(/[,\s]+/) : []
      proxy_ip = ip_addresses.last

      if !(request.host =~ /heroku/) && ::Rails.application.config.cloudflare.ips.any?{ |proxy| proxy === proxy_ip }
        false
      else
        ::Rails.logger.warn "Rack Attack IP Filtering: blocked request from #{proxy_ip} to #{request.url}"
        true
      end
    end
  end
end

Adding these two layers of security to your Heroku + Cloudflare setup will save you a lot of pain, especially in the unfortunate case of an active attacker scenario. Add them now. You'll be glad you did later.


Lawson Kurtz is a Viget alumnus and senior software engineer at Unreasonable, a business accelerator that scales for-profit businesses making the world a better place.