554

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?

2

11 Answers 11

800

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; 

       [....]
}
25
  • 7
    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, 2012 at 23:57
  • 32
    @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. Jan 5, 2013 at 20:06
  • 6
    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, 2015 at 13:58
  • 10
    @everyone Use a 307 redirect to preserve POST content. May 14, 2015 at 16:46
  • 12
    Note that you must use $host instead of $server_name if you're using subdomains.
    – Catfish
    Jan 19, 2016 at 14:27
291

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.

13
  • 15
    I think it should be 80 - as this is listening for http and then telling the client to come back as https (443). Dec 8, 2011 at 22:30
  • 4
    This should be the top answer! Aug 31, 2012 at 22:52
  • 3
    This is the most taxing answer.
    – Case
    Nov 21, 2012 at 7: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, 2013 at 15:32
  • 12
    @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, 2013 at 3:38
132

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 ;)

7
  • Nice you try to quote it, but 301 does not work on HTTPS.
    – Case
    Nov 21, 2012 at 7: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, 2012 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. Jun 4, 2013 at 0:03
  • When using a domain (non-ip) request, does not work unless I change '[::]:80' to '80'. Nov 17, 2013 at 19:58
  • that could be the expected behaviour: trac.nginx.org/nginx/ticket/345. I updated the answer to describe the listen option.
    – kmindi
    Nov 17, 2013 at 20:35
26

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;
}
6
  • 2
    This configuration caused my server to produce a redirect loop Oct 16, 2015 at 13:58
  • Maybe cause there's another redirect in place or https is not enable in your site/app
    – Oriol
    Oct 31, 2015 at 3:59
  • 1
    None of the others seemed to work except this one. Using nginx version: nginx/1.10.0 (Ubuntu)
    – ThatGuy343
    Jul 2, 2016 at 1:22
  • upvoted for https://$host$uri
    – user138870
    Nov 27, 2016 at 17:12
  • 4
    This is the way to go if you are behind a loadbalancer !
    – Antwan
    Mar 28, 2017 at 19:50
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
# ...
}
4
  • 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, 2013 at 0: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 1, 2013 at 21:16
  • 1
    This is the only answer that worked for me, thanks!
    – Snowman
    Jul 21, 2016 at 0: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. Jan 11, 2017 at 14:51
12

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

       [....]
}
1
5

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";
}
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

1
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.

1
  • 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. Mar 30, 2020 at 18:36
3

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.

0
rewrite ^!https https://$host$request_uri permanent;

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .