Debian 13: Виправлення «Забагато перенаправлень» в Nginx — корекція канонічних і HTTPS циклів (випадок №71)

Було корисно?

Ви змінили «одне маленьке перенаправлення» — і раптом у всіх браузерів загорілося «Too many redirects». Сайт ніби телепортується між HTTP та HTTPS або між www і апексом, поки браузер не здається. Логи Nginx виглядають невинно. Ваш балансувальник наполягає, що він не при чому. І всім треба це виправити до наступної наради.

Це випадок №71: цикл канонічного/HTTPS перенаправлення. Це нудно, часто трапляється і цілком уникнуто — якщо перестати гадати й почати перевіряти, що насправді бачить клієнт, що Nginx думає, що бачить, і що робить ваш upstream-додаток.

Що насправді означає «Too many redirects» в термінах Nginx

Браузери автоматично йдуть за HTTP-перенаправленнями. Вони підуть за багатьма з них — поки не перестануть. Коли ви бачите «Too many redirects», це не моральний вирок. Це цикл: запит A викликає перенаправлення на B; запит B викликає перенаправлення на A (або на C, який зрештою повертається до A). Браузер зупиняється, щоб уникнути нескінченного пінг-понгу.

У світі Nginx цикл зазвичай походить з однієї з таких моделей:

  • Цикл схеми: HTTP → HTTPS → HTTP (часто через плутанину з заголовками проксі).
  • Цикл канонічного хоста: example.comwww.example.comexample.com (два правила перенаправлення суперечать одне одному).
  • Цикл нормалізації шляху: /app/app//app (обробка слешів у Nginx vs додатку vs upstream).
  • Цикл порту: перенаправлення містить явний :443 або :80 і щось «виправляє» це назад.
  • Змішані рівні: перенаправлення CDN/LB плюс перенаправлення Nginx плюс перенаправлення додатку.

Суб’єктивна порада: якщо у вас і Nginx, і додаток роблять канонізацію, оберіть одну сторону. Суперечливі перенаправлення — це як суперечливі таблиці джерел правди: всі програють.

Одна операційна реальність: цикли перенаправлень можна діагностувати за кілька хвилин у терміналі з дисципліною. Найшвидше виправлення — не «спробуй інший rewrite». Найшвидше — зібрати ланцюжок перенаправлень, знайти атрибут, що переключається (схема/хост/шлях) і прибрати одне з двох конфліктних правил.

Факти та контекст, які роблять проблему менш таємничою

  • HTTP-перенаправлення — давнє явище. Коди статусу 301/302 походять із ранніх специфікацій HTTP; «Moved Permanently» з’явився ще до більшості сучасних стеків.
  • 301 став інструментом кешування. Браузери й проміжні компоненти можуть агресивно кешувати 301; відлагодження перетворюється на «я виправив, але мій ноутбук не оновився».
  • 307/308 існують не просто так. Раніше 302 змінював POST на GET у деяких клієнтів; 307/308 зберігають семантику методу послідовніше.
  • Команда return в Nginx безпечніша за rewrite. Старий рушій rewrite потужний, але ним легко створити цикли або випадково продовжувати внутрішні перезаписи.
  • Канонічні перенаправлення хоста почалися як SEO-гігієна. Пошукові системи карали за дублювання контенту; операційники успадкували біль, коли поверх SEO додали TLS.
  • Проксі змінили поняття «HTTPS». Якщо TLS завершується на балансувальнику, Nginx бачить plain HTTP, якщо ви не повідомите йому про X-Forwarded-Proto.
  • HSTS підняла ставки. Після увімкнення HSTS клієнти будуть намагатися HTTPS у будь-якому разі; зламане HTTPS-перенаправлення стає помітним для користувачів одразу.
  • CDN люблять «допомагати». Багато CDN можуть примусово включати HTTPS або переписувати хости. Це добре, доки й Nginx не робить те ж саме.

Швидкий план діагностики (перший/другий/третій)

Перший: зафіксуйте ланцюг перенаправлень точно так, як його бачить клієнт

  1. Використайте curl -I -L і зафіксуйте Location, коди статусів і чи змінюються хост/схема на кожному кроці.
  2. Перевірте, чи цикл чергується між HTTP/HTTPS або між хостами (apex vs www).
  3. Підтвердіть, чи перенаправлення йде від Nginx чи від чогось вище по ланцюжку, дивлячись на заголовки відповіді (Server, кастомні заголовки або трасувальний заголовок, який ви додасте).

