Generating https urls in Django using CloudFront

TL;DR: Configure CloudFront to set the Cloudfront-Forwarded-Proto in order to allow a Django application to know the client’s request protocol.

Recently, while developing an API that makes use of Django Rest Framework and is delivered using CloudFront, we noticed the absolute URLs it generated to be HTTP, whereas the CloudFront distribution is HTTPS only. Not really surprising when thinking it through, as in our case Cloudfront did TLS termination and traffic between upstream components was HTTP (Yes, we’ll look into that, HTTPS being preferred).

URLs in pages (or JSON data) should match the protocol of the request. Typically you want the protocol to be determined based on facts (so the protocol the client request has) instead of configuration (the protocol you assume the client request to have). Less configuration. Easy for development setups that might be HTTP-only. No assumptions.

X-Forwarded-Proto

Let’s look a bit into how reverse proxies forward the client’s request protocol.

It’s quite common to configure reverse proxies to forward the request protocol by means of adding a X-Forwarded-Proto header to the upstream request.

For nginx it looks like:

proxy_set_header X-Forwarded-Proto $scheme;

For HAProxy it looks like:

http-request add-header X-Forwarded-Proto https if { ssl_fc }

And Amazon ELBs support it as well.

Note that these configurations set the X-Forwarded-Proto header but do not propagate it.

The setup of an application using Cloudfront typically looks like this:

             
 Application -- ELB -- Cloudfront -- Client
(uwsgi/nginx)          

Now this leaves for a lot of variations in what protocols are used between layers:

  • For custom origins, CloudFront can be configured to match the client protocol for backend requests, or use only HTTP or HTTPS.
  • Likewise, for ELBs listeners can be configured for HTTP and HTTPS, mirroring the client request, or always map to either HTTP or HTTPS on the instances (the application).
  • Any additional or alternate reverse proxies offer similar flexibility, as briefly illustrated in the above configuration examples.

The main take-away is that, to have reliable info in your application about the actual client’s request protocol, you’d either have to:

  • Rigourously match the downstream request protocol accross all layers.
  • Use TCP mode wherever possible.
  • Forward incoming X-Forwarded-Proto headers. But that’s not the best of practices, as that information shouldn’t be provided by client but be based on the actual request protocol.
  • Determine the client request protocol at the layer nearest to the client, and propagate that in a way that isn’t overwritten by upstream layers.

That last option is exactly what is offered by CloudFront’s Cloudfront-Forwarded-Proto header.

Cloudfront-Forwarded-Proto

By default CloudFront doesn’t set the Cloudfront-Forwarded-Proto header. This header can be added to the ‘Whitelist headers’ when configuring behaviours.

AWS CloudFront Behaviour Configuration: Whitelist Headers

Note the setting ‘Cache Based on Selected Request Headers’. This has the options None (improves caching), Whitelist and All. So, adding the the Cloudfront-Forwarded-Proto header to the whitelist not only causes the client’s request protocol to be available to the application, it also configures CloudFront to cache based on client request protocol.

Typically this is a good thing as it:

  • Allows links on a page to be generated matching the request protocol.
  • Prevents redirect loops in case you redirect clients from HTTP to HTTPS from your application server (note that CloudFront behaviours can be configured to do this as well).

Django configuration

Having set up CloudFront as described, configuring a Django application accordingly is trivial by adding to settings:

SECURE_PROXY_SSL_HEADER = ('HTTP_CLOUDFRONT_FORWARDED_PROTO', 'https')

Wrapping it up

This articles shows how to have a Django application obey the client’s request protocol when using CloudFront.

Other CDNs will likely offer similar configuration options (e.g. Fastly allows setting request headers when using req.proto as source).

Added advantage is that by doing so, large part of the architecture can be changed without requiring any configuration change. Examples would be swapping out ELBs with ALBs or moving the application to Kubernetes.