Cloudflare + Heroku SSL / Certificates Explained

Jon Sully headshot

Jon Sully

@jon-sully

If you followed along in our recent three-parter all about Propshaft, static assets, and CDNs, you might be considering integrating a reverse-proxy CDN into your own app! As we mentioned in part two, that’s exactly what we recommend and follow for all of our own fresh applications. But if you’ve ever actually tried to integrate Cloudflare with Heroku, you might’ve run into the most common snag between the two. SSL certificates and proper HTTPS configuration.

A diagram showing web visitors wanting to visit a site hosted on Heroku, going through Cloudflare, with questions marks between all three parties representing confusion around how they all connect
What goes where?

So let’s explore and figure this out. We want to understand what’s actually happening here, how it ought to work, and how we actually set it up in practice (it’s super easy!)

What’s Actually Happening

Let’s rewind just a smidge. In “How CDNs Work” we walked out what actually happens when you have a properly configured, running CDN. The CDN intercepts requests for static assets and serves them on behalf of your application after loading them the first time. We used this image in that article:

A sequence chart showing the first few requests between the browser and the server, now showing the requests for css and js going to a CDN in the middle rather than the Heroku Dyno

Then we talked about how reverse-proxy CDNs actually sit between every requests to your application, only intercepting them and serving them directly when they’re requests for static assets. We showed this diagram there, too:

Diagram showing a browser requesting various content from a domain and the reverse proxy CDN in the middle deciding what goes through to origin and what it serves itself

And both of these tease around a point that we need to be more explicit about in this article to better understand the interaction. When a request hits Cloudflare for any proxied domain (that is, the ‘orange cloud’ is enabled in the Cloudflare DNS record), two separate web requests actually occur:

A diagram showing Cloudflare acting as a request middle-man and two actual requests happening between the web visitor and ultimate Heroku Dyno vs. just a single request

In the upper section, “Without Cloudflare”, we see that a request being made to the site / app resolves directly to, and is served by, the web dyno. As we’ve mentioned before, this is a totally fine setup for a small or proof-of-concept application, and it actually means your SSL setup will be simpler. In fact, if you’re not using any reverse-proxy CDN and your application’s web domain is CNAME’d to your ___.herokudns.com target, you can use Heroku’s automatic SSL management and be done:

A screenshot of Heroku’s SSL management dashboard showing the automatic management option enabled

But that’s not what we’re here for! That would mean we have no CDN in front of our application at all — which simply won’t do! We’re here for Heroku plus Cloudflare. So we need to go a little deeper and understand what’s happening in that “With Cloudflare” track from the image above.

The first key point to understand is that when Cloudflare is actively proxying your domain, the DNS target for that domain does not directly resolve to your dynos!

Screenshot of the `nslookup` tool resolving different IP addresses for Judoscale.com and the real herokuapp subdomain for this application

Here we see that directly querying the DNS system for judoscale.com resolves to the IP address 104.26.1.154. But what system / company / service owns that IP address? Let’s check.

WHOIS 104.26.1.154

NetRange:       104.16.0.0 - 104.31.255.255
CIDR:           104.16.0.0/12
NetName:        CLOUDFLARENET
NetHandle:      NET-104-16-0-0-1
Parent:         NET104 (NET-104-0-0-0-0)
NetType:        Direct Allocation
OriginAS:       AS13335
Organization:   Cloudflare, Inc. (CLOUD14)
RegDate:        2014-03-28
Updated:        2024-09-04
Comment:        All Cloudflare abuse reporting can be done via https://www.cloudflare.com/abuse
Comment:        Geofeed: https://api.cloudflare.com/local-ip-ranges.csv

Cloudflare! So when we hit judoscale.com, we’re actually connecting to a Cloudflare IP address, and thus, a Cloudflare server. Not a Heroku server.

On the other hand, if we query the DNS system for our __.herokuapp.com domain we see that it resolves to 18.208.60.216, which we can also check ownership of:

WHOIS 18.208.60.216

NetRange:       18.32.0.0 - 18.255.255.255
CIDR:           18.32.0.0/11, 18.128.0.0/9, 18.64.0.0/10
NetName:        AT-88-Z
NetHandle:      NET-18-32-0-0-1
Parent:         NET18 (NET-18-0-0-0-0)
NetType:        Direct Allocation
OriginAS:       
Organization:   Amazon Technologies Inc. (AT-88-Z)

While not as clear as “Cloudflare owns it!”, we know that Heroku uses AWS servers under the hood, so it’s clear enough to say that this domain resolves directly to our Heroku web dyno. And indeed, if we hit this domain with our web browser we do see our site!

A screenshot of the Judoscale website being resolved in a browser from the heroku-app domain

This duality illustrates a key point in this paradigm. When using a reverse proxy CDN the end-user never connects directly to your web dynos. That’s ultimately why there are two separate web requests that occur each time an end-user requests some HTML from your site/app. Since an end-user will always connect to Cloudflare’s servers for any of their requests, Cloudflare then needs to request content from our Heroku dyno on the user’s behalf if the request is for something Cloudflare can’t serve itself.

