July 10, 2021

HTTPS redirection in Apache HTTP Server, properly

HTTP is the standard text-based protocol for retrieving documents from web servers, but, like most net protocols, has the unfortunate issue of using a transport method that is insecure. The packets that correspond to an HTTP message may be observed by routers before they reach their destination, and the whole message may be reassembled and inspected by malicious devices. Thus plain HTTP is commonly wrapped in TLS, resulting in HTTPS wherein messages are encrypted and can be decrypted only by the actual endpoints.

HTTPS is not absolutely necessary, however. As a user, you don't need it when browsing static content, unless you care about anyone in the way not knowing what browser you use and what content you view, or you want to verify the identity of the server. HTTPS is good to have as there will always be users that need it, but in these cases, it shouldn't however be forced upon other users, as it does bring some inconveniences. You have to present a valid certificate, it has to be signed, it cannot be expired, and it is always possible the version of TLS you use becomes obsolete. In any of these cases, all web clients start warning the user or stop the website from being accessed at all.

This is where Upgrade-Insecure-Requests steps in. It is a header sent by all modern browsers, informing the server that the client wishes to use HTTPS whenever possible. It can be turned off however, and most other tools (command line-based or in programming languages) do not send it by default, so you can specify the protocol by yourself freely.

Without further ado, here is how you should use it:

<Macro HTTPS $WEBSITE_DOMAIN>
RewriteEngine on
RewriteCond "%{HTTP:Upgrade-Insecure-Requests}" "!^0*$"
RewriteRule "^/(.*)$" "https://$WEBSITE_DOMAIN/$1" [R=permanent,L,E=HTTPS_UPGRADE]
Header always set Vary "Upgrade-Insecure-Requests"
</Macro>
Use HTTPS mydomain.com

Let's describe the directives in detail now.

The snippet is presented as a configuration macro, meaning you can re-use it for any virtual host you specify. It should only be used for HTTP (:80) hosts, as it would create a redirect loop otherwise.

The rewrite condition checks for the presence of the Upgrade-Insecure-Requests header. The pattern is a bit more permissive than what W3C specifies, treating anything other than zero or many 0 characters as a signal to upgrade.

The actual rewrite rule is one you may commonly find in unconditional HTTPS rewrites. However, there is some uncertainty about the status code that should be used. 301 Moved Permanently is commonly used for HTTPS upgrades, but the W3C document uses 307 Temporary Redirect in the example instead. There are two factors that affect which status code to choose: whether the redirect is permanent or temporary, and whether non-GET requests must be repeated or not.

I don't really consider this redirect temporary, as the server is unlikely to remove it from configuration, and the client will always get the same response for the same request. However, a permanent redirect also carries an implicit instruction to rewrite all links that were used to navigate to the page, if the client has the possibility to do so. This one is a bit more ambiguous, as on one hand, HTTPS is good in general for the majority of users, but on the other hand, the point of this redirect was not to force HTTPS onto clients that don't want to use it. Nevertheless, downgrading to HTTP is always intentional, so any navigator can try HTTP first if it encounters an HTTPS link.

Next question is whether the method needs to be preserved. The original 3xx status codes don't mandate this, which is why some navigators might switch to GET upon receiving the response. In this case however, it would mean the client has already unintentionally sent (possibly confidential) POST data through HTTP for some reason, so it might be a good idea to break something (this should however only occur via improperly configured internal forms, or external forms; both are a cause for concerns).

And that is all that is needed. However, there is still one minor issue with caches, since caching the redirect is not desirable if Upgrade-Insecure-Requests is dropped. This is what the Vary header is for. In the case of HTTP, a potential cached response is not desirable if the value of the header changes, and so "Vary: Upgrade-Insecure-Requests" is set, also effectively telling the client that it understands that request header. (Originally, I had this header generate only when a redirect actually took place, hence the HTTPS_UPGRADE environment variable to indicate that, but that would mean a redirect would not be served if a client later added the header but had the original response cached.)

There is not much else to this macro. Use it whenever you can, but it is still a good idea to enforce HTTPS whenever authentication and authorization is critical to your website or its users.

No comments:

Post a Comment