556

I want to rewrite all http requests on my web server to be https requests, I started with the following:

server {
    listen      80;

    location / {
      rewrite     ^(.*)   https://mysite.com$1 permanent;
    }
...


One Problem is that this strips away any subdomain information (e.g., node1.mysite.com/folder), how could I rewrite the above to reroute everything to https and maintain the sub-domain?

MikeN
  • 8,602

11 Answers11

805

Correct way in new versions of nginx

Turn out my first answer to this question was correct at certain time, but it turned into another pitfall - to stay up to date please check Taxing rewrite pitfalls

## Redirect all HTTP traffic to HTTPS
server {
  listen          [::]:80 default_server;
  listen               80 default_server;
  server_name     _;
  return          307 https://$host$request_uri;
}

server { listen 443 ssl http2 quic; server_name my.domain.com;

add Strict-Transport-Security to prevent man in the middle attacks

add_header Strict-Transport-Security "max-age=31536000" always;

... }

Saif Bechan
  • 11,060
292

NOTE: The best way to do this was provided by https://serverfault.com/a/401632/3641 - but is repeated here:

server {
    listen         80;
    return 301 https://$host$request_uri;
}

In the simplest case your host will be fixed to be your service you want to send them to - this will do a 301 redirect to the browser and the browser URL will update accordingly.

Below is the previous answer, which is inefficient due to regex, a simple 301 is great as shown by @kmindi

I have been using nginx 0.8.39 and above, and used the following:

 server {
       listen 80;
       rewrite ^(.*) https://$host$1 permanent;
 }

Sends a permanent redirect to the client.

134

I think the best and only way should be using a HTTP 301 Moved Permanently redirect like this:

server {
    listen         [::]:80;
    return 301 https://$host$request_uri;
}

The HTTP 301 Moved Permanently redirect is also the most efficient because there is no regex to be evaluated, according to already mentioned pitfails.


The new HTTP 308 Moved Permanently preserves the Request method and is supported by major browsers. For example, using 308 prevents browsers from changing the request method from POST to GET for the redirect request.


If you want to preserve the hostname and subdomain this is the way.

This does still work if you have no DNS, as I am also using it locally. I am requesting for example with http://192.168.0.100/index.php and will get redirected to exactly https://192.168.0.100/index.php.

I use listen [::]:80 on my host because i have bindv6only set to false, so it also binds to ipv4 socket. change it to listen 80 if you don't want IPv6 or want to bind elsewhere.

The solution from Saif Bechan uses the server_name which in my case is localhost but that is not reachable over a network.

The solution from Michael Neale is good, but according to the pitfails, there is a better solution with redirect 301 ;)

kmindi
  • 1,511
27

Within the server block you can also do the following:

# Force HTTPS connection. This rules is domain agnostic
if ($scheme != "https") {
    rewrite ^ https://$host$request_uri permanent;
}
Oriol
  • 411
20

The above didn't work for with with new subdomains being created all the time. e.g. AAA.example.com BBB.example.com for about 30 subdomains.

Finally got a config working with the following:

server {
  listen 80;
  server_name _;
  rewrite ^ https://$host$request_uri? permanent;
}
server {
  listen  443;
  server_name example.com;
  ssl on;
  ssl_certificate /etc/ssl/certs/myssl.crt;
  ssl_certificate_key /etc/ssl/private/myssl.key;
  ssl_prefer_server_ciphers       on;
# ...
# rest of config here
# ...
}
13

I posted a comment on the correct answer a long, long time ago with a very important correction, but I feel it is necessary to highlight this correction in its own answer. None of the previous answers are safe to use if at any point you had unsecure HTTP set up and expect user content, have forms, host an API, or have configured any website, tool, application, or utility to speak to your site.

The problem occurs when a POST request is made to your server. If the server response with a plain 301/302 redirect the POST content will be lost. What happens is that the browser/client will upgrade the request to SSL but downgrade the POST to a GET request. The POST parameters will be lost and incorrect request will be made to your server.

The solution is simple. You need to use a HTTP 1.1 307 redirect. This is detailed in RFC 7231 S6.4.7:

  Note: This status code is similar to 302 (Found), except that it
  does not allow changing the request method from POST to GET.  This
  specification defines no equivalent counterpart for 301 (Moved
  Permanently) ([RFC7238], however, defines the status code 308
  (Permanent Redirect) for this purpose).

