238

I have a signup page on a subdomain like: https://signup.example.com

It should only be accessible via HTTPS but I'm worried people might somehow stumble upon it via HTTP and get a 404.

My html/server block in nginx looks like this:

html {
  server {
    listen 443;
    server_name signup.example.com;

    ssl                        on;
    ssl_certificate        /path/to/my/cert;
    ssl_certificate_key  /path/to/my/key;

    ssl_session_timeout 30m;

    location / {
      root /path/to/my/rails/app/public;
      index index.html;
        passenger_enabled on;
    }
  }
}

What can I add so that people who go to http://signup.example.com get redirected to https://signup.example.com ? (FYI I know there are Rails plugins that can force SSL but was hoping to avoid that)

1

7 Answers 7

149

According to nginx pitfalls, it's slightly better to omit the unnecessary capture, using $request_uri instead. In that case, append a question mark to prevent nginx from doubling any query args.

server {
    listen      80;
    server_name signup.mysite.com;
    rewrite     ^   https://$server_name$request_uri? permanent;
}
7
  • 71
    Or, according to the site you linked, "BETTER": return 301 http://domain.com$request_uri;
    – nh2
    Aug 14, 2012 at 4:56
  • 13
    one comment. the $server_name$ picks up the first server_name variable. So be aware of this if you have non FQN names in your configuration Aug 15, 2012 at 15:48
  • 2
    @nh2 This is another case of the documentation being wrong since using return 301... causes a "too many redirects" error while the rewrite method actually works. Jul 2, 2015 at 12:06
  • 4
    That's now documented as "also BAD". @MikeBethany return 301 does work, unless (I guess) you're triggering it also for correct URLs, by listening on both ports (example of config. triggering the problem: take serverfault.com/a/474345/29689's first answer and omit the if). Aug 3, 2015 at 8:57
  • 1
    I wonder what has changed over the years and whether this other answer is better: serverfault.com/a/337893/119666
    – Ryan
    Jul 14, 2018 at 0:59
278

The best way as it described in the official how-to is by using the return directive:

server {
    listen      80;
    server_name signup.mysite.com;
    return 301 https://$server_name$request_uri;
}
7
  • 6
    shortest answer and worked perfectly in my case Sep 8, 2012 at 22:37
  • 1
    This is generally recommended because it returns a 301 Moved Permanently (your links have permanently moved) as well as re-writing
    – sgb
    Dec 17, 2014 at 17:17
  • 2
    This doesn't work as it causes a "too many redirects" error even if you have set proxy_set_header X-Forwarded-Proto https; Jul 2, 2015 at 12:03
  • 1
    @MikeBethany are you defining listen 443; in the same block?
    – Joe B
    Aug 12, 2015 at 18:06
  • 3
    This should be the accepted answer.
    – sjas
    Sep 13, 2016 at 10:08
134

This is the correct and most efficient way if you want to keep it all in one server block:

server {
    listen   80;
    listen   [::]:80;
    listen   443 default_server ssl;

    server_name www.example.com;

    ssl_certificate        /path/to/my/cert;
    ssl_certificate_key  /path/to/my/key;

    if ($scheme = http) {
        return 301 https://$server_name$request_uri;
    }
}

Everything else above, using "rewrite" or "if ssl_protocol" etc is slower and worse.

Here is the same, but even more efficient, by only running the rewrite on the http protocol it avoids having to check the $scheme variable on every request. But seriously, it's such a minor thing that you don't need to separate them.

server {
    listen   80;
    listen   [::]:80;

    server_name www.example.com;

    return 301 https://$server_name$request_uri;
}
server {
    listen   443 default_server ssl;

    server_name www.example.com;

    ssl_certificate        /path/to/my/cert;
    ssl_certificate_key  /path/to/my/key;
}
6
  • 9
    Great, some coward voted this answer down without saying why, even though this answer is correct. Maybe another one of those "if is evil" cultists. If you bother to read the Nginx documentation about If, you will know that IfIsNOTEvil, just CERTAIN uses of it within a location{} context, none of which we do here. My answer is absolutely the correct way of doing things!
    – DELETEDACC
    Feb 26, 2013 at 21:50
  • 2
    I didn't down vote this, but I would like to point out that default has been changed to 'default_server' in the most recent versions.
    – spuder
    Oct 22, 2013 at 3:32
  • 1
    The first solution cannot be the most efficient, if the second one is even more efficient. And you even described, why you shouldn't use an if there: "it avoids having to check the $scheme variable on every request". The point of not using ifs is not only about performance, but also about being declarative, and not imperative.
    – pepkin88
    Oct 24, 2016 at 11:12
  • 1
    +1 for if ($scheme = http) Nov 27, 2016 at 21:34
  • Should use $host here, as mentioned in the other answers. Jan 7, 2017 at 9:25
55

If you are using the new dual HTTP and HTTPS server definition, you can use the following:

server {
    listen   80;
    listen   [::]:80;
    listen   443 default ssl;

    server_name www.example.com;

    ssl_certificate        /path/to/my/cert;
    ssl_certificate_key  /path/to/my/key;

    if ($ssl_protocol = "") {
       rewrite ^   https://$server_name$request_uri? permanent;
    }
}

This appears to work for me and doesn't cause redirect loops.

Edit:

Replaced:

rewrite ^/(.*) https://$server_name/$1 permanent;

with Pratik's rewrite line.

6
  • 2
    @DavidPashley your solution worked like a charm for me. Thanks May 17, 2012 at 12:23
  • 1
    If you are using the new dual HTTP and HTTPS server definition then you should separate it.
    – VBart
    Feb 23, 2013 at 14:42
  • 2
    elegant and works perfect!
    – jacktrade
    Sep 17, 2014 at 17:01
  • 2
    This was the only solution that worked for me with my Laravel/Homestead Nginx configuration. Jan 5, 2015 at 17:04
  • 1
    Also the rewrite line ought to be return 301 https://$server_name$request_uri; as this is the preferred method. Apr 20, 2015 at 15:17
31

Yet another variant, that preserves the Host: request header and follows the "GOOD" example on nginx pitfalls:

server {
    listen   10.0.0.134:80 default_server;

    server_name  site1;
    server_name  site2;
    server_name  10.0.0.134;

    return 301 https://$host$request_uri;
}

Here are the results. Note that using $server_name instead of $host would always redirect to https://site1.

# curl -Is http://site1/ | grep Location
Location: https://site1/

# curl -Is http://site2/ | grep Location
Location: https://site2/


# curl -Is http://site1/foo/bar | grep Location
Location: https://site1/foo/bar

# curl -Is http://site1/foo/bar?baz=qux | grep Location
Location: https://site1/foo/bar?baz=qux
6
  • Note that using $server_name instead of $host would always redirect to https://site1 isn't that what $request_uri is for? Apr 27, 2014 at 9:49
  • 2
    $request_uri does not contain a host or domain name. In other words, it always starts with a "/" character.
    – Peter
    Apr 27, 2014 at 11:17
  • 2
    Best answer by far.
    – Ashesh
    Jul 3, 2015 at 14:42
  • 3
    I am not sure why this answer is so low in votes. It's the only one worth using.
    – zopieux
    Dec 8, 2015 at 18:02
  • 2
    Cant believe so many people use $server_name this is the correct way to do it
    – Greg Ennis
    Jun 29, 2016 at 2:14
2

I think this is the most simple solution. Forces both non-HTTPS and non-WWW traffic to HTTPS and www only.

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

EDIT - Apr 2018: Solution w/o IF's can be found in my post here: https://stackoverflow.com/a/36777526/6076984

4
  • 1
    Aren't IF conditions considered evil and inefficient in the nginx world?
    – PKHunter
    Feb 14, 2017 at 20:09
  • Yes they are, in general. But for this simple checks I would guess not. I do have a proper configuration file which involves more code writing though, but avoids IF's completely.
    – stamster
    Feb 22, 2017 at 19:29
  • Google advises to use 301 instead of 303. Source: support.google.com/webmasters/answer/6073543?hl=en
    – dylanh724
    Apr 17, 2018 at 6:49
  • @DylanHunt - I left 303 only for testing, take a note that 1st handler was set to 301, only 2nd I forgot to change :) Also, solution w/o IF's: stackoverflow.com/a/36777526/6076984
    – stamster
    Apr 20, 2018 at 18:27
1
server {
    listen x.x.x.x:80;

    server_name domain.tld;
    server_name www.domian.tld;
    server_name ipv4.domain.tld;

    rewrite     ^   https://$server_name$request_uri? permanent;
}

This works better i think. x.x.x.x refers to your server's IP. If you are working with Plesk 12, you can do that by changing the "nginx.conf" file in the directory "/var/www/vhosts/system/domain.tld/conf" for whichever domain you want. Do not forget to restart nginx service after you save the configuration.

1
  • rewrite ^ https://$host$request_uri? permanent; would be a better solution as you might have several server names on a vhost
    – user333008
    Jan 20, 2016 at 15:39

You must log in to answer this question.

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