Другий: перевірте, що Nginx вважає запитом

  1. Перегляньте конфіг Nginx на предмет return 301, rewrite, блоків if та дубльованих server-блоків для одного й того ж імені.
  2. Подивіться access-логи з $scheme, $host і $http_x_forwarded_proto (тимчасово додайте debug-формат логів, якщо потрібно).
  3. Якщо за проксі/CDN, підтвердіть, що ви довіряєте forwarded-заголовкам тільки з відомих IP.

Третій: ізолюйте шари

  1. Обійдіть CDN/LB, якщо можливо (напряму в origin IP з потрібним заголовком Host).
  2. Тимчасово вимкніть канонічні перенаправлення додатку або встановіть базову URL явно, щоб збігалося з політикою Nginx.
  3. Перевірте ще раз і зупиніться, коли ланцюг матиме максимум одне перенаправлення (краще — нуль для вже канонічних запитів).

Перефразована ідея (приписують керівникам надійності): «Надія — це не стратегія.» Ставтеся до перенаправлень так само: перевіряйте, не відчувайте за настроєм.

Анатомія перенаправлення: канонічний хост, схема та шлях

Рішення про канонічний хост (виберіть одне й застосуйте один раз)

Канонічний хост означає, що ви вирішуєте, чи сайт «живе» на example.com або на www.example.com. Обидва варіанти підходять; невизначеність — ні. Застосуйте правило на одному рівні — бажано на краю (Nginx), бо це дешево й послідовно.

Дві суперечливі правила канонічності — класичний цикл:

  • Nginx змушує www.
  • Додаток змушує апекс (або CDN робить це).
  • Результат: нескінченне відбиття.

Рішення про канонічну схему (HTTPS — будьте чесні)

Якщо TLS завершується на Nginx, $scheme реальний. Якщо TLS завершується раніше, ніж Nginx, $scheme бреше (він буде http), якщо ви не передасте йому X-Forwarded-Proto або стандартизований Forwarded. Багато прикладів «HTTPS redirect» в інтернеті припускають, що Nginx бачить TLS. Це припущення породжує випадок №71.

Рішення про канонічний шлях (слеші та індексні файли)

Цикли шляху виникають, коли кілька компонентів по-різному «нормалізують» URL. Nginx може перенаправляти /app на /app/ через try_files або autoindex, у той час як додаток повертає на /app, тому що вважає, що маршрути не мають закінчуватися слешем. Визначте канонічну політику і впровадьте її в одному місці.

Жарт №1 (коротко, по темі): Цикли перенаправлень — це просто тест навантаження, на який ви не виділили бюджету.

Практичні завдання: команди, виводи та рішення

Це не «запусти це і сподівайся». Кожне завдання включає, що шукати і яке рішення воно підказує. Виконуйте їх на Debian 13, але логіка переносима.

Завдання 1: Відтворіть з повним трасуванням перенаправлень

cr0x@server:~$ curl -sS -D- -o /dev/null -L -I http://example.com/
HTTP/1.1 301 Moved Permanently
Server: nginx
Location: https://example.com/
HTTP/2 301
server: nginx
location: http://example.com/
HTTP/1.1 301 Moved Permanently
Server: nginx
Location: https://example.com/

Що це означає: Схема чергується HTTPS → HTTP → HTTPS. Це цикл. Якщо ви бачите чергування хостів, у вас боротьба за канонічний хост.

Рішення: Перестаньте крутити шляхи. Перейдіть прямо до логіки схеми/каноніки і перевірте заголовки проксі.

Завдання 2: Показати лише Location заголовки (швидкий підпис циклу)

cr0x@server:~$ curl -sS -I http://example.com/ | sed -n 's/^Location: //p'
https://example.com/

Що це означає: Перший хоп — HTTP → HTTPS. Сам по собі це нормально.

Рішення: Тепер протестуйте HTTPS-ендпоінт, щоб побачити, хто повертає вас назад в HTTP.

Завдання 3: Інспект HTTPS-відповіді без переходу за перенаправленнями

cr0x@server:~$ curl -sS -I https://example.com/ | sed -n '1p;/^Location:/p'
HTTP/2 301
location: http://example.com/

Що це означає: Хтось, хто віддає HTTPS, перенаправляє на HTTP. Тим «хтось» може бути Nginx, додаток або проміжний проксі.

Рішення: Визначте, який рівень видав цю відповідь (заголовки, логи і тести обходу).

Завдання 4: Підтвердіть, який Nginx відповідає (відбиток заголовків)

cr0x@server:~$ curl -sS -I https://example.com/ | grep -iE '^(server:|via:|x-cache:|x-served-by:|cf-|x-amz-)'
server: nginx

