Как настроить редирект с HTTP на HTTPS без циклов
Надёжный переход на HTTPS сохраняет путь и query string, приводит каждый URL к одному каноническому адресу и не заставляет proxy и приложение перенаправлять друг друга.
Корректный редирект HTTP на HTTPS возвращает постоянный статус и Location с тем же ресурсом на защищённой схеме, сохраняет путь и параметры запроса и заканчивается одним каноническим URL. Правило лучше выполнять на первом доверенном узле, который принимает публичный HTTP: CDN, load balancer или веб-сервер. Приложение не должно повторно перенаправлять запрос, уже пришедший к пользователю по HTTPS.
До включения редиректа HTTPS-адрес обязан работать самостоятельно. Установите сертификат для каждого принимаемого имени, проверьте цепочку и убедитесь, что виртуальный хост на порту 443 отдаёт ожидаемый сайт. Редирект не помогает при ошибке сертификата: TLS-проверка происходит до получения HTTP-ответа.
301 и 308: в чём разница
Оба кода обозначают постоянное перенаправление, но отличаются обработкой метода запроса.
301 Moved Permanently исторически ориентирован на перенос ресурсов и повсеместно поддерживается. По RFC 9110 пользовательский агент может преобразовать POST в GET при автоматическом переходе по Location. Для обычной навигации GET это различие незаметно, но для формы или API тело может быть потеряно.
308 Permanent Redirect запрещает менять метод: POST остаётся POST, PUT — PUT, вместе с телом. Это точнее для API и других запросов, семантика которых должна сохраниться. Поддержку 308 следует проверить у всех критичных клиентов, особенно у старого встроенного ПО или нестандартных библиотек.
Для контентного сайта с преимущественно GET часто выбирают 301 ради привычной совместимости. Для современного API, где перенос метода обязателен, подходит 308. Однако чувствительные данные вообще не должны впервые отправляться по HTTP: даже если 308 повторит POST на HTTPS, исходное незашифрованное обращение уже могло раскрыть тело. Формы и API-клиенты должны изначально использовать HTTPS.
Рекомендуемая настройка Nginx
Безопаснее направлять все известные HTTP-имена сразу на фиксированный канонический HTTPS-host. Например, если основным выбран example.com:
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 308 https://example.com$request_uri;
}
$request_uri сохраняет исходный путь и query string. Фиксированный hostname исключает зависимость назначения от пользовательского Host. Вариант https://$http_host$request_uri опасен: сырой заголовок Host поступает от клиента и может превратить сервер в источник перенаправления на чужой домен. Если действительно нужно сохранять hostname, принимайте только явно перечисленные server_name, используйте нормализованную переменную и предусмотрите отдельный default server, который отклоняет неизвестные имена.
HTTPS обслуживается в другом блоке:
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;
root /var/www/example.com;
}
Не помещайте безусловный редирект на тот же https://example.com$request_uri внутрь основного блока 443: каждый уже канонический запрос получит такой же Location, и браузер остановится с ошибкой количества перенаправлений. Если www должен переходить на основной домен и по HTTPS, создайте отдельный TLS-блок для www с сертификатом, покрывающим это имя, и направьте его на example.com.
Перед применением выполните:
nginx -t
Только после успешной проверки синтаксиса перезагрузите Nginx штатной командой вашей системы. Держите открытой рабочую административную сессию и подготовьте возврат предыдущей конфигурации, но не используйте отключение HTTPS как способ отката после включения HSTS.
Reverse proxy и причина циклов
В схеме «браузер → HTTPS proxy → HTTP-приложение» TLS завершается на внешнем узле. Внутренний запрос к приложению действительно идёт по HTTP, хотя пользователь открыл HTTPS. Если приложение оценивает только локальную схему, оно отвечает редиректом на HTTPS. Proxy снова принимает HTTPS и передаёт HTTP внутрь — цикл повторяется.
Внешний Nginx может передать исходную схему так:
location / {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://app_backend;
}
Заголовок X-Forwarded-Proto не аутентифицирован сам по себе. Proxy должен перезаписывать, а не слепо сохранять клиентское значение. Сетевые правила должны запрещать обход proxy и прямое публичное подключение к backend. Иначе злоумышленник сможет прислать X-Forwarded-Proto: https или другое forwarded-поле и повлиять на построение URL, secure cookies или логику доступа.
Приложение на Flask/Werkzeug может учитывать один доверенный proxy с помощью middleware:
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=0, x_proto=1)
Число x_proto — не переключатель «доверять всем», а точное количество proxy-значений с конца цепочки, которые контролирует оператор. Если узлов два, конфигурацию проектируют под два; если приложение доступно напрямую, использовать ProxyFix без сетевого ограничения небезопасно. Для X-Forwarded-Host, адреса клиента и префикса задаются отдельные параметры, их не стоит включать без необходимости.
Лучший способ избежать дублирования — назначить одного владельца HTTP→HTTPS-перехода. Обычно edge перенаправляет порт 80, сообщает backend проверенную внешнюю схему, а приложение генерирует HTTPS-ссылки и не выполняет второй безусловный редирект. Если переход делает платформа CDN, удалите конкурирующее правило из Nginx или согласуйте чёткие условия.
Настройка WordPress
В одиночной установке WordPress откройте «Настройки → Общие» и проверьте два значения:
- WordPress Address (URL), или адрес WordPress, указывает, где находятся файлы ядра;
- Site Address (URL), или адрес сайта, задаёт публичный адрес для посетителей.
При обычной установке оба значения должны начинаться с https:// и обычно совпадают, например https://example.com, без завершающего слеша. Если ядро намеренно находится в отдельном каталоге, адреса могут различаться — не выравнивайте их механически. Сделайте резервную копию базы до изменения и убедитесь, что новый URL уже доступен.
Когда административная панель недоступна, значения можно временно зафиксировать в wp-config.php:
define('WP_HOME', 'https://example.com');
define('WP_SITEURL', 'https://example.com');
При таком способе поля нельзя редактировать в общих настройках, пока константы заданы. Не добавляйте случайные плагины принудительного HTTPS поверх правил CDN и Nginx: несколько компонентов часто перенаправляют между http и https, www и доменом без www в противоположных направлениях.
За reverse proxy WordPress должен распознавать внешнюю HTTPS-схему. Для показанной выше схемы с одним доверенным Nginx, который перезаписывает X-Forwarded-Proto значением $scheme, и при закрытом прямом доступе к backend добавьте перед строкой /* That's all, stop editing! */ в wp-config.php:
if (
isset($_SERVER['HTTP_X_FORWARDED_PROTO'])
&& $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https'
) {
$_SERVER['HTTPS'] = 'on';
}
Не используйте этот фрагмент, если backend доступен напрямую из интернета или proxy не перезаписывает заголовок: клиент сможет подделать внешнюю схему. Для цепочки из нескольких proxy сначала определите, какой узел формирует доверенное значение и в каком формате оно приходит.
После смены URL проверьте mixed content: старые абсолютные http:// ссылки на изображения и скрипты не исправляются серверным редиректом внутри HTML. Массовая замена в базе требует резервной копии и инструмента, который понимает сериализованные данные WordPress. Значение guid записей нельзя бездумно изменять вместе с контентными URL.
Как проверить цепочку редиректов
Сначала запросите четыре основных варианта: HTTP и HTTPS для домена с www и без него. Каждый должен закончиться одним выбранным HTTPS-адресом. Посмотреть заголовки вручную можно так:
curl -IL --max-redirs 10 'http://example.com/account?from=test'
curl -IL --max-redirs 10 'https://example.com/account?from=test'
curl -IL --max-redirs 10 'http://www.example.com/account?from=test'
curl -IL --max-redirs 10 'https://www.example.com/account?from=test'
В выводе проверьте каждый статус и Location: путь /account и параметр from=test должны сохраниться, неизвестный хост не должен попасть в назначение, а финальный ответ не должен снова перенаправлять на себя. -I отправляет HEAD, поэтому для приложения с особой обработкой методов повторите тест обычным GET. POST/PUT проверяйте отдельно на тестовых данных и убедитесь, что выбранный код сохраняет или меняет метод именно так, как ожидает API.
Проверка HTTP-заголовков помогает увидеть статус и Location с внешней точки. Проверка сайта показывает доступность конечного URL, а проверка SSL нужна для каждого HTTPS-hostname, встречающегося в цепочке. Проверяйте не только главную страницу, но и старые пути, query string, страницу входа, API и ошибку 404.
После стабилизации редиректа обновите внутренние ссылки, sitemap, canonical и интеграции на прямые HTTPS-адреса. Это сокращает лишний сетевой переход. HSTS вводите отдельным поэтапным изменением после того, как сертификаты, поддомены и аварийное продление проверены.
Опасные конфигурации и частые ошибки
- Редиректить и порт 80, и основной порт 443 без условия на один и тот же URL.
- Доверять клиентскому
X-Forwarded-Protoна backend, доступном из интернета. - Настроить HTTPS на CDN, но заставлять origin судить о внешней схеме только по внутреннему HTTP.
- Использовать
$http_hostвLocationбез allowlist доменов. - Перенаправлять всё на главную и терять путь или query string.
- Сначала включить редирект, а сертификат для
wwwили другого исходного имени установить потом. - Создать противоположные правила: CDN добавляет
www, а WordPress удаляет его. - Выбрать 301 для API, не проверив, не превратит ли клиент POST в GET.
- Считать 308 защитой содержимого HTTP POST. Первый запрос всё равно был незашифрованным.
- Включить большой HSTS
max-ageодновременно с миграцией, лишив себя простого отката. - Проверить только финальную страницу и не посмотреть промежуточные
Location. - Кешировать постоянный редирект до завершения тестов: браузеры и промежуточные кеши могут долго хранить ошибочное направление.
Частые вопросы
Какой код выбрать для постоянного перехода: 301 или 308?
Для обычных страниц с GET широко совместим 301. Если клиент обязан сохранить исходный метод и тело запроса, семантически точнее 308: при 301 пользовательский агент по историческим причинам может заменить POST на GET. Поведение критичных API нужно проверять на поддерживаемых клиентах.
Почему после включения HTTPS возникает бесконечный редирект?
Частая причина — TLS завершается на reverse proxy, а приложение видит внутренний HTTP и снова перенаправляет уже защищённый внешний запрос. Также цикл создают противоречащие правила CDN, Nginx, приложения и неверные Site URL или Home URL в WordPress.
Можно ли доверять X-Forwarded-Proto от любого клиента?
Нет. Клиент может подделать этот заголовок. Его должен перезаписывать известный reverse proxy, прямой доступ к приложению следует ограничить, а приложение должно доверять ровно ожидаемому числу proxy-узлов.