53

I want to create a rule in nginx that does two things:

  1. Removes the "www." from the request URI
  2. Redirects to "https" if the request URI is "http"

There are plenty of examples of how to do each of those things individually, but I can't figure out a solution that does both correctly (i.e. doesn't create a redirect loop and handles all cases properly).

It needs to handle all of these cases:

1. http://www.example.com/path
2. https://www.example.com/path
3. http://example.com/path
4. https://example.com/path

These should all end up at https://example.com/path (#4) without looping. Any ideas?

  • I just redirected www.mydomain.com to mydomain.com at the DNS level and added a 301 for non-https to https in nginx. Seems like that should be fine ¯\_(ツ)_/¯ – jonathanbell May 26 '17 at 23:02
83

The best way to accomplish this is using three server blocks: one to redirect http to https, one to redirect the https www-name to no-www, and one to actually handle requests. The reason for using extra server blocks instead of ifs is that server selection is performed using a hash table, and is very fast. Using a server-level if means the if is run for every request, which is wasteful. Also, capturing the requested uri in the rewrite is wasteful, as nginx already has this information in the $uri and $request_uri variables (without and with query string, respectively).

server {
    server_name www.example.com example.com;
    return 301 https://example.com$request_uri;
}

server {
    listen 443 ssl;
    ssl_certificate /path/to/server.cert;
    ssl_certificate_key /path/to/server.key;
    server_name www.example.com;
    return 301 https://example.com$request_uri;
}