The solution, adapted from the accepted solution, is to use 307 in your redirect code:

server {
       listen         80;
       server_name    my.domain.com;
       return         307 https://$server_name$request_uri;
}

server { listen 443 ssl; server_name my.domain.com; # add Strict-Transport-Security to prevent man in the middle attacks add_header Strict-Transport-Security "max-age=31536000";

   [....]

}

7

I am running ngnix behind an AWS ELB. The ELB is talking to ngnix over http. Since the ELB has no way to send out redirects to clients, I check for the X-Forwarded-Proto header and redirect:

if ($http_x_forwarded_proto != 'https') {
    return 301 "https://www.exampl.com";
}
MANCHUCK
  • 171
5

I managed to do it like this:

server {
listen 80;
listen 443 ssl;

server_name domain.tld www.domain.tld;

# global HTTP handler
if ($scheme = http) {
        return 301 https://www.domain.tld$request_uri;
}

# global non-WWW HTTPS handler
if ($http_host = domain.tld){
        return 303 https://www.domain.tld$request_uri;
}
}

https://stackoverflow.com/a/36777526/6076984

stamster
  • 210
5

If you return 301 https://$host$request_uri; as the default response on port 80, then your server may sooner or later get on a list of open proxies[1] and start being abused to send traffic elsewhere on the Internet. If your logs fill up with messages like this one, then you know it's happened to you:

42.232.104.114 - - [25/Mar/2018:04:50:49 +0000] "GET http://www.ioffer.com/i/new-fashion-fine-gold-bracelet-versaec-bracelet-641175733 HTTP/1.1" 301 185 "http://www.ioffer.com/" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0; Hotbar 4.1.8.0; RogueCleaner; Alexa Toolbar)"

The problem is that $host will echo back whatever the browser sends in the Host header or even the hostname from HTTP's opening line, like this one:

GET http://www.ioffer.com/i/new-fashion-fine-gold-bracelet-versaec-bracelet-641175733 HTTP/1.1

Because of that problem, some other answers here recommend using $server_name instead of $host. $server_name always evaluates to what you put in the server_name declaration. But if you have multiple subdomains there or use a wildcard, that won't work, because $server_name only uses the first entry after the server_name declaration, and more importantly will just echo back a wildcard (not expand it).

So how to support multiple domains while maintaining security? On my own systems I've dealt with this dilemma by first listing a default_server block that doesn't use $host, and then listing a wildcard block that does:

server {
  listen 80 default_server;
  server_name example.com;
  return 301 https://example.com$request_uri;
}
server {
  listen 80;
  server_name *.example.com;
  return 301 https://$host$request_uri;
}

(You could also list more than one domain in the second block.)

With that combination, unmatched domains will get redirected somewhere hardcoded (always example.com), and domains that match your own will go to the right place. Your server won't be useful as an open proxy, so you won't be attracting trouble.

If you are feeling ornery, I suppose you could also make the default_server block match none of your legitimate domains and serve something offensive. . . .

[1] Technically "proxy" is the wrong word, because your server isn't going out and fulfilling requests for the clients, just sending a redirect, but I'm not sure what the right word would be. I'm also not sure what the goal is, but it fills up your logs with noise and consumes your CPU and bandwidth, so you might as well put a stop to it.

4

Looks like nobody really got it 100% right. To have port 80 requests go to their 443 equivalents for an entire webserver, you need to use the listen directive, not the server_name directive to specify the catch-all name. See also https://nginx.org/en/docs/http/request_processing.html

server {
    listen 80 default;
    listen [::]:80 default;
      return 307 https://$host$request_uri;
}
  • $host catches subdomain names.
  • 307 and 308 include both POST and GET request URIs.
  • 307 is Temporary, change to the Permanent 308 after thorough testing:

And make sure you check what's already in /etc/nginx/conf.d/ because more often than not I had issues where the default.conf returned some existing vhost. My order of working with nginx issues is always starting by moving out the default file, putting it back commenting out line by line to see where it goes wrong.

Julius
  • 173
1
rewrite ^!https https://$host$request_uri permanent;
sysadmin1138
  • 135,853
nntb2a
  • 47