Apache: force HTTPS on when behind SSL-terminating reverse proxy

713 views Asked by At

I am using apache 2.4.57 behind an SSL-terminating reverse-proxy (Traefik in my case). Traefik takes https requests and proxies them to apache port 80 with the following HTTP headers set: X-Forwarded-Proto and X-Forwarded-For.

My problem is that I cannot get apache to recognize that requests are actually using the https scheme. This is problematic because I have a ton of RewriteRule configurations that result in 302 responses which contain an http scheme like this: location: http://myhostname.org/mypath. This sometimes results in MixedContent errors with my application.

Ideally, I'd like to add this to my VirtualHost config:

SetEnvIf X-Forwarded-Proto "https" HTTPS=on

But the HTTPS variable always remains off. In fact, even when I configure SetEnv HTTPS on the HTTPS variable still stays off.

Abbreviated config is something like this:

<VirtualHost *:80>
    SetEnvIf X-Forwarded-Proto "https" HTTPS=on
    RewriteEngine On

    # Debug: Show X-Forwarded-Proto header and HTTPS variable
    RewriteRule ^/debug$ /debug?xfp=%{HTTP:X-Forwarded-Proto}&https=%{HTTPS} [R,L]

    # Lots of other RewriteRules below...
</VirtualHost>

When I try a test with curl I get:

curl -v https://myhostname.org/debug 2>&1 |grep location
< location: http://myhostname.org/debug?xfp=https&https=off

Why doesn't this work? How do I force HTTPS on and make all my RewriteRules return a location with https?

UPDATE

I ended up using a modified version of the answer provided by @VonC. Note that SetEnvIf works to set a user environment variable, it apparently just can't be used to override the built-in apache variable HTTPS. This solution unfortunately required me to manually change all my RewriteRule stanzas.

SetEnvIf X-Forwarded-Proto https HTTP_SCHEME=https
SetEnvIf HTTP_SCHEME ^$ HTTP_SCHEME=http

# prepend scheme/host to substitution portion of each RewriteRule 
RewriteRule ^/foo$ %{ENV:HTTP_SCHEME}://%{HTTP_HOST}/bar [R,L]

In the future, I'm hoping that apache/httpd PR 191 provides an easier way to address this issue.

1

There are 1 answers

2
VonC On BEST ANSWER

The OP Noky mentions in the comments that SetEnvIf directive (in SetEnvIf X-Forwarded-Proto "https" HTTPS=on) did not set the built-in apache HTTPS variable described in the mod_ssl docs, it sets a user variable with the same name.

You could try mod_remoteip in Apache, but since, in your case, Traefik is already sending X-Forwarded-Proto, you can use instead a conditional RewriteCond and RewriteRule to manipulate the URL schema.

<VirtualHost *:80>
    RewriteEngine On
    
    # Set an environment variable if X-Forwarded-Proto is https
    RewriteCond %{HTTP:X-Forwarded-Proto} ^https$
    RewriteRule ^ - [E=HTTP_SCHEME:https]
    
    # Fallback to http if the above condition did not match
    RewriteCond %{ENV:HTTP_SCHEME} ^$
    RewriteRule ^ - [E=HTTP_SCHEME:http]
    
    # Your existing RewriteRules here, but modify to use the determined scheme
    RewriteRule ^/debug$ %{ENV:HTTP_SCHEME}://%{HTTP_HOST}/debug?xfp=%{HTTP:X-Forwarded-Proto}&https=%{ENV:HTTP_SCHEME} [R,L]
    
    # other RewriteRules
</VirtualHost>

RewriteCond is used to inspect X-Forwarded-Proto. If it is "https", we set an environment variable HTTP_SCHEME to "https". Then in your RewriteRule, we use this environment variable to construct the URLs.

That should yield URLs with the correct scheme in the Location header for 302 responses.

Note: Noky also points out to apache/httpd PR 191 "mod_remoteip is extended by multiple options handy for frontend proxies moving traffic to Apache backends", which would need to be applied.

Dec. 2023: apache/httpd PR 191 proposes httpd-2.4-remoteip-2.4.58-20231211.patch, which could be applied to Apache 2.4.58.

/**
 * This hook allows modules that virtualize SSL state (i.e. mod_remoteip)
 * based on the data from remote frontend to register their inquiry function
 * for checking if a remote frontend connection is using SSL for the request.
 * @param r The current request
 * @return OK iff the frontend connection is using SSL, DONE if not,
 *         DECLINED iff the state is undefined (let later modules decide).
 * @ingroup hooks
 */
AP_DECLARE_HOOK(int,remote_is_ssl,(request_rec *r))

/**
 * Return != 0 iff the frontend connection for the request is encrypted with SSL.
 * If there is no data from the hook (DECLINED), falls back to ap_ssl_conn_is_ssl()
 * and so corresponds to the local connection security state (we are the frontend).
 * @param r The current request
 */
AP_DECLARE(int) ap_remote_is_ssl(request_rec *r);