Короткий ответ
Пользователь должен попадать на 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 :80 | nginx не слушает 80, старт |
Возможные причины
- Нет server на 80, только 443.
- На 80 тот же
rootбез return. - Certbot вписал редирект только для apex, www живёт отдельно.
- Петля: приложение редиректит на https, прокси говорит, что запрос уже https (
X-Forwarded-Proto httpsпотерян) → снова http origin. - CDN Flexible: edge https, origin http, origin 301 на https edge, edge снова на origin http.
if ($scheme = http)в том же server 443 — антипаттерн и сюрпризы.- 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/null3. Заголовки прокси
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://www → https://www → https://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 | headHTTP: 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
Оба. Иначе каноникал разъедется.