# Redirect HTTP to HTTPS: Nginx, Apache, Caddy, IIS and CDNs

> Force HTTPS with one 301 or 308 redirect on Nginx, Apache, Caddy, IIS or a CDN, avoid loops behind proxies, keep ACME HTTP-01 renewals working.

Source: https://ssl.sh/guides/redirect-http-to-https/
Last updated: 2026-11-10

Redirect HTTP to HTTPS with one permanent (301 or 308) redirect on the port-80 listener, from each `http://` URL to the matching `https://` URL. Set up this way, it causes no loops behind proxies and does not break certificate renewal. The examples cover Nginx, Apache httpd 2.4, Caddy 2, IIS 10 and CDNs such as Cloudflare.

## Prerequisites

HTTPS must already work before you force it.

- A valid certificate installed and `https://example.com` loading without warnings (check with the [SSL checker](https://ssl.sh/tools/ssl-checker/)).
- Access to the web server or CDN configuration.
- Pages free of hard-coded `http://` resources; the [mixed content scanner](https://ssl.sh/tools/mixed-content-scanner/) finds them.

## Step 1: Nginx

Give port 80 its own server block that does nothing but redirect:

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

Using a fixed host collapses `www` and the apex in one hop; use `https://$host$request_uri` to keep the requested host. Then `sudo nginx -t && sudo systemctl reload nginx`.

## Step 2: Apache

Apache redirects with `Redirect permanent` in the port-80 virtual host:

```apache
<VirtualHost *:80>
    ServerName example.com
    ServerAlias www.example.com
    Redirect permanent / https://example.com/
</VirtualHost>
```

If you can only use `.htaccess` (shared hosting), use mod_rewrite:

```apache
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
```

## Step 3: Caddy and IIS

Caddy redirects HTTP to HTTPS automatically for every site with a domain name; no configuration is needed. On IIS, install the URL Rewrite module and add this rule to `web.config`:

```xml
<rule name="HTTP to HTTPS" stopProcessing="true">
  <match url="(.*)" />
  <conditions>
    <add input="{HTTPS}" pattern="^OFF$" />
  </conditions>
  <action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" />
</rule>
```

## Step 4: Behind a CDN or load balancer

Redirect at the edge, or only when the proxy reports HTTP. If the CDN connects to the origin over plain HTTP, the origin sees every request as HTTP and an origin-side redirect loops. Either enable the redirect at the edge (Cloudflare **Always Use HTTPS**, or the equivalent "force HTTPS" switch of your CDN or load balancer) and remove it from the origin, or redirect only when the proxy says the client used HTTP:

```nginx
if ($http_x_forwarded_proto = "http") {
    return 301 https://$host$request_uri;
}
```

Better still, make the CDN connect to the origin over HTTPS with certificate validation (Cloudflare's Full (strict) mode).

## Verify

Each `http://` entry point should return a single 301 or 308 redirect to the final HTTPS URL. Check every entry point:

```bash
curl -sI http://example.com | grep -iE '^(HTTP|location)'
curl -sI http://www.example.com | grep -iE '^(HTTP|location)'
curl -sIL http://www.example.com | grep -iE '^(HTTP|location)'
```

Then confirm the HTTPS endpoint itself with the [SSL checker](https://ssl.sh/tools/ssl-checker/), or from any terminal:

```bash
curl ssl.sh/example.com
```

## Automatic renewal

The redirect does not interfere with Let's Encrypt renewals: HTTP-01 validation follows up to 10 redirects to `http` or `https` on ports 80 or 443 and does not validate the certificate of the HTTPS target, so even an expired certificate does not block renewal. What does break HTTP-01 is redirecting to a different host or port that does not serve the challenge file, or removing the port-80 listener. Keep the port-80 server in place, and let your ACME client renew as usual ([automatic renewal](https://ssl.sh/guides/renew-certificates-automatically/)). After the redirect works, add [HSTS](https://ssl.sh/guides/enable-hsts/).

## Troubleshooting

- **ERR_TOO_MANY_REDIRECTS**: the origin redirects requests that the CDN already received over HTTPS; see Step 4.
- **Mixed content warnings after the switch**: pages still load `http://` scripts or images; see [mixed content warning](https://ssl.sh/errors/mixed-content-warning/).
- **Certificate warning after the redirect**: the HTTPS certificate does not cover the host you redirect to; see [your connection is not private](https://ssl.sh/errors/your-connection-is-not-private/).
- **Forms or API clients break**: a 301 turned POST into GET; use 308 for API endpoints.
- **Renewal fails after adding the redirect**: the redirect points to another host or port; see [certbot timeout during connect](https://ssl.sh/errors/certbot-timeout-during-connect/).
