1. 1
  2. 2
  3. 3
  4. 4
  5. 5

# теория · шаг 1 из 5

Техзадание на конфиг магазина

Коротко

Дальше - работа целиком: конфиг магазина с нуля, пятнадцать требований и двадцать пять проверочных запросов. Новых механизмов здесь нет ни одного, всё уже было. Собирать надо слоями и проверять после каждого, а спотыкаются на трёх вещах, и все три - про наследование: add_header заменяется целиком, proxy_set_header тоже, а SPA-фолбэк уводит запрос в другой location, где заголовки уровня выше уже не действуют.

Готов писать - тебе нужен блок «Правило» со списком требований. Разборы стоит прочитать, если проверка покажет красное на заголовках.

Конфиг выглядит выполняющим все требования: nosniff объявлен на уровне server, у оболочки no-store, у ассетов вечный кеш. Смотрим ответ на /katalog/tovar-42:

↳  выводcurl -sD- на «правильном» конфиге
HTTP/1.1 200 OK
Content-Type: text/html
Cache-Control: no-store

Требование 6 выполнено, требование 10 - нет: заголовка X-Content-Type-Options в ответе не существует, хотя он объявлен уровнем выше.

На что это похоже

Техзадание здесь работает как приёмка, а не как пожелание: каждый пункт превращён в запрос, который либо проходит, либо нет. Это ближе всего к тому, как выглядит настоящая передача сайта в эксплуатацию - пятнадцать строк, по которым идут и говорят «показывай».

Отсюда и способ работы: не писать всё сразу, а закрывать по слою и после каждого проверять. Конфиг, собранный целиком и проваливший семь проверок из двадцати пяти, отлаживается вдвое дольше.

Механизм: четыре слоя сборки

скелет: 80 и 443, SNI, www четыре запроса статика: root, SPA, /assets/ заголовки и 404 бэкенд: /api/ и /ws/ заголовки клиента политика: кеш, лимит, доступ коды 429 и 403 проверяй после каждого слоя, а не в конце
Каждый слой закрывает свою группу проверок и опирается на предыдущий

Правило: пятнадцать требований

Проект - интернет-магазин shop.local. Фронтенд: SPA в /var/www/shop/dist (index.html плюс assets/ с хешем в именах файлов). Бэкенд: app-1:8000 и запасной app-2:8000. Сертификат покрывает shop.local и www.shop.local.

  1. Весь http - на https. Порт 80: постоянный редирект на тот же адрес, вместе со строкой параметров.
  2. Кроме ACME. /.well-known/acme-challenge/ на 80-м отдаётся файлами из /var/www/certbot, иначе сертификат перестанет продлеваться.
  3. Чужой SNI - отказ рукопожатия, а не твой сайт с предупреждением.
  4. Канонизация имени. www.shop.local - постоянный редирект на shop.local с сохранением адреса.
  5. SPA. Корень /var/www/shop/dist; неизвестный путь отдаёт index.html.
  6. Оболочка не кешируется: Cache-Control: no-store.
  7. Ассеты кешируются навсегда: public, max-age=31536000, immutable.
  8. Несуществующий ассет - 404, а не оболочка SPA, и без вечного кеша в ответе: закешированное «нет такого файла» переживёт выкладку.
  9. Сжатие: CSS, JS и JSON сжаты и с Vary: Accept-Encoding; картинки не жмутся; мелочь меньше килобайта не жмётся.
  10. X-Content-Type-Options: nosniff доезжает и с оболочкой, и с ассетами.
  11. API. /api/ - в группу app; бэкенду уходят Host, X-Real-IP, X-Forwarded-For, X-Forwarded-Proto.
  12. WebSocket. /ws/ с апгрейдом через map $http_upgrade $connection_upgrade и поднятым таймаутом чтения.
  13. Кеш каталога. /api/catalog/ на минуту в зоне catalog; запрос с Authorization идёт мимо кеша и не кладётся в него; статус виден как X-Cache-Status.
  14. Защита входа. /api/login - пять попыток в минуту с адреса, всплеск до трёх без задержки, отказ кодом 429.
  15. Служебное. /admin/ только из 192.168.1.0/24; файлы и каталоги с точки закрыты; /healthz отвечает 200 и не пишется в лог.

Разбор: почему оболочка теряет nosniff

Требования 5, 6 и 10 сталкиваются в одной точке. Наивная раскладка выглядит безупречно:

/etc/nginx/conf.d/shop.local.confhttp server
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):

/etc/nginx/conf.d/shop.local.confhttp server
add_header_inherit merge;

Замер после неё: у / появляется и no-store, и nosniff; у ассетов - и вечный кеш, и nosniff.

Разбор: 404 ассета, закешированный на год

Требование 7 просят выполнить с always - привычка, выработанная в пятом и двенадцатом разделах. На ассетах это даёт неожиданный побочный эффект:

↳  выводcurl -sD- /assets/missing.js
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 в логе.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.