236

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)

Frederik
  • 3,293
  • 3
  • 30
  • 46
Callmeed
  • 2,705
  • 4
  • 18
  • 15
  • 4
    Possible duplicate of [In Nginx, how can I rewrite all http requests to https while maintaining sub-domain?](http://serverfault.com/questions/67316/in-nginx-how-can-i-rewrite-all-http-requests-to-https-while-maintaining-sub-dom) – Nasreddine Mar 28 '16 at 19:01

7 Answers7

277

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;
}
030
  • 5,731
  • 12
  • 61
  • 107
VBart
  • 8,159
  • 3
  • 24
  • 25
  • 6
    shortest answer and worked perfectly in my case – mateusz.fiolka Sep 08 '12 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 '14 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;` – Mike Bethany Jul 02 '15 at 12:03
  • 1
    @MikeBethany are you defining `listen 443;` in the same block? – Joe B Aug 12 '15 at 18:06
  • 3
    This should be the accepted answer. – sjas Sep 13 '16 at 10:08
  • If you use CloudFlare, turn forced HTTPS under the Crypto tab. – Mārtiņš Briedis Jan 13 '19 at 19:30
  • 1
    Administrators that are commit to run HTTPS only servers should also consider enable HTST header so that modern clients knows that all future requests targeted at that domain must be performed over HTTPS. In nginx, this can be done by adding the line `add_header Strict-Transport-Security "max-age=15768000" always;` to the corresponding HTTPS block. – jwatkins Jan 16 '19 at 20:06
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;
}
techraf
  • 4,163
  • 8
  • 27
  • 44
Pratik Amin
  • 3,293
  • 3
  • 20
  • 19
  • 71
    Or, according to the site you linked, *"BETTER"*: `return 301 http://domain.com$request_uri;` – nh2 Aug 14 '12 at 04: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 – engineerDave Aug 15 '12 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. – Mike Bethany Jul 02 '15 at 12:06
  • 3
    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 http://serverfault.com/a/474345/29689's first answer and omit the if). – Blaisorblade Aug 03 '15 at 08:57
  • @Blaisorblade Thanks, that's a much better way to do it. – Mike Bethany Aug 03 '15 at 15:42
  • It's probably wise to `listen [::]:80;` in addition so as to bind/redirect on IPv6 also. – Jason R. Coombs Apr 06 '18 at 13:16
  • 1
    I wonder what has changed over the years and whether this other answer is better: https://serverfault.com/a/337893/119666 – Ryan Jul 14 '18 at 00:59
133

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;
}
DELETEDACC
  • 1,517
  • 1
  • 10
  • 2
  • 8
    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 '13 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 '13 at 03: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 '16 at 11:12
  • 1
    +1 for if ($scheme = http) – Fernando Kosh Nov 27 '16 at 21:34
  • Should use $host here, as mentioned in the other answers. – Artem Russakovskii Jan 07 '17 at 09:25
  • `nginx: [emerg] unknown directive "if($scheme" in /etc/nginx/sites-enabled/mysite.com:16` – Zee Jun 13 '19 at 12:16
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.

David Pashley
  • 23,151
  • 2
  • 41
  • 71
30

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
Michael Hampton
  • 237,123
  • 42
  • 477
  • 940
Peter
  • 556
  • 4
  • 5
  • `Note that using $server_name instead of $host would always redirect to https://site1` isn't that what `$request_uri` is for? – Jürgen Paul Apr 27 '14 at 09:49
  • 2
    `$request_uri` does not contain a host or domain name. In other words, it always starts with a "/" character. – Peter Apr 27 '14 at 11:17
  • 2
    Best answer by far. – Ashesh Jul 03 '15 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 08 '15 at 18:02
  • 2
    Cant believe so many people use $server_name this is the correct way to do it – Greg Ennis Jun 29 '16 at 02:14
  • Both $host and $server_name are correct, in different environments. $host reuses the hostname used in the client URL; $server_name forces a redirect to the correct, canonical hostname of the site. For most uses I prefer server_name. – Harald Oct 04 '16 at 18:10
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.

Caner SAYGIN
  • 111
  • 1
  • `rewrite ^ https://$host$request_uri? permanent; ` would be a better solution as you might have several server names on a vhost –  Jan 20 '16 at 15:39
0

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

stamster
  • 180
  • 5
  • 1
    Aren't IF conditions considered evil and inefficient in the nginx world? – PKHunter Feb 14 '17 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 '17 at 19:29
  • Google advises to use 301 instead of 303. Source: https://support.google.com/webmasters/answer/6073543?hl=en – dylanh724 Apr 17 '18 at 06: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: https://stackoverflow.com/a/36777526/6076984 – stamster Apr 20 '18 at 18:27