Diagram further showing the two-request paradigm between an end-user’s request that actually hits Cloudflare (via DNS) then a second request from Cloudflare to the Heroku dyno (via DNS)

Now, the reality is that this setup is fine and as-designed. Cloudflare’s network is extremely fast and built specifically for this architecture. It’s rare that we ever need to unravel this complexity and consider the dual requests…. But SSL certificates is indeed one of those rare cases.

Remember that SSL certificates essentially provide a third-party confirmation that the server presenting the certificate is indeed authorized to represent the domain you’re requesting (e.g. judoscale.com). But the tricky bit here is that in order to generate an SSL certificate, you need to prove both that you control the domain and that the server requesting the certificate is authorized to serve traffic for it. But that’s just it — we don’t control Cloudflare’s servers directly, and they’re the ones serving our domain!

And, to clarify, let’s put this back in the context of dual-requests.

A simplified diagram from above with the request between the end-user and Cloudflare being labeled Request 1, while the request from Cloudflare to the Heroku server being labeled Request 2

Since we have two different requests between three separate parties, we actually need to worry about two different SSL certificates.

How It Ought To Work

First, we want Request 1 to be secure. That is, the user should be navigating to a https:// and, ideally, Cloudflare should force requests to http:// over to https://. This is good for general user security and should be a baseline for any site / app. That means that Cloudflare needs to have an SSL certificate which covers our domain and that it serves freely.

Second, we want Request 2 to be secure. This is actually just as important as Request 1 but overlooked way too often. Let me put it this way — if a user is sending personal information to your app and it looks secure from their perspective (browser 🔒 symbol), it’s our duty to make sure that the security is upheld all the way to our origin server so that we’re not lying to our users. “Well it’s secure to our CDN but then plain-text from the CDN to our servers” is not what you want to tell your users should the issue ever arise.

Cloudflare calls this setup “Full” encryption, and it should be the standard for all web applications:

A screenshot of Cloudflare’s “full” encryption diagram in their dashboard UI
Dashboard->SSL/TLS->Overview

That is, both request 1 and 2 must be covered by verified, trusted certificates.

👀 Note

Just for context, Cloudflare offers a few different levels of SSL/TLS Encryption:

Screenshot from the Cloudflare dashboard showing all of the available SSL options, which we’ll now discuss

And each of these tiers slightly modifies how Cloudflare behaves when interacting with the end-user/site visitor, itself, and our origin servers. The top three are worth understanding in better detail — Full, Full (Strict), and Strict. The latter is only available to Cloudflare Enterprise customers (💸) and is the best choice, but as long as you’ve got the “Always Use HTTPS” switch enabled (see TIP below), Full (Strict) will have the same benefit.

Importantly, the difference between Full and Full (Strict) boils down to which certificate your origin server gives back to Cloudflare when Cloudflare attempts to setup a secure connection to your server. Full means that, as long as your origin server returns an SSL cert, it will use that cert for a secure connection. This is a good first step, but the whole point of an SSL certificate is to validate it against a third party (the party that issued the cert, ideally).

That’s precisely what Full (Strict) does — it requires a cert from your origin server and it validates that cert to ensure that it’s valid with the original issuing party. It ensures that the server acting as your origin isn’t giving back a random, unknown certificate. Instead, that it’s giving back a valid, legitimate certificate. The best way to get this working on Heroku is to setup your origin server with a certificate that Cloudflare itself generates! That way Cloudflare can immediately validate the cert.

So let’s talk about how we do that… and it’s not as hard as it sounds!

Putting It In Practice

The good news here is that getting all of these certs and layers going is actually quite simple.

First, the cert for Request 1 — between the user’s browser and Cloudflare. Great news here: Cloudflare automatically sets up a certificate for your domain when you enable the Cloudflare proxy (orange cloud) and automatically serves it. Cloudflare calls these certificates “Edge Certificates” and every plan (yes, free plans!) includes a Universal certificate that covers the root domain and all single-layer subdomains (*.example.com).

Screenshot of the Cloudflare dashboard UI showing the Edge certificates issued for this website

And yes, that’s as simple as it sounds! When you add your domain in Cloudflare, Cloudflare handles all the work to get a cert running for Request 1.

✅ Tip

While we’re at it (call it step 1.5), we should configure Cloudflare to force all incoming traffic to HTTPS rather than HTTP. Again, this has become the standard for web traffic over the last decade and, luckily for us, is as simple as toggling a switch.

Head over to the SSL/TLS -> Edge Certificates page in the Cloudflare domain dashboard then scroll down until you see the Always Use HTTPS toggle. Just turn it on!

Screenshot of the Always Use HTTPS toggle switch in the Cloudflare Dashboard UI

So now we’re here:

Similar diagram as shown before with three parties and two requests between them, now showing the request between end-users’ browsers in the color green with a Cloudflare Edge cert making it secure, but with the request (2) between Cloudflare and Heroku still being red since we haven’t configured that side yet