server {
    listen 443 ssl;
    ssl_certificate /path/to/server.cert;
    ssl_certificate_key /path/to/server.key;
    server_name example.com;

    <locations for processing requests>
}
  • 2
    Is the middle block necessary? Isn't the first block already rewriting from www to non-www? – pbreitenbach May 16 '11 at 20:39
  • 3
    The first block only handles http. The middle block is necessary to redirect https requests from https:// www.example.com/ to https:// example.com/. (Sorry for the extra spaces, I can't make it show the https otherwise) – kolbyjack May 17 '11 at 1:03
  • 1
    just a minor formatting note - if you want to avoid making a link, you can put comment text inside back-quotes ` , the one under tilde. It would show up like: https://example.com/ – Cyclops Jun 9 '11 at 21:13
  • 8
    the second block also needs cert info. – ricka May 6 '16 at 1:20
  • 2
    Trying this answer, I ran into another problem. Thought I could 301 redirect from www.sub.example.com to sub.example.com and then only obtain an SSL certificate for sub.example.com Now I know that ssl cert check happens before the 301 redirect, so it can not work. More explanation here: serverfault.com/a/358625/144811 – Gruzzles Nov 24 '16 at 18:08
8

This works for me:

server {
    listen              80;
    server_name         www.yourdomain.com yourdomain.com;
    return              301 https://yourdomain.com$request_uri;
}

server {
    listen              443 ssl;
    server_name         www.yourdomain.com;
    ssl_certificate     /path/to/certificate.crt;
    ssl_certificate_key /path/to/private/key.pem;
    ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;
    return              301 https://yourdomain.com$request_uri;
}

server {
    listen              443 ssl;
    server_name         yourdomain.com;
    ssl_certificate     /path/to/certificate.crt;
    ssl_certificate_key /path/to/private/key.pem;
    ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;

    # do the proper handling of the request
}

Keep in mind that both yourdomain.com and www.yourdomain.com must be in your SSL certificate. This is possible with a wildcard certificate or with a Server Alternate Name as explained here. Check https://www.startssl.com for nice and free certificates that do this. (Edith: beginning with Chrome version 56, startssl certificates will not be trusted anymore. Try https://letsencrypt.org/ instead.)

  • This one actually works, but I thought it could be done in more clear way without a lot of duplicate config lines. – zloynemec Sep 18 '17 at 8:48
  • @zloynemec You could put the SSL stuff in a separate .conf file and use the include rule to add it to both SSL server blocks. – Igettäjä Apr 2 '18 at 9:03
  • Also if you are using cloudflare you need to pay the $10/mo cert to be able to redirect and proxy the 2 subdomains ( www + something ). Let me know if there is a workaround. – Freedo Sep 24 '18 at 4:17
7

After spending so much time with hundreds of similar cases, I've come up with the following snippet. It's short and can be easily tweaked to fit anything.

server {
    listen 80;
    listen 443 ssl;
    server_name example.com www.example.com;
    ssl_certificate /path/to/my/certs/example.com/fullchain.pem;
    ssl_certificate_key /path/to/my/certs/example.com/privkey.pem;

    # Redirect to the correct place, if needed
    set $https_redirect 0;
    if ($server_port = 80) { set $https_redirect 1; }
    if ($host ~ '^www\.') { set $https_redirect 1; }
    if ($https_redirect = 1) {
        return 301 https://example.com$request_uri;
    }

    location / {
    # ...
}

Oh but if is evil!

Yes it can be. But it exists for a reason, and should do no harm to those who know how to use it properly. ;)

  • I like this, but do you have any data on the performance hit? Thank you! – Freedo Sep 24 '18 at 4:17
  • 1
    Honestly I never benchmarked that, but I believe there would be hardly an impact compared to separate rules since the effect is pretty much the same. – emyller Sep 24 '18 at 20:38
  • benchmark on redirection? it's not realy pertinent no? (true question, not a troll ^^) – Matrix Nov 23 '18 at 4:42
3

I prefer to return with a response code so the browser knows you are redirecting it to another URL.

server {
    listen   80;
    server_name  www.example.com;

    return 301 https://example.com$request_uri;
}

then another server configurations block for the https

server {
        listen   443 ssl;
        server_name  example.com;
        ...
    }
0

how about creating a server block for this purpose:

server{
    listen 80;
    server_name www.example.net example.net;
    rewrite ^(.*) https://example.net$1 permanent;
}

then restarting nginx

  • I get a "conflicting server name" error when restarting. Also, that command won't listen on port 443 for SSL and I need to worry about redirecting https://www.example.com to https://example.com as well. – Devin Apr 11 '11 at 16:56
0

I think this should work.

On your plain HTTP server definition something like anthonysomerset suggested, that is:

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

Then on your SSL server definition:

if ($host ~ /^www\./) {
  rewrite ^(.*) https://example.net$1 permanent;
}

This way the redirect should only happen once per request no matter which URL the user goes to originally.

  • That worked, thanks. I had to change your conditional to if ($host = 'www.example.com') { since your regex wasn't working for me, though. No idea why, as it looks correct. – Devin Apr 11 '11 at 18:20
  • Do note that if is evil and it's generally better to use a declarative way. – Blaise Mar 21 '15 at 6:35
0

Here's the full example that ended up working for me. The problem was that I didn't have the ssl details (ssl_certificate, etc.) in the www redirect block. Remember to check your logs (sudo tail -f /var/log/nginx/error.log)!

# HTTP — redirect all traffic to HTTPS
server {
    listen 80;
    listen [::]:80 default_server ipv6only=on;
    return 301 https://$host$request_uri;
}

# HTTPS — redirects www to non-www
server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name www.example.com;

    # Use the Let's Encrypt certificates
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Include the SSL configuration from cipherli.st
    include snippets/ssl-params.conf;
    return 301 https://example.com$request_uri;
}

# HTTPS — proxy all requests to the app (port 3001)
server {
    # Enable HTTP/2
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com sub.example.com;

    # Use the Let's Encrypt certificates
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Include the SSL configuration from cipherli.st
    include snippets/ssl-params.conf;

    # For LetsEncrypt:
    location ~ /.well-known {
        root /var/www/html;
        allow all;
    }

    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-NginX-Proxy true;
        proxy_pass http://localhost:3001;
        proxy_ssl_session_reuse off;
        proxy_set_header Host $http_host;
        proxy_cache_bypass $http_upgrade;
        proxy_redirect off;
    }
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.