# теория · шаг 1 из 5
Техзадание на конфиг магазина
Коротко
Дальше - работа целиком: конфиг магазина с нуля, пятнадцать требований и
двадцать пять проверочных запросов. Новых механизмов здесь нет ни одного, всё
уже было. Собирать надо слоями и проверять после каждого, а спотыкаются на трёх
вещах, и все три - про наследование: add_header заменяется целиком,
proxy_set_header тоже, а SPA-фолбэк уводит запрос в другой location, где
заголовки уровня выше уже не действуют.
Готов писать - тебе нужен блок «Правило» со списком требований. Разборы стоит прочитать, если проверка покажет красное на заголовках.
Конфиг выглядит выполняющим все требования: nosniff объявлен на уровне
server, у оболочки no-store, у ассетов вечный кеш. Смотрим ответ на
/katalog/tovar-42:
HTTP/1.1 200 OK
Content-Type: text/html
Cache-Control: no-store
Требование 6 выполнено, требование 10 - нет: заголовка X-Content-Type-Options
в ответе не существует, хотя он объявлен уровнем выше.
На что это похоже
Техзадание здесь работает как приёмка, а не как пожелание: каждый пункт превращён в запрос, который либо проходит, либо нет. Это ближе всего к тому, как выглядит настоящая передача сайта в эксплуатацию - пятнадцать строк, по которым идут и говорят «показывай».
Отсюда и способ работы: не писать всё сразу, а закрывать по слою и после каждого проверять. Конфиг, собранный целиком и проваливший семь проверок из двадцати пяти, отлаживается вдвое дольше.
Механизм: четыре слоя сборки
Правило: пятнадцать требований
Проект - интернет-магазин shop.local. Фронтенд: SPA в /var/www/shop/dist
(index.html плюс assets/ с хешем в именах файлов). Бэкенд: app-1:8000 и
запасной app-2:8000. Сертификат покрывает shop.local и www.shop.local.
- Весь http - на https. Порт 80: постоянный редирект на тот же адрес, вместе со строкой параметров.
- Кроме ACME.
/.well-known/acme-challenge/на 80-м отдаётся файлами из/var/www/certbot, иначе сертификат перестанет продлеваться. - Чужой SNI - отказ рукопожатия, а не твой сайт с предупреждением.
- Канонизация имени.
www.shop.local- постоянный редирект наshop.localс сохранением адреса. - SPA. Корень
/var/www/shop/dist; неизвестный путь отдаётindex.html. - Оболочка не кешируется:
Cache-Control: no-store. - Ассеты кешируются навсегда:
public, max-age=31536000, immutable. - Несуществующий ассет - 404, а не оболочка SPA, и без вечного кеша в ответе: закешированное «нет такого файла» переживёт выкладку.
- Сжатие: CSS, JS и JSON сжаты и с
Vary: Accept-Encoding; картинки не жмутся; мелочь меньше килобайта не жмётся. X-Content-Type-Options: nosniffдоезжает и с оболочкой, и с ассетами.- API.
/api/- в группуapp; бэкенду уходятHost,X-Real-IP,X-Forwarded-For,X-Forwarded-Proto. - WebSocket.
/ws/с апгрейдом черезmap $http_upgrade $connection_upgradeи поднятым таймаутом чтения. - Кеш каталога.
/api/catalog/на минуту в зонеcatalog; запрос сAuthorizationидёт мимо кеша и не кладётся в него; статус виден какX-Cache-Status. - Защита входа.
/api/login- пять попыток в минуту с адреса, всплеск до трёх без задержки, отказ кодом 429. - Служебное.
/admin/только из192.168.1.0/24; файлы и каталоги с точки закрыты;/healthzотвечает 200 и не пишется в лог.
Разбор: почему оболочка теряет nosniff
Требования 5, 6 и 10 сталкиваются в одной точке. Наивная раскладка выглядит безупречно:
add_header X-Content-Type-Options nosniff always;
location / { try_files $uri /index.html; }
location = /index.html { add_header Cache-Control "no-store" always; }
Замер по трём адресам сразу объясняет, что произошло:
| запрос | Cache-Control |
X-Content-Type-Options |
|---|---|---|
/ |
no-store |
отсутствует |
/katalog/tovar-42 |
no-store |
отсутствует |
/assets/app.abc123.js |
immutable |
отсутствует |
Механизм двойной. try_files с фолбэком делает внутренний редирект на
/index.html, а тот заново проходит выбор location и попадает в location =
/index.html. Дальше срабатывает правило из пятого раздела: свой add_header в
блоке заменяет унаследованный набор целиком, а не дополняет его.
Лечится двумя способами. Повторить nosniff в каждом блоке, где есть свой
add_header, - надёжно и многословно. Либо одной строкой - той самой add_header_inherit merge из раздела про
наследование (новой директивы тут нет, механизм знаком, появилась она в 1.29.3):
add_header_inherit merge;
Замер после неё: у / появляется и no-store, и nosniff; у ассетов - и
вечный кеш, и nosniff.
Разбор: 404 ассета, закешированный на год
Требование 7 просят выполнить с always - привычка, выработанная в пятом и
двенадцатом разделах. На ассетах это даёт неожиданный побочный эффект:
HTTP/1.1 404 Not Found
Cache-Control: public, max-age=31536000, immutable
Требование 8 формально выполнено - код 404. Но заголовок с always уехал и на
него, то есть браузер и CDN получили указание помнить это «нет такого файла»
целый год. Выложишь недостающий файл - половина клиентов его не увидит.
Правильно здесь always не ставить: без него замер даёт 404 без
Cache-Control, а на успешном ответе заголовок остаётся. always нужен там,
где заголовок важен именно на ошибках, - Retry-After на заглушке,
nosniff, HSTS.
Что ломается без этого
Забытый include mime.types вместе с nosniff даёт белую страницу. Замер
в браузере: JS, отданный как text/plain, при включённом nosniff
Refused to execute script from '.../app.abc123.js' because its MIME type
('text/plain') is not executable
Сайт при этом отвечает 200 на оба запроса, в access_log всё зелено, в
error_log пусто. С include /etc/nginx/mime.types тип становится
application/javascript, и скрипт выполняется. Проверять это надо в браузере -
curl покажет 200 и не заметит проблемы.
proxy_set_header в /ws/ уносит заголовки о клиенте. То же правило
наследования, что у add_header: объявив в блоке WebSocket Upgrade и
Connection, ты потеряешь Host, X-Real-IP и остальное из уровня выше.
В седьмом разделе этот замер уже был - 101 превращается в 200.
Зачем это в работе
Пятнадцать требований выше - не выдумка ради упражнения, а сокращённая версия того, что реально спрашивают при приёмке сайта: шифрование и его продление, канонический адрес, кеширование, заголовки безопасности, лимит на вход, закрытая админка. Собрав это один раз своими руками, ты получаешь скелет, который дальше правится под любой проект.
И привычка, которая переживёт курс: каждое требование проверяется запросом. Не «я написал строку», а «я послал запрос и увидел заголовок». Разница между этими двумя состояниями и есть разница между конфигом, который работает, и конфигом, который выглядит работающим.
Теперь сам
Прежде чем открывать задачу, ответь: в каком порядке ты будешь проверять требования 5, 6, 7 и 8, и какой запрос отличает выполненное требование 8 от невыполненного?
Ответ: сначала 5 - неизвестный путь отдаёт оболочку; потом 6 и 7 - заголовки
у оболочки и у ассета; и только потом 8, потому что он ломается именно тогда,
когда 5 выполнено слишком широко. Отличающий запрос - /assets/missing.js:
если в ответе 404 и Content-Type: text/html от страницы ошибки, требование
выполнено; если 200 и в теле оболочка SPA, значит фолбэк накрыл и /assets/,
и сборщик будет получать html вместо JavaScript. Лечится отдельным блоком с
try_files $uri =404;.
Главное
Пятнадцать требований, двадцать пять проверок, ни одного нового механизма.
Собирай слоями: скелет и TLS → статика и SPA → бэкенд и WebSocket → кеш,
лимиты и доступ, проверяя после каждого слоя. Три места, где рассыпается, - все
про наследование: SPA-фолбэк уводит запрос внутренним редиректом в location =
/index.html, где свой add_header выбрасывает унаследованный nosniff
(лечится повтором или add_header_inherit merge); proxy_set_header в /ws/
так же уносит заголовки о клиенте; а always на вечном кеше ассетов
проставляет immutable и на 404. И проверяй запросами, а не глазами: забытый
include mime.types при включённом nosniff даёт белую страницу при двух
зелёных 200 в логе.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий