АйТи Фреш
Главная / Статьи / DevOps и автоматизация
DevOps и автоматизация

Почему Vite за Nginx блокирует hostname и как настроить allowedHosts

Автор: Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш» · · ~9 мин чтения
Почему Vite за Nginx блокирует hostname и как настроить allowedHosts

После обновления Vite внутренний адрес перестал открываться, а `allowedHosts: true` мгновенно всё починил? Я оставляю точный список: сначала выясняю, какой Host передаёт Nginx, затем разрешаю нужное имя. Я — Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш». Покажу на лабораторном стенде, какие имена Compose действительно нужны, почему отключение проверки меняет защиту и как проверить результат.

Обновление включило проверку, которой раньше не было

Узнаваемая ситуация: контейнер запущен, Nginx отвечает, но вместо приложения приходит `Blocked request. This host ("frontend") is not allowed`. Первым делом я смотрю тело ответа и статус. Такой 403 означает, что запрос дошёл до проверки Vite. Ошибки DNS, отказ соединения и типичный 502 от прокси разбираются иначе. Пересобирать Docker-сеть и менять порты до этого простого разделения — верный способ добавить к одной неисправности ещё две.

Проверка Host появилась при исправлении GHSA-vg6x-rcgg-rjx6: первые исправленные версии — 6.0.9, 5.4.12 и 4.5.6, advisory опубликовано 20 января 2025 года. Поэтому даже обновление patch-версии могло сломать доступ через внутреннее имя. Это историческая граница изменения, а не рекомендация установить сегодня 6.0.9. В 2026 году нужно брать исправленный выпуск поддерживаемой ветки. [Описание изменения и сценария reverse proxy](https://github.com/vitejs/vite/security/advisories/GHSA-vg6x-rcgg-rjx6).

Я начинаю с фактически работающего процесса: `docker compose exec frontend npm ls vite`, затем смотрю команду запуска в Compose и `docker compose exec nginx nginx -T`. Версия в старом README ничего не доказывает. Заодно выясняю, запущен `vite` или `vite preview`: у preview есть собственный `preview.allowedHosts`, который по умолчанию наследует `server.allowedHosts`, но может быть переопределён. Исправлять соседний режим бессмысленно. [Настройки preview](https://vite.dev/config/preview-options#preview-allowedhosts).

Не путайте server.host и server.allowedHosts: первая настройка задаёт интерфейсы прослушивания, вторая — допустимые имена в запросах.

Разрешать нужно имя из HTTP Host, а не все имена контейнеров

В этой задаче легко смешать три разные вещи: адрес в браузере, имя для соединения Nginx с upstream и заголовок Host, который получает Vite. Пусть браузер обращается к `portal.vector.test:8080`, а Nginx соединяется с `frontend:5173`. Из этого ещё не следует, какое имя проверит Vite. Решает конфигурация прокси. В исходной реализации middleware читает `req.headers.host` и сравнивает hostname без порта; `Origin` и `X-Forwarded-Host` эту проверку не заменяют. [Код проверки Vite 6.0.9](https://raw.githubusercontent.com/vitejs/vite/v6.0.9/packages/vite/src/node/server/middlewares/hostCheck.ts).

Без явного `proxy_set_header Host` Nginx по умолчанию использует `$proxy_host`. Для `proxy_pass http://frontend:5173` это будет `frontend:5173`, и разрешать потребуется `frontend`. Если проксирование идёт через именованную upstream-группу `vitepool`, в Host может оказаться `vitepool`. А с `proxy_set_header Host $host` до Vite дойдёт имя из клиентского обращения. Поэтому я сначала читаю итоговый конфиг с include-файлами, а уже потом составляю список. [Правила proxy_set_header](https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_set_header).

Мой выбор для обычного dev-стенда — сохранять `$host` и явно разрешать адрес приложения. Это проще проверять и объяснять следующему разработчику. Имя сервиса Compose нужно только для реального маршрута, где оно становится HTTP Host: например, для прямого запроса тестового контейнера. Названия сети, проекта, контейнера Nginx и всех соседних сервисов в список не попадают «на всякий случай». Docker DNS обеспечивает поиск сервиса, но не превращает его имя в обязательный элемент allowedHosts. [Сеть и имена сервисов Compose](https://docs.docker.com/compose/how-tos/networking/).

В allowedHosts нужны hostname без протокола, порта и пути. Список сервисов из compose.yaml копировать туда не нужно.
Почему Vite за Nginx блокирует hostname и как настроить allowedHosts — схема

Стенд «Вектор»: воспроизведение и схема для Compose

Для разбора беру стенд «Вектор», моделирующий внутреннюю панель заявок. Название условное; это лабораторное воспроизведение, а не история раскрытого клиентского проекта. В измеренной части использованы Vite 6.0.9, Node.js 20.20.2 и Nginx 1.30.4. Vite слушал 127.0.0.1:5173, Nginx работал в контейнере с сетью хоста, а имя frontend было локальным alias для 127.0.0.1. Три конфигурации прокси слушали loopback-порты 18080–18082. Это проверка HTTP-заголовков; работу внутреннего DNS Compose этим опытом я не подтверждаю.

Сначала в allowedHosts стояло только `portal.vector.test`. Прокси без явного Host вернул 403 с именем `frontend`. Прокси с `Host $host` дал 200 для `portal.vector.test` и 403 для `evil.invalid`. Добавление `frontend` в точный массив восстановило первый маршрут, сохранив отказ для чужого имени на втором. Затем контрольное включение `true` изменило ответ для `evil.invalid` на 200. Вот чем закончилась проверка: обе точные конфигурации работоспособны, но у каждой свой ожидаемый Host; общий выключатель пропускает дополнительный нежелательный запрос. Браузерный HMR в этом опыте не проверялся. После возврата точного списка отдельно проверен входной фильтр Nginx на порту 18083: нужное имя получило 200, чужое — 400. По окончании опыта временный контейнер удалён, исторический Vite остановлен.

Для переноса схемы в Compose ниже предполагается готовый проект с `package-lock.json`, поддерживаемым Vite 8.2.x и скриптом `dev: vite`. Это отдельный пример конфигурации, не отчёт о запуске всей связки. Файл `compose.yaml`: ```yaml services: frontend: image: node:24-bookworm-slim working_dir: /app command: ["sh", "-c", "npm ci && npm run dev"] volumes: - .:/app - frontend_modules:/app/node_modules nginx: image: nginx:1.30.4-alpine ports: - "127.0.0.1:8080:80" volumes: - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro depends_on: - frontend volumes: frontend_modules: ``` У frontend нет `ports`: Nginx обращается к контейнерному 5173 через общую сеть. `expose` для такой связи необязателен. При этом отсутствие публикации порта не изолирует frontend от остальных участников сети. [Публикация портов Docker](https://docs.docker.com/get-started/docker-concepts/running-containers/publishing-ports/).

Порт 8080 доступен с Docker-хоста; для браузера сопоставьте `portal.vector.test` с 127.0.0.1 через hosts или DNS. Команды ниже используют `--resolve`, который действует только для соответствующего запроса curl. `depends_on` задаёт порядок запуска, но не ждёт окончания `npm ci`: ранний 502 ожидаем. Тег Node обновляется внутри ветки, поэтому для воспроизводимого командного стенда я фиксирую проверенный digest образа. Node 24 LTS выбран для нового окружения; исторический runtime воспроизведения переносить не нужно. Ни нормативов по памяти, ни оценок производительности из этого маленького опыта вывести нельзя.

Цифры и версии: Стенд «Вектор»: воспроизведение и схема для Compose — схема
Цифры и версии: Стенд «Вектор»: воспроизведение и схема для Compose

Конфигурация, которую я оставляю после исправления

В существующем `vite.config.ts` дополняю блок server, сохраняя плагины React/Vue и остальные настройки проекта. Ниже показан минимальный пример. Протокол, порт и путь в массив не входят. `host: '0.0.0.0'` нужен, чтобы Vite слушал интерфейс контейнера и был доступен соседнему Nginx; это другая настройка. `strictPort` не позволяет незаметно уйти с 5173 на следующий порт, пока прокси продолжает стучаться в старый. При таком устройстве мне не нужен `frontend` в allowedHosts: Nginx передаёт `portal.vector.test`. ```ts import { defineConfig } from 'vite' export default defineConfig({ server: { host: '0.0.0.0', port: 5173, strictPort: true, allowedHosts: ['portal.vector.test'], }, }) ```

В файле `nginx.conf` я сохраняю Host и отдельно отклоняю неизвестные имена на входе. Этот файл заменяет `/etc/nginx/conf.d/default.conf` официального образа. `map` находится в контексте `http`, куда включается `conf.d`; внутрь `server` его переносить нельзя. Отдельный default server нужен потому, что один `server_name` сам по себе не запрещает остальные имена: запрос может попасть в сервер по умолчанию. [Выбор виртуального сервера Nginx](https://nginx.org/en/docs/http/request_processing.html). ```nginx map $http_upgrade $connection_upgrade { default upgrade; '' close; } server { listen 80 default_server; server_name _; return 400; } server { listen 80; server_name portal.vector.test; location / { proxy_pass http://frontend:5173; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; } } ```

Добавить ровно `frontend` вместо изменения Host тоже допустимо, если это осознанная архитектура. Но здесь есть существенная оговорка. Когда прокси принимает любое внешнее имя и всем запросам подставляет разрешённый `frontend`, Vite уже не видит исходное чужое имя. Точный массив остаётся в файле, а нужную границу фактически должен обеспечивать Nginx. Я предпочитаю, чтобы это было видно в конфигурации, а не существовало только в голове её автора.

Если прокси заменяет любой входящий Host на разрешённый, проверять исходное имя должен сам прокси.

Что снимает allowedHosts: true и насколько это опасно

`allowedHosts: true` разрешает любые имена. При DNS rebinding страница атакующего может сначала загрузиться с его сервера, а затем через изменённый DNS обратиться к доступному dev-серверу, сохраняя собственное доменное имя в запросе. Проверка Host позволяет отвергнуть такое обращение. Возможность эксплуатации зависит от браузера и сетевых ограничений; утверждать, что после одной строки непременно украдут весь репозиторий, неправильно. Но доступ к исходникам, которые обслуживает dev-сервер, — вполне предметный риск. [Модель угроз из advisory Vite](https://github.com/vitejs/vite/security/advisories/GHSA-vg6x-rcgg-rjx6).

В закрытом стенде с контролируемым доступом риск обычно ниже, чем у открытого порта на рабочем ноутбуке. Эта строка сама не публикует порт и не отменяет VPN, авторизацию прокси или ограничения файлов. Однако и точный список не заменяет аутентификацию: обычный HTTP-клиент способен отправить допустимый Host вручную. Я не драматизирую временную диагностическую правку, но оставлять её постоянной считаю плохой экономией. Указать одно известное имя ненамного сложнее, чем написать `true`.

У точного массива тоже есть границы. Vite автоматически допускает localhost, его поддомены и IP-адреса; есть дополнительные разрешения из других настроек, например `server.origin`. Поэтому обещание «теперь разрешён только один Host вообще» неверно. Запись `.vector.test` дополнительно охватывает корневое имя и все его поддомены; для одного портала я её не использую. Разрешать стоит только контролируемые имена. [Документация allowedHosts](https://vite.dev/config/server-options#server-allowedhosts), [дополнительные разрешения в коде](https://raw.githubusercontent.com/vitejs/vite/main/packages/vite/src/node/server/middlewares/hostCheck.ts).

Точный allowedHosts не является аутентификацией и не отменяет автоматические разрешения localhost и IP.
Обратите внимание: Что снимает allowedHosts: true и насколько это опасно — схема
Обратите внимание: Что снимает allowedHosts: true и насколько это опасно

Проверяю отказ отдельно от успешного открытия

После запуска `docker compose up -d` жду готовности Vite в `docker compose logs frontend`, затем проверяю Nginx командой `docker compose exec nginx nginx -t`. Одной открывшейся страницы недостаточно. Мне нужны положительный запрос и отрицательный: иначе конфигурация с `true` выглядит столь же исправной, как точный список. Для показанной Compose-схемы ожидаются следующие ответы: ```bash curl --resolve portal.vector.test:8080:127.0.0.1 \ -o /dev/null -s -w '%{http_code}\n' \ http://portal.vector.test:8080/ # 200 curl -H 'Host: foreign.test' \ -o /dev/null -s -w '%{http_code}\n' \ http://127.0.0.1:8080/ # 400 от Nginx ```

Второй запрос проверяет входной фильтр Nginx, а не allowedHosts самого Vite. Для отдельной проверки приложения я использую Node внутри frontend: публиковать 5173 ради диагностики не требуется. Здесь правильный адрес должен получить 200, а чужое имя — 403. Это функциональная проверка политики имён, не демонстрация эксплуатации DNS rebinding. ```bash docker compose exec frontend node --input-type=module -e ' for (const host of ["portal.vector.test", "foreign.test"]) { const r = await fetch("http://127.0.0.1:5173/", { headers: { Host: host } }); console.log(host, r.status); }' ```

Дальше проверяю HMR в браузере: соединение WebSocket получает 101, изменение исходника появляется без ручной перезагрузки. Заголовки Upgrade и Connection в конфиге выше нужны именно для этого. Если HTML уже отдаётся, а обновления не приходят, снова расширять allowedHosts не надо. Сначала смотрю WebSocket URL и проксирование. В Vite 8.1 параметры соединения перенесены в `server.ws`; прежние поля `server.hmr` ещё совместимы, но устаревают. Для показанного маршрута ручные ws-настройки обычно не нужны. [WebSocket в Nginx](https://nginx.org/en/docs/http/websocket.html), [изменения Vite 8.1](https://raw.githubusercontent.com/vitejs/vite/v8.1.0/packages/vite/CHANGELOG.md).

Ещё одна ловушка — HTTPS перед Nginx. Документация говорит о пропуске host-check, когда HTTPS включён у самого Vite. Если TLS завершается на прокси, а дальше стоит `proxy_pass http://frontend:5173`, сервер Vite остаётся HTTP-сервером и проверка действует. Замок в адресной строке этого не меняет. Я также не добавляю `cors: true` для исправления Host: разрешения Origin и hostname решают разные задачи, и массовое снятие обеих проверок только затрудняет диагностику. [Условие установки middleware в Vite](https://raw.githubusercontent.com/vitejs/vite/main/packages/vite/src/node/server/index.ts).

400 в показанном примере проверяет фильтр Nginx; 403 при прямом запросе проверяет Vite.

Что закрепить, чтобы следующая сборка не вернула проблему

Порядок действий для меня простой: выяснить реальный Host, восстановить нужный маршрут точным разрешением, проверить чужое имя, затем проверить HMR. Очистку всех кешей, смену Docker DNS и переезд на другой порт откладываю до появления соответствующих симптомов. После правки фиксирую рядом Nginx-конфиг, Vite-конфиг и короткие проверки. Комментарий «portal.vector.test приходит через Host $host» полезнее длинного списка имён, происхождение которых через месяц никто не вспомнит. В автоматическую проверку после обновления зависимости я включаю оба HTTP-сценария. Положительный ловит сломанный доступ команды, отрицательный — случайный возврат `true`. Если прямые CI-запросы действительно используют имя `frontend`, это отдельное документированное разрешение, которое удаляется вместе с соответствующим маршрутом.

Учитываю и способ доставки изменений. Новый конфиг, который запечён в образ, требует пересборки; изменённые переменные Compose — пересоздания контейнера, одного `restart` недостаточно. После пересоздания frontend его IP может поменяться, а простой статический upstream Nginx — сохранить прежний адрес: для этого стенда предусматриваю reload или перезапуск Nginx. Такая неисправность обычно проявляется как ошибка связи. Добавление ещё одного hostname её не исправит.

На 5 сентября 2026 года регулярные исправления получает Vite 8.2; для нового окружения я выбираю поддерживаемую ветку и Node 24 LTS. Исторические 6.0.9 и Node 20 из воспроизведения не являются целевым комплектом. А если внутренний портал уже обслуживает сотрудников, сначала выясняю, нужен ли ему вообще dev-сервер: обычную SPA собираю через `vite build` и отдаю `dist` Nginx. `vite preview` тоже предназначен для проверки сборки, а не production. Для SSR нужен предусмотренный фреймворком runtime. [Поддержка Vite](https://vite.dev/releases), [релизы Node.js](https://nodejs.org/en/about/previous-releases), [развёртывание сборки](https://vite.dev/guide/static-deploy).

Цифры и версии: Что закрепить, чтобы следующая сборка не вернула проблему — схема
Цифры и версии: Что закрепить, чтобы следующая сборка не вернула проблему

Частые вопросы

Нужно ли разрешать frontend, nginx и имя сети Compose?

Только если конкретное имя реально приходит в HTTP Host допустимого запроса. При Host $host и адресе portal.vector.test имя frontend остаётся адресом соединения Nginx с сервисом и само по себе не требует разрешения.

Можно ли настроить разрешённое имя через environment?

Да. Для Vite 8.2 передайте процессу через Compose environment переменную __VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS=portal.vector.test. Она дополняет разрешения. Поддержка нескольких имён через запятую появилась в Vite 8.1; для совместимости со старыми ветками используйте массив в конфиге. После изменения environment пересоздайте контейнер.

Почему доступ по IP работает, хотя имени нет в списке?

IP-адреса разрешены Vite автоматически, как localhost и его поддомены. Точный массив задаёт дополнительные имена и не превращает Host-проверку в сетевой ACL.

Почему после исправления 403 не работает HMR?

Проверьте проксирование WebSocket и адрес соединения в браузере. Успешная выдача HTML не подтверждает работу HMR; расширение allowedHosts не исправит отсутствующие Upgrade и Connection.

Разберём ваш стенд разработки
Если после обновления Vite стенд держится на `allowedHosts: true`, я помогу разобрать цепочку запросов и настроить точные разрешения. В «АйТи-Фреш» можно обратиться с задачей по Nginx, Docker и инфраструктуре разработки — начнём с конфигурации и проверяемого результата.
Бесплатная консультация →
© ООО «АйТи-Фреш» · Москва · Все статьи