548

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,252
  • 5
  • 22
  • 18

11 Answers11

792

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

I have been corrected by many SE users, so the credit goes to them, but more importantly, here is the correct code:

server {
       listen         80;
       server_name    my.domain.com;
       return         301 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" always; 

       [....]
}
sffc
  • 382
  • 1
  • 3
  • 11
Saif Bechan
  • 10,892
  • 10
  • 40
  • 63
  • 6
    You would have to do this on a domain by domain basis however - no? What if you wanted to apply it to every domain on your server? – JM4 Aug 15 '12 at 23:57
  • This seems all to work very well. But then, what will happen to the POST parameters? Body/Headers? Anybody got some inputs on this? – nembleton Aug 18 '12 at 10:46
  • @nembleton Nothing will happen to the parameters. They will just be passed trough to your application. – Saif Bechan Aug 18 '12 at 11:00
  • 31
    @JM4: if you use $host$ in the rewrite instead of server_name and add default_server to the listen directive it will work for every domain on your server. – Klaas van Schelven Jan 05 '13 at 20:06
  • As of version 0.7.14, the standard way to enable SSL is through the listen parameter. Therefore it should be `listen 443 ssl;` instead of having `ssl on;` – Hengjie Jan 08 '13 at 23:52
  • @nembleton That's not true. POST body will be stripped off. – Gajus Apr 14 '13 at 12:22
  • @Guy Are you sure nginx strips the POST body during a rewrite? – Alexandr Kurilin May 18 '13 at 01:02
  • I had also to add the line keepalive_requests 0; in order to work ok in safari and chrome. – jbaylina Oct 12 '14 at 20:42
  • @JM4 @Klass van Schleven: you could also change `listen 80;` to `listen 80 default;` and then eliminate the `server_name` directive. – Roshambo Jan 22 '15 at 19:30
  • I had to do something similar with a vhost with both ssl and non ssl, the following rule worked for me: `rewrite ^!https https://$server_name$request_uri? permanent` – Dan Bowling Jan 28 '15 at 20:08
  • 5
    It's important to mention that the 301 is stored in your local cache without expiry date. Not very useful when config changes – Trefex Apr 10 '15 at 13:58
  • 10
    @everyone Use a 307 redirect to preserve POST content. – Mahmoud Al-Qudsi May 14 '15 at 16:46
  • 1
    So $server_name didn't work for me, but $host did. Not sure if this is a mistake or something wrong with my deployment. I see some of the other examples below recommending $host though. – Eric Rini Oct 08 '15 at 14:00
  • 1
    @Trefex It's worse than that. Because this uses HSTS, your browser won't ever consult the HTTP server. – Andrea Dec 21 '15 at 18:25
  • 12
    Note that you must use `$host` instead of `$server_name` if you're using subdomains. – Catfish Jan 19 '16 at 14:27
  • 1
    Not working if you have a proxy like CloudFlare that handles https, while your server listens on port 80 – the_nuts Mar 25 '16 at 18:21
  • @Catfish I'm using a subdomain and `$server_name` worked fine? – Johhan Santana May 10 '16 at 13:59
  • It'll work, but $server_name will always redirect to the first in the list, not the host you've connected to. – Artem Russakovskii Jan 07 '17 at 09:29
  • 1
    @MahmoudAl-Qudsi: This is in no way confirmed by the HTTP spec - 307 is no different than 301 in that regard (see https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html section 10.3.8) – ChrisWue Mar 09 '17 at 20:48
  • @ChrisWue see 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." – Mahmoud Al-Qudsi Mar 16 '17 at 21:04
  • @Catfish +1, you saved my day – Tarun Gupta Aug 17 '17 at 06:44
  • Where is this file? – Philip Rego May 01 '20 at 21:46
  • The problem with "return 301 https://$server_name$request_uri;" is that $request_uri is never empty it contains at least a "/". so http ://example.com would redirect to https ://example.com/ (note the trailing slash). Services like Hardenize complain about this, is there a way to eliminate this slash? – windyjonas Nov 30 '20 at 13:08
287

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.