Що це означає: Не остаточно, але якщо ви бачите CDN-специфічні заголовки, ви не говорите напряму зі своїм Nginx.

Рішення: Якщо підозрюєте проміжний елемент, обійдіть його далі.

Завдання 5: Обійдіть DNS/CDN і влучте в origin IP з Host заголовком

cr0x@server:~$ curl -sS -I --resolve example.com:443:203.0.113.10 https://example.com/ | sed -n '1p;/^location:/Ip'
HTTP/2 301
location: http://example.com/

Що це означає: Навіть влучаючи в origin, HTTPS перенаправляє на HTTP. Ймовірно, це Nginx або upstream-додаток за Nginx.

Рішення: Перевірте конфіг Nginx та upstream-перенаправлення додатку.

Завдання 6: Здампіть активну конфігурацію Nginx (щоб не гадати, який файл включено)

cr0x@server:~$ sudo nginx -T 2>/dev/null | sed -n '1,80p'
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
# configuration file /etc/nginx/nginx.conf:
user www-data;
worker_processes auto;
...

Що це означає: Ви отримали відрендерену конфігурацію разом з include файлами. Це ваші дані істини.

Рішення: Шукай вивід за редирект-правилами й дубльованими server-блоками.

Завдання 7: Знайдіть директиви перенаправлення і підозрілі умови if

cr0x@server:~$ sudo nginx -T 2>/dev/null | grep -nE 'return 30[12]|rewrite |if \(|server_name|listen 80|listen 443'
412:    listen 80;
417:    server_name example.com www.example.com;
420:    if ($scheme = http) { return 301 https://$host$request_uri; }
612:    listen 443 ssl http2;
618:    if ($scheme = https) { return 301 http://$host$request_uri; }

Що це означає: Ви буквально маєте протилежні перенаправлення: HTTP→HTTPS в одному сервері, HTTPS→HTTP в іншому. Ось ваш цикл.

Рішення: Видаліть неправильне HTTPS→HTTP перенаправлення. Замініть усім єдиною канонічною політикою.

Завдання 8: Підтвердіть, куди потрапляють запити (який server block) через access-логи

cr0x@server:~$ sudo tail -n 3 /var/log/nginx/access.log
203.0.113.55 - - [30/Dec/2025:11:32:18 +0000] "GET / HTTP/1.1" 301 169 "-" "curl/8.5.0"
203.0.113.55 - - [30/Dec/2025:11:32:18 +0000] "GET / HTTP/2.0" 301 169 "-" "curl/8.5.0"
203.0.113.55 - - [30/Dec/2025:11:32:19 +0000] "GET / HTTP/1.1" 301 169 "-" "curl/8.5.0"

Що це означає: Той самий клієнт, багаторазові 301. Потрібен додатковий контекст: host, scheme і forwarded proto.

Рішення: Тимчасово додайте debug log_format, що друкує важливі змінні.

Завдання 9: Додайте тимчасовий log_format, щоб показати scheme/host/forwarded proto

cr0x@server:~$ sudo tee /etc/nginx/conf.d/zz-debug-logformat.conf >/dev/null <<'EOF'
log_format diag '$remote_addr host=$host scheme=$scheme '
               'xfp=$http_x_forwarded_proto uri=$request_uri '
               'status=$status loc=$sent_http_location';
access_log /var/log/nginx/access_diag.log diag;
EOF

Що це означає: Ви створили окремий access-log для діагностики, не чіпаючи основний формат.

Рішення: Перезавантажте Nginx і виконайте один curl-запит; потім прочитайте diag-лог.

Завдання 10: Безпечно перезавантажте Nginx і підтвердіть валідність конфігурації

cr0x@server:~$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
cr0x@server:~$ sudo systemctl reload nginx

Що це означає: Синтаксичних помилок немає; перезавантаження застосовано.

Рішення: Тепер ви можете довіряти diag-логу як відображенню поточної поведінки.

Завдання 11: Згенеруйте один запит і прочитайте діагностичний лог

cr0x@server:~$ curl -sS -I https://example.com/ >/dev/null
cr0x@server:~$ sudo tail -n 1 /var/log/nginx/access_diag.log
203.0.113.55 host=example.com scheme=https xfp= uri=/ status=301 loc=http://example.com/

Що це означає: Nginx бачить scheme=https (отже TLS, ймовірно, на Nginx), але все одно повертає перенаправлення на http://. Це явне правило в конфігу, а не плутанина з проксі.

Рішення: Видаліть будь-яке HTTPS→HTTP перенаправлення. Якщо вам потрібен внутрішній HTTP, тримайте його внутрішнім — не понижуйте зовнішніх клієнтів до HTTP.

Завдання 12: Якщо за проксі, перевірте, як виглядає forwarded proto

cr0x@server:~$ curl -sS -I --resolve example.com:80:203.0.113.10 http://example.com/ -H 'X-Forwarded-Proto: https' | sed -n '1p;/^Location:/p'
HTTP/1.1 301 Moved Permanently
Location: https://example.com/

Що це означає: Коли ви підказуєте Nginx, що оригінальна схема була HTTPS, він обирає HTTPS. Добре: вашу логіку можна зробити proxy-aware.

Рішення: Реалізуйте обробку forwarded-proto правильно й безпечно (довіряйте лише відомим IP проксі).

Завдання 13: Визначте, хто слухає порти 80/443 (уникніть тіньових сервісів)

cr0x@server:~$ sudo ss -ltnp | grep -E ':(80|443)\s'
LISTEN 0      511          0.0.0.0:80        0.0.0.0:*    users:(("nginx",pid=1234,fd=6))
LISTEN 0      511          0.0.0.0:443       0.0.0.0:*    users:(("nginx",pid=1234,fd=7))

Що це означає: Nginx — єдиний слухач на 80/443. Якщо б ви бачили щось інше (Apache, дев-сервер), ви б дебажили не той процес.

Рішення: Якщо порти конфліктують, виправте це спочатку. Логіка перенаправлень марна, якщо відповідає не та демона.

Завдання 14: Перегляньте файли віртуальних хостів, увімкнені на Debian

cr0x@server:~$ ls -l /etc/nginx/sites-enabled/
total 0
lrwxrwxrwx 1 root root 34 Dec 30 10:58 example.conf -> ../sites-available/example.conf

Що це означає: У вас один увімкнений сайт. Якщо є кілька файлів з перекриваючим server_name, очікуйте непередбачуваного матчингу.

Рішення: Переконайтеся, що саме один канонічний server block «володіє» кожним іменем хоста.

Завдання 15: Протестуйте вибір сервера Nginx з явними Host заголовками

cr0x@server:~$ curl -sS -I http://203.0.113.10/ -H 'Host: example.com' | sed -n '1p;/^Location:/p'
HTTP/1.1 301 Moved Permanently
Location: https://example.com/
cr0x@server:~$ curl -sS -I http://203.0.113.10/ -H 'Host: www.example.com' | sed -n '1p;/^Location:/p'
HTTP/1.1 301 Moved Permanently
Location: https://www.example.com/

Що це означає: Обидва хости перенаправляють себе на HTTPS. Якщо ви хотіли каноналізувати на апекс лише, це неправильно (але не обов’язково цикл).

Рішення: Вирішіть канонічний хост і застосуйте його явно (одне перенаправлення, а не два паралельні світи).

Завдання 16: Перевірте, що додаток не віддає власні перенаправлення схеми/хоста

cr0x@server:~$ curl -sS -I http://127.0.0.1:8080/ | sed -n '1p;/^Location:/p;/^Server:/p'
HTTP/1.1 301 Moved Permanently
Server: gunicorn
Location: https://example.com/

Що це означає: Ваш upstream-додаток перенаправляє на HTTPS самостійно. Це може бути коректним, але тільки якщо він погоджується з Nginx. Якщо Nginx теж перенаправляє (або ще гірше — у зворотному напрямку), виникнуть цикли.

Рішення: Виберіть: канонічні перенаправлення або в Nginx, або в додатку. Потім вимкніть іншу сторону.

Шаблони виправлень, що працюють (правильна конфігурація Nginx)

Шаблон A: TLS завершується на Nginx (найпростіше, найнадійніше)

Це найчистіша конфігурація: клієнти підключаються до Nginx на 443, і Nginx знає істинну схему. Ваші перенаправлення можуть використовувати $scheme безпечно, бо він відображає реальність.

Правила:

  • Сервер на порті 80: перенаправляє все на канонічний HTTPS-хост.
  • Сервер на порті 443: віддає контент; опційно перенаправляє неканонічні хости на канонічний хост (залишаючись на HTTPS).
  • Ніколи не «якщо схема https, то редиректувати на http». Якщо вам потрібен plain HTTP для приватної мережі, зробіть це на іншому імені хоста або слухачі, а не знижуючи публічних користувачів.
cr0x@server:~$ sudo tee /etc/nginx/sites-available/example.conf >/dev/null <<'EOF'
# Canonical policy:
# - canonical host: example.com (no www)
# - canonical scheme: https
# - all HTTP requests redirect to https://example.com$request_uri
# - all HTTPS requests to www redirect to https://example.com$request_uri

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    return 301 https://example.com$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Your normal site config:
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name www.example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    return 301 https://example.com$request_uri;
}
EOF

Чому це працює: рішення про каноніку приймається один раз для кожної неканонічної точки входу. Ви ніколи не перенаправляєте з канонічного → неканонічного.

Шаблон B: TLS завершується upstream (балансувальник/CDN), Nginx бачить лише HTTP

Саме тут люди спотикаються. Nginx бачить $scheme=http, бо LB підключається до Nginx по plain HTTP. Якщо ви написали «if scheme is http redirect to https», ви просто змусили LB→origin-хоп теж перенаправляти. Залежно від того, як LB поводиться, це може створити цикл або принаймні зайві перенаправлення.

Що насправді потрібно: «Якщо клієнт використав HTTP, перенаправити на HTTPS». Цю «клієнтську схему» треба брати з довіреного заголовка.

Робіть по-людськи: довіряйте X-Forwarded-Proto лише з підмереж ваших проксі/балансувальників, і використовуйте змінну, що представляє оригінальну схему клієнта.

cr0x@server:~$ sudo tee /etc/nginx/conf.d/forwarded-proto.conf >/dev/null <<'EOF'
# Trust X-Forwarded-Proto only from known proxies/LBs.
# Replace these with your actual proxy subnets.
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 192.168.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

# Derive a client-facing scheme.
map $http_x_forwarded_proto $client_scheme {
    default $scheme;
    https https;
    http  http;
}
EOF

Тепер використовуйте $client_scheme в логіці перенаправлень замість $scheme:

cr0x@server:~$ sudo tee /etc/nginx/sites-available/example.conf >/dev/null <<'EOF'
# Canonical policy behind a TLS-terminating proxy:
# - canonical host: example.com
# - canonical scheme: https (as seen by the client)
# - Nginx listens on 80 only (proxy-to-origin), but still enforces canonical policy
#   using X-Forwarded-Proto from trusted proxies.

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    # Redirect non-https clients to https canonical.
    if ($client_scheme != "https") {
        return 301 https://example.com$request_uri;
    }

    # Redirect www to apex (still https).
    if ($host = "www.example.com") {
        return 301 https://example.com$request_uri;
    }

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $client_scheme;
    }
}
EOF