And this is where most folks get tripped up, bail, and set their Cloudflare SSL settings to “Flexible” — not actually using HTTPS at all between Cloudflare and Heroku! This is bad! Don’t do this!

Screenshot of the Cloudflare Dashboard UI with the SSL setting to Origin set to “Flexible”!
‼️ BAD! Don’t do this!

Instead, follow these few steps.

First, head over to the “Origin Server” section of the SSL/TLS panel in the Cloudflare Dashboard for your domain. Then click “Create Certificate”.

Screenshot of the Cloudflare Dashboard UI’s Origin Server certificate creating screen

Just go with the defaults here, they’re all fine. And yes, a 15-year expiration is fine here! This is going to be a very specific certificate that your Heroku dyno will serve to Cloudflare (and Cloudflare only) so it doesn’t need to be cycled with any real frequency.

Screenshot of the Origin Certificate create form in the Cloudflare Dashboard UI

Once you click “Create” and the cert is setup by Cloudflare, you’ll get this screen with some special key strings. Don’t navigate away from this screen. You’ll need these values and can’t ever access the second one (Private Key) again once you close the page!

Screenshot of the certificate key and private key that are generated after creating

So, in a different browser tab, head over to your Heroku app’s Settings tab and scroll down to the SSL Certificates section. Hit “Add certificate”

Screenshot of the section of the Settings tab in a Heroku app where the SSL certificates are managed

The “Configure Certificate” overlay panel should pop up and give you the option to specify a name for the cert (we usually use “Cloudflare Origin Cert” to help us remember where it came from) as well as a link to switch the input from a file drag-and-drop to a plain text field for pasting.

Screenshot of the Heroku “Configure Certificate” panel with nothing in it yet

Finally, we just need to copy and paste the certificate information and private key that Cloudflare generated for us over into the Heroku panel.

Screenshot of the Cloudflare UI dashboard showing the certificate and private key being pasted (the plain text) over into the Heroku Configure Cert overlay panel

Hit “Next”, click the check-box for your application’s domain, and save that thing!

Now your Heroku SSL settings should look like this:

Screenshot of the Heroku SSL Certificates again, now with a certificate showing in the list

Which essentially means that when somebody requests “Judoscale.com” from Heroku’s servers, that’s the certificate that will be served! Now, remember that in the public DNS system, judoscale.com actually resolves to Cloudflare. But when Cloudflare requests our site from Heroku (Request 2), it requests the same domain from Heroku — judoscale.com. And thus, Heroku will serve the Cloudflare certificate back to Cloudflare! This is how Cloudflare validates that the origin is both correct and secure. That makes our diagram now look like this:

Final diagram of the three-party, two-request setup, but this time showing the request between Cloudflare and Heroku in green with a Secure check thanks to the origin cert installed on the Heroku side

But there’s one last step! Now that our Heroku Web Dyno can serve requests to Cloudflare securely, we need to configure Cloudflare to only allow that level of security — no less! Let’s hop into our Cloudflare Dashboard one last time, head to SSL/TLS -> Overview and set our configuration to “Full (Strict)”.

A screenshot of the Cloudflare SSL configuration panel now set to “Full”, ensuring SSL validation between all parties

And that’s it! Our app should now be serving fully secured requests on our domain and Cloudflare should be communicating securely to our origin server without issue!

👀 Note

One last quick aside here to clarify a few thoughts about the ‘orange cloud’ control. For any given DNS record in a Cloudflare site, that record can be “Proxied” (orange cloud on) or “DNS only” (orange cloud off). When the record is set to “DNS only” (gray), the entire Cloudflare platform (tooling, features, etc.) is unavailable for that record. When gray, Cloudflare is essentially a simple DNS provider and simply resolves the DNS record in a traditional manner. This can be helpful for certain setups or specific use-cases where you really do need your domain to resolve to the hosting IP address directly, but it side-steps the point of using Cloudflare in the first place.

In contrast, activating the orange cloud (“Proxied”) is what actually sets up the Cloudflare-as-middle-man architecture we just covered. When Proxied, Cloudflare routes all requests through their systems and activates all features and benefits of the CDN.

Screenshot of the Cloudflare dashboard showing the orange cloud lit up for a particular DNS record
Screenshot of the Cloudflare dashboard showing the cloud as Gray, not orange, with the text ‘DNS Only’ next to it
Reverse-proxy CDN activated No CDN, simple DNS resolver

Tl;dr:

For those of you that just want to scroll to the bottom, here’s the ultra-short version:

  1. Add your domain to Cloudflare, enable proxying (orange cloud)
  2. Generate an Origin Certificate for your app in Cloudflare’s dashboard
  3. Add that origin cert to the Heroku app
    1. Make sure the domain (same one) is already added to the Heroku app
  4. Set your Cloudflare settings to Force HTTPS (SSL/TLS->“Edge Certificates” page then scroll down) and set your encryption configuration to “Full (Strict)” (SSL/TLS->“Overview” page then “Configure”)
  5. Put on your sunglasses, you SSL master, you! 😎

We hope this clears some things up for everyone and gives some insight into how all this plumbing works! ✌️