2026年11月12日

2026年11月12日

Nginx HTTPS Redirect Best Practices: One Hop, www, Unknown Hosts, HSTS

This article is a localized edition of the Japanese original. Read the Japanese original

The basic way to redirect http:// to https:// in Nginx is to put return 301 https://$host$request_uri; in the port 80 server block (see "Nginx HTTP to HTTPS redirect settings" for the steps).

The basic form does redirect, but on a site you run in production, tightening the following points reduces the number of redirects and keeps the server from answering for unexpected host names. The configuration in this article was tested with curl on Nginx 1.24 and 1.30 (as of November 2026).

The full recommended configuration

This example makes www.example.com the canonical URL. If you want the non-www name to be canonical, swap www.example.com and example.com.

# 1. Close HTTP connections for unknown host names without a response
server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;
    return 444;
}

# 2. Reject TLS connections for unknown host names over HTTPS (1.19.4 and later)
server {
    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;
    server_name _;
    ssl_reject_handshake on;
}

# 3. HTTP: redirect to the canonical URL in one hop
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    # Let's Encrypt validation files (only if you need it; see below)
    location /.well-known/acme-challenge/ {
        root /var/www/letsencrypt;
    }

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

# 4. HTTPS without www: redirect to www
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

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

# 5. HTTPS canonical URL: serve the site
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name www.example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    root /var/www/example.com;
    # existing settings follow
}

Use a certificate that covers both example.com and www.example.com (with Certbot, one obtained with -d example.com -d www.example.com). The reasons for each part are explained below.

Redirect in a single hop

When both www and HTTP/HTTPS need correcting, the basic form can redirect http://example.com/ twice:

  1. http://example.com/ → https://example.com/ (Nginx's HTTPS redirect)
  2. https://example.com/ → https://www.example.com/ (www normalization by WordPress or similar)

Block 3 in the configuration above sends http://example.com/ straight to the final https://www.example.com/. With fewer redirects, visitors wait less before the page appears.

