Today it is considered a de facto security standard for the web, that all HTTP requests automatically be redirected to HTTPS to prevent Man-in-the-middle attacks. Of course, this is a good standard to conform to, but there can also be benefits with providing an insecure HTTP version of your site as a fallback for users who, for one reason or another, are not able to use HTTPS. You can read more about that here.
Luckily, the web specifications have a way for the client to
tell the server whether or not it wants insecure connections
to be upgraded or not: The
Upgrade-Insecure-Requests
header.
By setting this header to 1
, the client can
indicate to the server, that it wants any insecure HTTP
requests to automatically be upgraded to HTTPS. This has
also been implemented in all major browsers since 2018. This
article will show you how to handle this header using nginx.
In its essence, the configuration is very simple: For every
request, we want to check if the scheme is http
and the Upgrade-Insecure-Requests
header is set
to 1
. If both of these conditions match, we
want to redirect to the same URL using HTTPS.
Sadly, you cannot have if-statements in nginx with multiple
conditions, so we will have to do a workaround. What we can
do is combine the current scheme with the value of the
header into a string and match that against a specific
value. This can be done using map
in nginx:
map "$scheme+$http_upgrade_insecure_requests" $upgrade { default 0; "http+1" 1; }
Here we combine the two values into a string with a
+
in the middle, and compare it against the
string "http+1"
. If it matches, the
$upgrade
variable is set to 1
,
otherwise 0
.
Now we just need to redirect based on this variable. This
must be done inside the server
block, while
the variable must be defined in the global scope. Make sure
also that the server is set to listen on both port 80 and
443.
if ($upgrade) { return 301 https://$host$request_uri; }
Putting it all together, we now have a functioning nginx configuration which redirects properly based on this header:
map "$scheme+$http_upgrade_insecure_requests" $upgrade { default 0; "http+1" 1; } server { listen 80; listen 443 ssl; server_name example.com; location / { # ... } if ($upgrade) { return 301 https://$host$request_uri; } }
If you are using Cloudflare, you will have to make some changes to this configuration. In this case, all requests your web server receives will be either HTTP or HTTPS depending on your Cloudflare configuration, regardless of the actual user's request.
Luckily, Cloudflare sends a header along with its request, telling
whether the user requested HTTP or HTTPS. This one is called
CF-Visitor
,
and the value is actually a JSON string that could tell more
information about the request, but currently it only
contains a single key (the scheme). This makes it very easy
to handle, as we can just continue using string matching.
map "$http_cf_visitor+$http_upgrade_insecure_requests" $upgrade { default 0; "{\"scheme\":\"http\"}+1" 1; }
Changing the map-statement from before to the above should make the configuration work with Cloudflare. Although this solution is not completely future-proof, Cloudflare has kept it like this for long enough, that I consider it pretty safe. If they change it, HTTPS redirection will be completely disabled.
You can use curl to make sure the configuration works
properly. The --head
option sends a
HEAD
request and makes curl output the response
code and headers.
curl --head http://example.com
This should give a response code of 200 and not redirect. Now you can add the header to the command:
curl --head http://example.com --header "Upgrade-Insecure-Requests: 1"
This should result in a 301 response with the
Location
header set to the same URL but with
HTTPS. As a last check, you can test the HTTPS URL and make
sure it also returns 200.