Michael Neale
  • 3,654
  • 5
  • 27
  • 26
  • 15
    I think it should be 80 - as this is listening for http and then telling the client to come back as https (443). – Michael Neale Dec 08 '11 at 22:30
  • 3
    This should be the top answer! – Nathan Stocks Aug 31 '12 at 22:52
  • 3
    This is the most taxing answer. – Case Nov 21 '12 at 07:36
  • 1
    this is the easiest one, but the least secure - this way you allow your server to redirect a user to any page, without checking if it's even allowed to be used on your server. If your server serves mydomain.co, malicious users could still use your server to redirect users to other domains like mydomain.co, such as google.com. – friedkiwi May 30 '13 at 15:32
  • 1
    Iscariot it is taxing - but only for port 80 traffic - if that helps ;) – Michael Neale Jun 03 '13 at 23:19
  • @cab0lt I don't follow - it sends a permanent redirect to the client so the browsers url will change (see http://serverfault.com/a/401632/3641 for a less taxing version) - if you want to check what $host$ you could via various means in your config, if that helps. – Michael Neale Jun 03 '13 at 23:21
  • @MichaelNeale: that's what I mean - combine a redirection with a wildcard always with a list of allowed locations – friedkiwi Jun 10 '13 at 07:36
  • got it, thanks. Have clarified things (hopefully!) - this Q&A keeps popping up over and over (which is good to see). – Michael Neale Jun 13 '13 at 01:42
  • 11
    @cab0lt there is no security issue here. Serving a redirect doesn't present a security risk. If there's access control requirements, those should be checked at the point where the browser requests the new URL. The browser won't gain access simply on the basis of the redirect, nor do they need the redirect to request the new URL. – mc0e Oct 16 '13 at 03:38
  • Does this answer imply settings for port 443 to be already present in the nginx configuration file? – Waseem Jun 03 '14 at 13:53
  • @Waseem yes, you should already have the other https blocks defined. This block should come above the others. – That Realtor Programmer Guy Jun 09 '14 at 01:12
  • `$host` >>> `$server_name` !!!! – sjas Sep 29 '16 at 14:08
  • For some reason this one works whereas the top solution doesn't – ScottishTapWater Apr 27 '17 at 16:25
129

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,441
  • 1
  • 10
  • 17
  • Nice you try to quote it, but 301 does not work on HTTPS. – Case Nov 21 '12 at 07:34
  • 5
    what does not work? the stated server section is for non-encrypted http (without s) traffic to be permanently redirected to encrypted server (that section that listens on 443 (https) is not listed) – kmindi Nov 21 '12 at 13:05
  • I checked this works great with https and everything - @kmindi I updated my answer with reference to yours - as I think it is the right way and this keeps popping up! Nice work. – Michael Neale Jun 04 '13 at 00:03
  • When using a domain (non-ip) request, does not work unless I change '[::]:80' to '80'. – Joseph Lust Nov 17 '13 at 19:58
  • that could be the expected behaviour: http://trac.nginx.org/nginx/ticket/345. I updated the answer to describe the listen option. – kmindi Nov 17 '13 at 20:35
  • Why would you use 301 or 308, when there is 307 ? This is actually very bad advice in a "let's try if it works" thread like this one here. Always use Temporary redirects until you're absolutely certain it all does what you want it to. Just leave it for a few days and check the logs for strange behavior. You can always change it to 308, or 301 later, no need to rush that change either. – Julius Feb 28 '19 at 08:43
  • Can similar behaviour be used for redirecting host names? I.e. redirect www hosts to non-www (full question here: https://serverfault.com/questions/1066676/nginx-how-do-i-force-all-server-domains-and-server-blocks-to-use-non-www-redir) – Matthew Spence Jun 14 '21 at 15:10
23

Within the server block you can also do the following:

# Force HTTPS connection. This rules is domain agnostic
if ($scheme != "https") {
    rewrite ^ https://$host$uri permanent;
}
Oriol
  • 361
  • 2
  • 3
19

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
# ...
}
Aleck Landgraf
  • 299
  • 2
  • 2
  • thank you! nginx would either return `301 https://*/` or cancel the request prematurely in the other answers here. `server_name _;` with `$host` was the answer that did the trick. +1 – zamnuts Mar 13 '13 at 00:53
  • 1
    This one is optimal! However, I recommend for some to replace `_` with the actual domain, e.g. `.domain.com` I had two servers, and nginx was accidentally directing one of my servers to the default server. – zzz Jul 01 '13 at 21:16
  • 1
    This is the only answer that worked for me, thanks! – Snowman Jul 21 '16 at 00:28
  • Thanks a lot buddy.. I have tried many of the solutions but didnt worked out. This solutions is awesome and it worked for me. server_name _; what is this meant for.. I didnt understand. Please explain me this. – Pavan Kumar Jan 11 '17 at 14:51
10

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 30x 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"; 

       [....]
}
Mahmoud Al-Qudsi
  • 509
  • 1
  • 6
  • 21
4

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
  • 180
  • 5
4

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
  • 141
  • 3
4

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.

  • Do you have any precedent about such "open proxy" because of code `return 301 https://$host$request_uri;` ? I like the idea to create one universal block to redirect any my HTTP website to the same but HTTPS. – Alexey Vazhnov Mar 30 '20 at 18:36
2

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
  • 143
  • 3
0
rewrite ^!https https://$host$request_uri permanent;
sysadmin1138
  • 131,083
  • 18
  • 173
  • 296
nntb2a
  • 37