Так, я використав if на рівні server. Це одне з небагатьох місць, де це нормально. Уникайте if всередині location для складних переписувань; серверні редиректи прості й передбачувані.

Жорстке правило: якщо ви можете винести канонічні перенаправлення на край (LB/CDN), зробіть це і вимкніть їх в Nginx. Але не розпорошуйте відповідальність по рівнях, якщо вам не подобається екстрені дзвінки.

Шаблон C: Нормалізація шляху — припиніть слеш-пінг-понг

Якщо ваш цикл переключає трейлінг-слеші, потрібно уніфікувати політику. Nginx може її змусити, але якщо ваш додаток теж це робить — оберіть одну сторону.

Звичайний безпечний вибір: «директорії закінчуються слешем, файли — ні». Nginx має власну думку з цього приводу. Якщо роутер вашого додатку ненавидить трейлінг-слеші, вимкніть автоматичні редиректи директорій у Nginx, узгодьте try_files і маршрути, або дайте додатку повністю відповідати за це.

За проксі/CDN: довіряти заголовкам без обману себе

Forwarded-заголовки одночасно необхідні й небезпечні. Необхідні, бо origin не бачить TLS клієнта. Небезпечні, бо будь-який клієнт може відправити X-Forwarded-Proto: https і обдурити наївні конфіги, змусивши генерувати HTTPS-посилання, позначати cookie як secure або пропускати перенаправлення.

Зробіть «довіру» явною

Довіра має бути умовною по IP-джерелу. На Debian 13 Nginx зазвичай постачається з пристойними значеннями за замовчуванням, але він не здогадається про межі вашої мережі. Ви повинні встановити set_real_ip_from на ваші фактичні підмережі проксі.

Знайте, який заголовок ваш проксі відправляє

Багато систем використовують X-Forwarded-Proto. Деякі — стандартизований Forwarded. Деякі ставлять вендор-специфічний заголовок. Суть не в назві; суть у послідовності по всьому ланцюжку.

Уникайте сюрпризу «абсолютного перенаправлення»

Nginx може генерувати абсолютні перенаправлення. Якщо ви випадково витікаєте внутрішні hostnames (наприклад origin.internal) у заголовок Location, користувачі безкоштовно дізнаються карту вашої мережі. Це приз не для вас.

Якщо ви бачите перенаправлення на невірний хост, ймовірно ви використали $host, коли мали на увазі фіксований канонічний домен, або ваш проксі несподівано переписує Host.

Коли це не Nginx: додаток, що воює з вами

Деякі фреймворки перенаправляють на «базовий URL», примушують HTTPS або застосовують правила щодо трейлінг-слешів. Якщо додаток працює за проксі і не розуміє forwarded-заголовків, він може думати, що кожен запит — HTTP, і «підвищувати» його — у той час як Nginx (або проксі) знижує назад, або навпаки.

Три тактичні кроки, які виправляють реальні продакшн-системи:

  • Встановіть зовнішню URL додатку явно (base URL / public URL). Багато систем мають одну конфігураційну опцію для цього, і вона запобігає плутанині хоста/схеми.
  • Переконайтеся, що додаток враховує forwarded-заголовки лише від довірених IP проксі (та сама ідея, що й для Nginx).
  • Вирішіть, хто відповідає за перенаправлення. Якщо додатку потрібні вони для маршрутизації, нехай додаток робить нормалізацію шляхів, а Nginx — лише канонікування схеми/хоста — або навпаки. Просто не дублюйте.

Жарт №2 (коротко, по темі): Якщо два компоненти обидва «запроваджують канонічні URL», вони нарешті погодяться — відразу після завершення інциденту.

Три корпоративні міні-історії з полів перенаправлень

Міні-історія 1: Інцидент через неправильне припущення

Компанія мала акуратну конфігурацію: керований балансувальник завершував TLS, потім форвардив трафік на Nginx на порт 80 у приватній мережі. Хтось додав «просте» правило в Nginx: редирект HTTP→HTTPS. Вони протестували це, прокрутивши origin напряму по HTTP і бачили очікуваний 301. Відправили в продакшн.

У продакшні балансувальник підключався до Nginx по HTTP (як задумано). Nginx бачив $scheme=http для кожного запиту. Тож він перенаправляв кожен запит на HTTPS. Балансувальник сумлінно слідував редиректам у своїх health-check’ах і почав падати, бо не міг погодити TLS з origin, що не говорив TLS. Пул виснажився. Сайт померк, хоч «редирект був правильний».

Виправлення було простим: прибрати схему-редиректи з origin і вимагати HTTPS на балансувальнику. Якщо ж обов’язково потрібно примусове перенаправлення на origin, використовувати X-Forwarded-Proto і довіряти лише підмережі балансувальника. Головний урок: не писати редиректи на основі того, що бачить origin, якщо origin не є TLS-точкою завершення.

Після цього додали перевірку деплою: запускається curl -I і порівнюється ланцюжок перенаправлень для публічної точки та обхідного шляху до origin. Наступного разу pipeline зловив невідповідність перед клієнтами.

Міні-історія 2: Оптимізація, що повернулась бумерангом

Інша організація вирішила зменшити кількість редиректів заради продуктивності. Мета: «жодних редиректів зовсім». Вони прибрали редирект на порті 80 і сконфігурували CDN для канонізації. На папері — чище: менше кругових поїздок, менше навантаження на origin і послідовна поведінка глобально.

Потім вони додали друге правило CDN: нормалізувати www в апекс. Тим часом додаток все ще примушував www через legacy-інтеграції. Цього не було видно в базовому моніторингу, бо сайт завантажувався для деяких користувачів — залежно від стану кешу і введеного хоста.

Гірше було в непостійності. CDN кешував 301 в деяких POP’ах. Деякі користувачі бачили цикл, деякі — ні. Внутрішні команди «не могли відтворити», що корпоративною мовою означає «я спробував один раз і втомився».

Вони відновились, оголосивши одну канонічну політику і реалізувавши її в одному місці: CDN. Потім вимкнули примус додатку до www і встановили base URL додатку на канонічний домен. Продуктивність покращилася і лишилася кращою, бо політика перестала осцилювати по рівнях.

Міні-історія 3: Нудна, але правильна практика, що врятувала

Команда платіжного сервісу мала правило: кожна зміна поведінки на краю вимагала «знімок ланцюга перенаправлень», прикріплений до запиту на зміну. Це було нудно. Люди скаржилися. Але це змушувало вияснити: що відбувається для HTTP апекс, HTTP www, HTTPS апекс, HTTPS www і одного дивного шляху з query string.

Під час рутинного оновлення Debian 13 конфіг Nginx був рефакторений. Новий інженер ненавмисно дублював server_name в двох увімкнених vhost-файлах. Nginx не помилявся; він просто обирав один server block для деяких запитів залежно від пріоритету матчингу. Перенаправлення стали непослідовні.

Оскільки була звичка робити знімки, рев’юер помітив, що HTTPS www перенаправляє на HTTP апекс — явно неправильно — до того, як зміна дійшла до продакшну. Ніяких героїчних дій, жодного інциденту, жодного постмортему. Просто відхилення зміни і виправлення.

Ця практика не виглядала інноваційною. Але була. Найкраща робота в операціях часто схожа на паперову роботу — поки одного дня вона не рятує від пожежі.

Типові помилки (симптом → корінна причина → виправлення)

1) Симптом: HTTP ↔ HTTPS пінг-понг

Корінна причина: TLS завершується на проксі, але origin перенаправляє на основі $scheme або недовіреного forwarded-заголовка. Або лишився явний HTTPS→HTTP редирект з старої міграції.

Виправлення: Застосуйте схему на TLS-точці завершення. Якщо Nginx має її примушувати за проксі, використовуйте відображення $http_x_forwarded_proto в $client_scheme і довіряйте йому лише від проксі-IP.

2) Симптом: www ↔ апекс пінг-понг

Корінна причина: Nginx канонізує на www, додаток — на апекс (або навпаки). Іноді CDN канонізує один шлях, а origin — інший.

Виправлення: Виберіть один канонічний хост і застосуйте його в одному шарі. Вимкніть хост-редиректи в іншому шарі або налаштуйте base URL відповідно.

3) Симптом: тільки деякі користувачі бачать цикл

Корінна причина: Кешовані 301 в браузерах, CDN або корпоративних проксі. Або у вас кілька origin/екземплярів з різними конфігами.

Виправлення: Очистіть CDN-кеші для відповідей з редиректами, якщо потрібно. Тестуйте з чистого клієнта і curl. Переконайтесь у консистентності конфігів між інстансами.

4) Симптом: цикл з’являється лише на конкретному шляху

Корінна причина: Нормалізація трейлінг-слешів відрізняється між Nginx і додатком для того маршруту, або try_files спричиняє директорне перенаправлення, яке додаток відхиляє.

Виправлення: Визначте одну політику шляху. Або дайте додатку це обробляти і уникайте Nginx path-redirects, або реалізуйте явні і послідовні редиректи в Nginx і вимкніть нормалізацію в додатку.

5) Симптом: редиректи ведуть на внутрішній хост або невірний порт

Корінна причина: Неправильне використання $host за проксі, який переписує host, або додаток генерує абсолютні URL на основі внутрішньої адреси слухача. Іноді це робить proxy_redirect «допоміжно».

Виправлення: Використовуйте фіксований канонічний домен у return 301. Налаштуйте upstream, щоб він знав свій публічний URL. Перевірте proxy_redirect.

6) Симптом: браузер продовжує цикл навіть після виправлення конфігу

Корінна причина: Закешований 301 у браузері, або HSTS змушує HTTPS і виявляє іншу проблему перенаправлення, або ви не перезавантажили Nginx (таке трапляється частіше, ніж хочеться зізнатися).

Виправлення: Перевірте з curl з чистого середовища. Підтвердіть перезавантаження і активний конфіг через nginx -T. Якщо HSTS увімкнено, переконайтесь, що HTTPS-ендпоінт правильний перед змінами HTTP.

Контрольні списки / покроковий план

Крок за кроком: безпечне виправлення канонічних + HTTPS циклів

  1. Запишіть вашу канонічну політику в одному реченні: «Канонічний — https://example.com (без www).» Якщо ви не можете її прописати, ви не зможете її домогтися.
  2. Зберіть ланцюги перенаправлень для чотирьох точок входу: HTTP апекс, HTTP www, HTTPS апекс, HTTPS www. Зафіксуйте коди статусів і цілі Location.
  3. Обійдіть проміжні елементи (CDN/LB), щоб побачити, чи origin сам створює цикл.
  4. Відрендерте активний конфіг Nginx через nginx -T і знайдіть всі директиви перенаправлення.
  5. Ліквідуйте суперечності: видаліть будь-яке правило, що перенаправляє з канонічного → неканонічного.
  6. Визначте, де живе примус схеми: якщо TLS завершується на LB/CDN, застосовуйте HTTPS там, а не в origin — якщо ви не імплементуєте довіру до forwarded-proto.
  7. Визначте, де живе примус хоста: робіть це на краю (Nginx або CDN) і вимкніть хост-редиректи в додатку або налаштуйте base URL відповідно.
  8. Безпечно перезавантажте (nginx -t потім systemctl reload), протестуйте через curl, потім браузер.
  9. Видаліть тимчасові діагностичні логи після підтвердження стабільності. Діагностика — добре; постійний шум — ні.
  10. Додайте регресійний тест: скрипт, що перевіряє ці чотири точки входу, має максимум одне перенаправлення і приводить до канонічного URL.

