have nginx proxy properly pass ssl/https to apache mod_wsgi url_scheme for django/pyramid/flask etc ... Aug
09
1
3

I've had problems getting mod_wsgi to properly set the url_scheme to https when running under SSL. This causes references to fully-qualified urls in statements like redirects and static file locations resolve incorrectly to http when I am viewing the ssl page through https.

I have an nginx web server hosting static files and proxy apache mod_wsgi for the python applications which includes any combination of django, pyramid, and flask frameworks, although this will also work for any mod_wsgi app running under the nginx/apache stack.

I've seen examples of how to set up the apache config, and other examples of how to set up the nginx config, but I have yet to see someone explain how to set up the two together nor has anyone explained what is actually going on.

I am not a server admin so it's a struggle for me to wrangle things like this. I am by no means an expert on this subject but I am documenting my process on what I have found to work in hopes that it'll help others save some pain and frustration that I went through.

Short Version

Add set_proxy_header to the nginx settings after the proxy_pass statement in the server location directive.

proxy_set_header X-Forwarded-Protocol $scheme;

Add the SetEnvIf statement to set the HTTPS environment variable in the apache settings to pass to the wsgi process.

SetEnvIf X-Forwarded-Protocol https HTTPS=1

Long Version

My nginx site config typically looks similar to:

upstream example_backend {
  server 127.0.0.1:8080;
}

server {
    listen 80;
    server_name example.com;
    rewrite ^ https://$server_name$request_uri? permanent;
}

server {
    listen 443;
    server_name example.com;

    ssl  on;
    ssl_certificate  /path/to/example_combined.crt;
    ssl_certificate_key  /path/to/example.key;

    ssl_session_timeout  5m;

    ssl_protocols  SSLv2 SSLv3 TLSv1;
    ssl_ciphers  ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP;
    ssl_prefer_server_ciphers   on;

    access_log  /var/log/nginx/example.access.log;
    error_log  /var/log/nginx/example.error.log;

    location / {
        # auth_basic "Restricted";
        # auth_basic_user_file /path/to/htpasswd;

        proxy_pass http://example_backend;
        include /etc/nginx/proxy.conf;

        # set a proxy header to pass tell apache if we're operating under ssl
        proxy_set_header X-Forwarded-Protocol $scheme;

        # you can use this header instead, or anyone you want, but coordinate it with the apache config
        # proxy_set_header X-Forwarded-HTTPS on;
    }

    location ~ ^/(media|static)/ {
        root   /path/to/html/root;
        #index  index.html index.htm;
    }
}

My /etc/nginx/proxy.conf file contains:

proxy_redirect              off;
proxy_set_header            Host $host;
proxy_set_header            X-Real-IP $remote_addr;
proxy_set_header            X-Forwarded-For $proxy_add_x_forwarded_for;
client_max_body_size        10m;
client_body_buffer_size     128k;
proxy_connect_timeout       90;
proxy_send_timeout          90;
proxy_read_timeout          90;
proxy_buffer_size           4k;
proxy_buffers               4 32k;
proxy_busy_buffers_size     64k;
proxy_temp_file_write_size  64k;

The key entry is the proxy header:

proxy_set_header X-Forwarded-Protocol $scheme;

This sets the X-Forwarded-Protocol header to http or https depending on the scheme. Since you're under SSL it should be set to https. An alternative is you can set a pre-defined header instead like:

proxy_set_header X-Forwarded-HTTPS on;

Choose one or the other, not both. You will check the header value in the apache config and set the HTTPS environment variable to pass to the mod_wsgi process which will set the wsgi.url_scheme variable for the wsgi app.

My apache config typically looks similar to:

WSGIPythonHome /path/to/virtualenv
WSGIPythonPath /path/to/virtualenv/lib/python2.7/site-packages

<VirtualHost *:8000>
    ServerAdmin user@example.com

    ServerName example.com
    # ServerAlias www.example.com

    #DocumentRoot /path/to/html

    WSGIDaemonProcess example user=example group=example processes=4 threads=4
    WSGIApplicationGroup %{GLOBAL}
    WSGIProcessGroup example
    WSGIPassAuthorization on
    WSGIScriptAlias / /path/to/production.wsgi

    # check the proxy header passed to us from nginx to set the HTTPS environment variable
    SetEnvIf X-Forwarded-Protocol https HTTPS=1

    # use this alternative if it was set in nginx
    # SetEnvIf X-Forwarded-HTTPS on HTTPS=1

    # use this if you do not want to check the headers with SetEnfIf (not recommended)
    # SetEnv HTTPS 1

    <Directory /path/to/wsgi/folder>
        Order deny,allow
        Allow from all
    </Directory>

    # Possible values include: debug, info, notice, warn, error, crit,
    # alert, emerg.
    LogLevel warn
    ErrorLog /var/log/apache2/example.error.log
    CustomLog /var/log/apache2/example.access.log combined
</VirtualHost>

You need to make sure the setenvif apache mod is enabled (on Ubuntu it is on by default). The key line here is making sure you're setting the HTTPS environment variable:

SetEnvIf X-Forwarded-Protocol https HTTPS=1

If you set X-Forwarded-HTTP on in the nginx as an alternative you should alter the conditional accordingly:

SetEnvIf X-Forwarded-HTTPS on HTTPS=1

Again,you should be using one or the other, not both.

If you want to force HTTPS onto mod-wsgi without checking for the proxy header for testing then you can manually set the variable:

SetEnv HTTPS 1

This is highly not recommended as it's a potential security hole.

Bookmark and Share
blog comments powered by Disqus