Короткий ответ

Пользователь должен попадать на https://site.example одним 301 с http://site.example (и обычно с www). На nginx это отдельный server на listen 80 с return 301 https://site.example$request_uri плюс исключение /.well-known/acme-challenge/. Origin 10.0.20.40, webroot /var/www/site.example. Если уже ERR_TOO_MANY_REDIRECTS — сначала разорвите петлю (CDN Flexible SSL, WordPress FORCE_SSL + редирект nginx, X-Forwarded-Proto), не добавляйте ещё один return.

HSTS не заменяет первый 301 для тех, кто ещё не видел заголовок: HSTS.

Проверяйте apex и www отдельно: один 301, второй 200 на http — типичный пропуск второго server. curl -sI без -L, иначе схлопнете цепочку.

Симптомы и как отличить

  • curl -sI http://site.example/ → 200 и HTML.
  • Или цепочка Location сама на себя.
  • HTTPS работает, HTTP «как отдельный сайт».
curl -I httpВывод
301 Location https://site.example/...цель этой статьи достигнута
200нет редиректа
301 Location http://...цикл или неверный target
connection refused :80nginx не слушает 80, старт

Возможные причины

  1. Нет server на 80, только 443.
  2. На 80 тот же root без return.
  3. Certbot вписал редирект только для apex, www живёт отдельно.
  4. Петля: приложение редиректит на https, прокси говорит, что запрос уже https (X-Forwarded-Proto https потерян) → снова http origin.
  5. CDN Flexible: edge https, origin http, origin 301 на https edge, edge снова на origin http.
  6. if ($scheme = http) в том же server 443 — антипаттерн и сюрпризы.
  7. Apache RewriteRule без RewriteEngine On.

Диагностика

1. Цепочка

curl -sI --resolve site.example:80:10.0.20.40 http://site.example/ | head
curl -sI --resolve www.site.example:80:10.0.20.40 http://www.site.example/ | head
curl -sIL --max-redirs 8 --resolve site.example:80:10.0.20.40 --resolve site.example:443:10.0.20.40 http://site.example/ | grep -iE '^HTTP|^Location'

2. Кто слушает 80

sudo ss -tlnp | grep ':80'
sudo nginx -T 2>/dev/null | grep -E 'listen |server_name |return |rewrite '
sudo apache2ctl -S 2>/dev/null

3. Заголовки прокси

curl -sI --resolve site.example:443:10.0.20.40 https://site.example/ | grep -iE 'strict-transport|location'

В access log смотрите $http_x_forwarded_proto если за CDN.

4. ACME не сломан ли будущим 301

curl -sI --resolve site.example:80:10.0.20.40 http://site.example/.well-known/acme-challenge/ping | head

См. certbot.

Решение

sudo tar -C /etc -czf /root/nginx-redirect-$(date +%F).tar.gz nginx

Сценарий A. Nginx канонический 301

server {
    listen 80;
    listen [::]:80;
    server_name site.example www.site.example;
    location /.well-known/acme-challenge/ {
        root /var/www/site.example;
    }
    location / {
        return 301 https://site.example$request_uri;
    }
}

HTTPS-сервер: server_name site.example, www можно 301 на apex (или наоборот — выберите один канон).

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name www.site.example;
    ssl_certificate     /etc/letsencrypt/live/site.example/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/site.example/privkey.pem;
    return 301 https://site.example$request_uri;
}

nginx -t && reload. Не rewrite ^(.*)$ https://... если хватает return.

Сценарий B. Apache 2.4

<VirtualHost *:80>
    ServerName site.example
    ServerAlias www.site.example
    Redirect permanent / https://site.example/
</VirtualHost>

Redirect склеивает путь. Для ACME — Alias до Redirect или RedirectMatch с исключением. mod_alias должен быть включён.

Сценарий C. Петля за прокси/CDN

Origin не должен 301 на https, если уже получает X-Forwarded-Proto: https. В nginx:

# только если доверяете hop
set_real_ip_from 10.0.20.0/24;
real_ip_header X-Forwarded-For;

Редирект:

if ($http_x_forwarded_proto = "http") {
    return 301 https://site.example$request_uri;
}

if здесь на заголовке — осознанный риск; лучше редирект на самом edge. Для Cloudflare выставьте SSL mode Full (strict) при валидном origin cert, не Flexible.

В WordPress уберите двойной редирект плагинов «Really Simple SSL» + nginx return.

Сценарий D. Certbot --redirect

certbot --nginx --redirect умеет вписать 301. Если vhost нестандартный — правьте руками, не --force-renewal.

Сценарий E. WordPress и is_ssl() за прокси

Если HTTPS терминируется на NPM/CDN, а PHP видит $_SERVER['HTTPS'] пустым, WP редиректит на http, nginx на 80 редиректит на https — петля. В nginx перед FastCGI:

fastcgi_param HTTPS on;
# или при доверенном прокси:
# fastcgi_param HTTPS $http_x_forwarded_proto;  # аккуратно, только свой hop

В wp-config.php допустимо $_SERVER['HTTPS']='on'; когда origin принимает только TLS от NPM в закрытой сети. Не ставьте это, если 80 ещё отдаёт контент наружу.

Проверьте www отдельно: цепочка http://wwwhttps://wwwhttps://apex должна быть два hop максимум, не цикл www↔apex. Канонический хост один.

Certbot --redirect мог вписать return 301 https://$host$request_uri в default, а ваш кастомный server на 80 без return остался default_server — пакеты попадут не туда. nginx -T и curl -I --resolve на IP.

Как проверить, что проблема устранена

curl -sI --resolve site.example:80:10.0.20.40 http://site.example/foo | head
curl -sI --resolve site.example:443:10.0.20.40 https://site.example/foo | head

HTTP: 301 и Location: https://site.example/foo. HTTPS: 200 без Location на http. Нет 5 hop. Mixed: mixed content.

Если не помогло

  • Только с телефона петля: HSTS + http каноникал в приложении.
  • return 301 https://$host$request_uri при Host: внутреннем имени.
  • IPv6 слушает другой конфиг без 301.
  • Панель хостинга ставит свой редирект поверх.
  • Certbot --apache/--nginx вписал редирект в один vhost, а www обслуживает соседний файл без return — тестируйте оба имени site.example и www.site.example через --resolve.

Профилактика

  • Канон apex/www в одном месте.
  • Тест ACME после каждого изменения server 80.
  • Не держать два плагина редиректа в CMS.

FAQ

301 или 302?

Для постоянного HTTPS — 301. 302 кэшируется слабее, для отладки петли временно удобнее.

rewrite ... permanent vs return 301

return проще и быстрее. Rewrite нужен для сложных условий.

Нужен ли редирект POST?

301 на POST часто превращается в GET. Для API не редиректите слепо; API сразу на 443.

HSTS preload без 301

Preload требует HTTPS и HSTS; первый визит по http всё равно нуждается в 301, пока браузер не в preload-списке.

www → apex на 80 и 443

Оба. Иначе каноникал разъедется.