Операційний чекліст: перед тим, як оголосити перемогу

  • Канонічний URL повертає 200 (або очікуваний статус додатку), а не 301.
  • Неканонічні URL повертають рівно одне перенаправлення на канонічний.
  • Ціль перенаправлення ніколи не понижує HTTPS до HTTP.
  • Жодне перенаправлення не вказує на внутрішній хост, приватний IP або несподіваний порт.
  • Логи підтверджують правильні змінні Host і схему.
  • Health checks (LB/CDN) не слідують за редиректами, що ламають доступність origin.

Питання та відповіді

1) Чому браузер каже «Too many redirects», а в error-log Nginx тиша?

Бо редиректи не є помилками для Nginx. 301 — нормальна відповідь. Браузер виявляє цикл, після повторних редиректів.

2) Чи використовувати rewrite чи return 301 в Nginx?

Використовуйте return 301 для канонічних хост/схема редиректів. Це зрозуміліше і менш схильне до циклів. rewrite — лише коли дійсно потрібна маніпуляція URI через regex.

3) Мій TLS завершується на load balancer. Чи завжди if ($scheme = http) — це помилка?

Це невірно для визначення, що використав клієнт, бо $scheme відображає хоп LB→origin. Використовуйте довірений forwarded-proto заголовок і мапте його в змінну на кшталт $client_scheme.

4) Чи можна довіряти X-Forwarded-Proto з інтернету?

Ні. Будь-хто може його відправити. Довіряйте тільки з відомих підмереж проксі. Інакше ви дозволяєте потенційним атакувальникам впливати на поведінку, важливу для безпеки.

5) Чому я бачу різну поведінку між curl і браузером?

Браузери кешують 301, можуть застосовувати HSTS і іноді мають кеш DNS або поведінку service worker. Curl зазвичай «свіжий», якщо ви не скриптуєте кеш. Якщо браузер відрізняється, тестуйте в приватному вікні і перевірте статус HSTS.

6) Який код статусу використовувати для HTTP → HTTPS?

Для типових сайтів 301 підходить. Якщо ви перенаправляєте POST і дбаєте про збереження методу, розгляньте 308. Послідовність важливіша за тренди.

7) Я виправив цикл, але тепер мене перенаправляє на невірний хост. Чому?

Ймовірно ви використали $host в редиректі, а вхідний Host-заголовок не той, що ви очікували (проксі переписує, альтернативні домени). Для канонізації краще вказувати фіксований домен у return.

8) Як зупинити цикли через трейлінг-слеші?

Виберіть одну політику і впровадьте її один раз. Якщо додаток хоче «без трейлінг-слешу», вимкніть поведінку Nginx, що додає його, і налаштуйте додаток генерувати посилання послідовно. Якщо Nginx відповідає за це, зробіть редирект явним і переконайтеся, що додаток не редиректить назад.

9) Чи змінює Debian 13 щось у перенаправленнях Nginx?

Фундаментально — ні. Ті ж способи виходу з ладу. Практично: оновлення може реорганізувати включувані конфіги або увімкнути нові site-файли, що підвищує ймовірність дублювання server-блоків і конфліктних редиректів.

Висновок: наступні кроки для запобігання повторенню

Цикли перенаправлень — це не містика. Це суперечливі політики, виконані сумлінно. Ваше завдання — прибрати суперечності.

Наступні кроки, які окупаються негайно:

  • Оголосіть канонічний URL (схема + хост) і впровадьте його в одному шарі.
  • Зробіть Nginx proxy-aware тільки якщо необхідно, і тільки з довіреними forwarded-заголовками.
  • Додайте регресійний тест ланцюга перенаправлень в деплойн: чотири точки входу всередині, один канонічний кінець зовні.
  • Тримайте редиректи нудними: використовуйте return, уникайте хитромудрих rewrite-ів і видаляйте старі міграційні правила, коли вони відпрацювали.

Якщо ви зробите це один раз правильно, ви припините бачити випадок №71 під час оновлень, змін CDN або тієї п’ятничної «невеликої SEO правки», яка якимось чином завжди потрапляє в продакшн.

← Попередня
Права доступу веб‑кореня в Debian/Ubuntu: припиніть 403 без 777 (Випадок №69)
Наступна →
WordPress «Папка призначення вже існує»: виправити встановлення без шкоди wp-content

Залишити коментар