For WordPress, set "WordPress Address (URL)" and "Site Address (URL)" under Settings → General (設定 → 一般 in the Japanese UI) to the same canonical URL as in Nginx (https://www.example.com in this example). If they differ, Nginx and WordPress redirect in opposite directions and loop.

For more on normalizing www, see also "Redirecting to unify www and non-www in Nginx".

Redirect to a fixed host name, not $host

$host is the value of the request's Host header. If the basic server block is the first one (the default server) on that port, it accepts requests for any host name and redirects them using that host name. In fact, sending an HTTP request with Host: evil.test to the basic configuration returned Location: https://evil.test/.

If your server answers for host names unrelated to your site, then when someone points another domain at your server, it behaves as that domain's site. Prevent this in two ways:

  • Make the redirect target a fixed host name, such as https://www.example.com$request_uri
  • Create a separate default_server server block that rejects requests that match no server_name

return 444 is an Nginx-specific code that closes the connection without sending a response. On the HTTPS side, ssl_reject_handshake on rejects TLS connections for names that match no server_name (Nginx 1.19.4 and later. It is not available in earlier versions; in that case, leave out server block 2 and keep only your site's server blocks).

In the Ubuntu and Debian packages, /etc/nginx/sites-enabled/default also contains listen 80 default_server;. Only one default_server is allowed per address and port, and nginx -t fails with "a duplicate default server", so either remove that link (sudo rm /etc/nginx/sites-enabled/default; the original file remains in sites-available) or put the contents of block 1 there.

With this configuration, the server also stops answering requests made directly to its IP address. If you need the site to load by IP address, create a separate server block for that purpose.

Use separate server blocks for ports 80 and 443

You can also listen on both 80 and 443 in one server block and branch with if ($scheme = http), but splitting them, with port 80 only redirecting and port 443 only serving, keeps the configuration easier to read and makes redirect loops less likely. The configuration above follows this approach.

Choosing between 301 and 308

Code Meaning Request after the redirect
301 Moved permanently The browser may change POST to GET
308 Moved permanently POST stays POST, and the body is sent again

For displaying pages, 301 is enough. If old forms or API calls might POST to http://, use 308 so the submitted data is not lost.

location / {
    return 308 https://www.example.com$request_uri;
}

Google Search Central's documentation explains that both 301 and 308 are treated as permanent redirects and signal that the target is the canonical URL. Nginx treats 308 as a redirect from version 1.13.0.

Let’s Encrypt renewals and redirects

Let's Encrypt's HTTP-01 validation requests /.well-known/acme-challenge/ over http://. According to Let's Encrypt's official documentation, this validation follows up to 10 redirects and does not validate the certificate when redirected to https://. So if the validation file is served from the same location on the HTTPS side, renewals work even with everything redirected.

If your setup cannot serve the validation file on the HTTPS side (for example, the HTTPS side is a reverse proxy to another application), do as block 3 above does: serve the files from /.well-known/acme-challenge/ in the port 80 server block without redirecting them. In that case, use Certbot's webroot method and specify that directory.

sudo certbot certonly --webroot -w /var/www/letsencrypt -d example.com -d www.example.com

Add HSTS only after confirming everything works

HSTS (HTTP Strict Transport Security) is a header that tells the browser "connect to this site only over HTTPS from now on". A browser that has stored it connects over HTTPS from the start even for http:// links, so no initial HTTP request is left.

Add it to the HTTPS server block for the canonical URL (block 5 above).

add_header Strict-Transport-Security "max-age=31536000" always;
  • With always, the header is also added to error responses (without it, it is added only to some responses such as 200 and 301)
  • Start with a short value such as max-age=300, and increase it after confirming there are no problems. Browsers enforce HTTPS for the max-age period, so even if you want to stop using HTTPS, you cannot go back during that time
  • Adding includeSubDomains covers every subdomain. Do not add it if any subdomain does not support HTTPS
  • If a location contains its own add_header, the server block's add_header is not used in that location. Add the same line to that location as well (on Nginx 1.29.3 and later, add_header_inherit merge; carries them over)

For detailed HSTS settings, see "Setting up HSTS to enforce HTTPS".

If you use a CDN or load balancer

If HTTPS terminates at Cloudflare or a load balancer that connects to Nginx over HTTP, redirecting to HTTPS on Nginx's port 80 causes a loop. In that case, do the redirect on the CDN or load balancer (on Cloudflare, "Always Use HTTPS"). If you set Cloudflare's SSL/TLS mode to "Full (strict)" so Cloudflare also connects to Nginx over HTTPS, you can use the configuration in this article as is. How to investigate loops is explained in "Causes and fixes for too many redirects".

Apply and verify

sudo nginx -t
sudo systemctl reload nginx

Use curl with -L to follow the redirects to the end and show the status and target at each step.

for u in http://example.com/ http://www.example.com/ https://example.com/; do
  echo "== $u"
  curl -sIL "$u" | grep -iE '^HTTP|^location'
done

If each shows 301 only once, Location is https://www.example.com/, and the last line is 200, it works as intended. On the canonical URL, also check the HSTS header.

curl -sI https://www.example.com/ | grep -i strict-transport-security

Summary

  • Decide the canonical URL (with or without www), and redirect both HTTP and non-www HTTPS to it in a single hop
  • Redirect to a fixed host name instead of $host, and reject unknown host names with default_server
  • Split server blocks: port 80 redirects, port 443 serves
  • Use 308 when you need to preserve POST
  • Let's Encrypt validation follows redirects, but if the HTTPS side cannot serve the files, exclude /.well-known/acme-challenge/ from the redirect
  • Add HSTS after confirming everything works, starting with a short max-age

Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)

お気軽にご相談ください

お見積りへ お問い合わせへ
▲