How do I redirect HTTPS to HTTP on NGINX?

location / {
    if ($scheme = https) {
        rewrite ^(.*)? http://$http_host$1 permanent;
    }
}

Why is something like that useful? At first look I wasn't sure if it could be done. But it presented an interesting question.

You might try putting a redirect statement in your config file and restarting your server. Two possibilities might happen:

  1. The server will issue the redirect - what you seem to want.
  2. The server will first do the https exchange, and THEN issue the redirect, in which case, what's the point?

Will add more if I come up with something more concrete.

UPDATE: (couple of hours later) You could try this. You need to put this in your nginx.conf file -

server {
       listen 443;
       server_name _ *;
       rewrite ^(.*) http://$host$1 permanent;
 }

Sends a permanent redirect to the client. I am assuming you are using port 443 (default) for https.

server {
    listen      80;
    server_name _ *;
    ...
}

Add this so that your normal http requests on port 80 are undisturbed.

UPDATE: 18th Dec 2016 - server_name _ should be used instead of server_name _ * in nginx versions > 0.6.25 (thanks to @Luca Steeb)


rewrite and if should be avoided with Nginx. The famous line is, "Nginx is not Apache": in other words, Nginx has better ways to handle URLs than rewriting. return is still technically part of the rewrite module, but it doesn't carry the overhead of rewrite, and isn't as caveat-ridden as if.

Nginx has an entire page on why if is "evil". It also provides a constructive page explaining why rewrite and if are bad, and how you can work around it. Here's what the page has to say regarding rewrite and if:

This is a wrong, cumbersome, and ineffective way.

You can solve this problem properly using return:

server {
    listen 443 ssl;

    # You will need a wildcard certificate if you want to specify multiple
    # hostnames here.
    server_name domain.example www.domain.example;

    # If you have a certificate that is shared among several servers, you
    # can move these outside the `server` block.
    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/cert.key;

    # 301          indicates a permanent redirect.  If your redirect is
    #              temporary, you can change it to 302 or omit the number
    #              altogether.
    # $http_host   is the hostname and, if applicable, port--unlike $host,
    #              which will break on non-standard ports
    # $request_uri is the raw URI requested by the client, including any
    #              querystring
    return 301 http://$http_host$request_uri;
}

If you expect a lot of bots that don't send a Host header, you can use $host instead of $http_host as long as you stick to ports 80 and 443. Otherwise, you'll need to dynamically populate an $http_host substitute. This code is efficient and safe as long as it appears in the root of server (rather than in a location block), despite using if. However, you'd need to be using a default server for this to be applicable, which should be avoided with https.

set $request_host $server_name:$server_port;
if ($http_host) {
    set $request_host $http_host;
}

If you want to enforce SSL/TLS for specific paths, but forbid it otherwise:

server {
    listen 443 ssl;
    server_name domain.example;

    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/cert.key;

    location / {
        return 301 http://$host$request_uri;
    }

    location /secure/ {
        try_files $uri =404;
    }
}

server {
    listen 80;
    server_name domain.example;

    location / {
        try_files $uri =404;
    }

    location /secure/ {
        return 301 https://$http_host$request_uri;
    }
}

If your server isn't in direct communication with the client--for example, if you're using CloudFlare--things get a bit more complicated. You'll need to ensure that any server in direct communication with the client adds an appropriate X-Forwarded-Proto header to the request.

Using this is a messy proposition; for a full explanation, see IfIsEvil. In order for this to be useful, the if block cannot be inside a location block, for a variety of complex reasons. This forces the use of rewrite for URI testing. In short, if you have to use this on a production server... don't. Think of it this way: if you've outgrown Apache, you've outgrown this solution.

/secure, /secure/, and anything in /secure/ will enforce https, while all other URIs will enforce http. The (?! ) PCRE construct is a negative lookahead assertion. (?: ) is a non-capturing group.

server {
    # If you're using https between servers, you'll need to modify the listen
    # block and ensure that proper ssl_* statements are either present or
    # inherited.
    listen 80;
    server_name domain.example;

    if ($http_x_forwarded_proto = https) {
        rewrite ^(?!/secure)/ http://$http_host$request_uri? permanent;
    }
    if ($http_x_forwarded_proto != https) {
        rewrite ^/secure(?:/|$) https://$http_host$request_uri? permanent;
    }
}