SSL и сайты

Как настроить редирект с 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-узлов.

Проверить на практике

Связанные термины

Источники

  1. RFC 9110: HTTP Semantics
  2. Nginx: ngx_http_rewrite_module return directive
  3. Nginx: ngx_http_proxy_module proxy_set_header directive
  4. Werkzeug: X-Forwarded-For Proxy Fix
  5. WordPress Developer Resources: Migrating WordPress and changing the Site URL
  6. WordPress Developer Resources: HTTPS and using a reverse proxy

Как проверить SSL-сертификат сайта

Проверка сертификата не ограничивается датой окончания: важны имя узла, цепочка до доверенного центра и фактическая конфигурация TLS-сервера.