81

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?

Devin
  • 933

11 Answers11

130

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>
}
kolbyjack
  • 8,287
  • 2
  • 39
  • 31
12

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

e18r
  • 221
  • 3
  • 6
9

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

emyller
  • 191
  • 1
  • 2
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;
        ...
    }
Scott Pack
  • 15,097
montss
  • 408
1

If you have many domains and you are looking for a more generic approach without loosing performance and without listing all domains all the time, check this.

How it works?

  1. listen to 80 and redirect all http to https - including http://www. which will go to https://www.
  2. listen to 433, but only on www server names and redirect to non-www using regular expression
  3. listen to 433 for each of your non-www server name - this is where all the traffic will end up

Is it fast?
Yes! Even though we use RegExp, it's only in the www-versions block which returns 301. So all normal traffic will be handled without any additional processing cost.

  # Redirect everything to HTTPS (including "www")
  server {
    listen 80 default_server;
    listen [::]:80 default_server;
    return 301 https://$host$request_uri;
  }

Redirect away from "www" versions:

server { listen [::]:443 ssl http2; listen 443 ssl http2; server_name www.example-1.com www.example-2.com www.example-3.com; # using generic "www" removal // https://stackoverflow.com/questions/11323735/nginx-remove-www-and-respond-to-both/45676731#45676731 if ( $host ~ ^www.(.+)$ ) { set $without_www $1; rewrite ^ $scheme://$without_www$uri permanent; } # SSL settings: ssl_certificate /etc/letsencrypt/live/example/fullchain.pem; # managed by Certbot ssl_certificate_key /etc/letsencrypt/live/example/privkey.pem; # managed by Certbot }

And finally one "server" block for each of your domains

server { listen [::]:443 ssl http2; listen 443 ssl http2; server_name example-1.com; # SSL settings, etc... } server { server_name example-2.com; # ... listen, ssl, etc... } server { server_name example-3.com; # ... etc }

PS: if you need help with SSL setup, checkout Mozilla SSL Configuration Generator:
https://ssl-config.mozilla.org/

Juraj
  • 191
  • 1
  • 5
0
server {
        listen 80;
        listen 443 ssl;
        server_name devly.co www.devly.co;

        ssl on;
        ssl_certificate /var/www/devly.co/cert/ssl-bundle.crt;
        ssl_certificate_key /var/www/devly.co/cert/devly_co.key;



        access_log   /var/log/nginx/devly.co.access.log rt_cache;
        error_log    /var/log/nginx/devly.co.error.log;
        root /var/www/devly.co/htdocs;
        index index.php index.htm index.html;

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


}
AlShar
  • 1
0

This, works for me

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

server { listen 80; listen 443 ssl; server_name example.com;

#location

}

p.s. with other solutions I got: ERR_TOO_MANY_REDIRECTS

0

First, redirect all http [80] requests to https permanently [301] from the first server block. Configure SSL in the second server block with the same domain and subdomain.

server {
    listen 80;
    listen [::]:80;
    server_name domain.com www.domain.com;
    return 301 https://$host$request_uri;
}

server { listen 443 ssl; listen [::]:443 ssl; root /var/www/html/app;

server_name domain.com www.domain.com;

ssl_certificate /etc/ssl/chain.crt;
ssl_certificate_key /etc/ssl/private.key;

}

Robot Boy
  • 101
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

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.

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