Как-то я болтала с приятелем о том, как было бы здорово, если бы был такой сайт-песочница, где бы можно было просто скопировать-вставить конфиг Nginx-а и протестировать его. И я поняла, что это не так уж сложно сделать, поэтому погрузилась в тему и сделала: nginx-playground.wizardzines.com.
Просьба от переводчика: пожалуйста, не насильничайте рабочий сервис хитрыми директивами вроде создания миллиона воркеров — автор не кичится супер-защищённой средой запуска Nginx. Даже наоборот — взывает к приличному поведению, потому что некоторые уязвимости явно открыты. Вы запросто сможете положить сервис, если хотя бы раз видели доку по настройке Nginx, поэтому тут нет повода для гордости. С другой стороны, мысли об уязвимых местах (а возможно сразу и методах избавления от них) приветствуются в комментариях. Спасибо!
Далее поговорим о том, как тут всё устроено, потому что в процессе я столкнулась с некоторыми любопытными нюансами и проблемами с неочевидными решениями.
Как пользоваться
Нужно задать конфигурацию Nginx, а также написать curl или http команду, которая отправит HTTP-запрос этому сконфигурированному вами веб-серверу.
Вы жмёте "Run" вверху-справа и получаете ответ:
- результат выполненной команды (если Nginx запустился нормально), либо
- лог ошибок Nginx-а (если что-то пошло не так).
Зачем нужна эта песочница?
Я обратила внимание, что подобные песочницы помогают мне учиться: очень полезно иметь возможность быстро и безопасно поэкспериментировать и проверить разные подходы к решению задач, при этом без опасений что-то сломать из-за допущенной ошибки.
А Nginx чрезвычайно привередлив в настройке, и поэтому, я думаю, что уж для него-то точно должен быть такой инструмент.
Вот три других похожих проекта, которые я сделала ранее:
- SQL-песочница — позволяет выполнять произвольные SQL-запросы на небольших данных к SQLite (использует sql.js).
- CSS примеры — используя CodePen демонстрируют некоторые поразительные примеры стилизации с помощью CSS, с которыми вы можете поиграться.
- DNS lookup — позволяет выполнять любые DNS-запросы и видеть ответы.
И несколько других замечательных песочниц, разработанных другими людьми:
- CodePen — для CSS/JS/HTML.
- RegExr — для регулярных выражений.
- DB Fiddle — для SQL (MySQL, PostgreSQL и SQLite).
- Nginx location match tester — имитирует алгоритм перебора директив location конфига Nginx (написан на TypeScript).
Делаем быстро, без усложнений
Сайт состоит из:
- статичного фронтенда (и применением Vue.js и Tailwind, моего типичного фронтендового набора);
- написанного на Go бэкенда с единственным API-методом, который делает лишь одно — запускает Nginx с заданным конфигом.
План прост, потому красив: всего-то нужно написать один API-метод, ну и ещё фронт, который его вызывает. Так же просто выполнен инструмент DNS lookup. Уж очень нравится мне такой подход, думаю, я и остальные проекты буду делать так же.
Давайте расскажу о том, что делает код на бэкенде, когда к нему поступает запрос с фронта.
Что происходит при поступлении запроса
Вот что делает Go-бэкенд, когда вы жмёте "Run" (вот вам сразу заметка с кодом):
- записываем конфиг во временный файл;
- создаём новый сетевой неймспейс (
ip netns add $RANDOM_NAMESPACE_NAME
); - запускаем go-httpbin на порту 7777, чтобы люди могли использовать его в качестве следующего узла в своих конфигурациях Nginx (для отладки обратного прокси);
- запускаем Nginx;
- ждём 100 миллисекунд — позволяем Nginx-у запуститься, а если он не заработал, то возвращаем клиенту лог с ошибками;
- выполняем заданную пользователем команду (предварительно убеждаемся, что команда начинается с
curl
илиhttp
); - возвращаем результат выполнения команды;
- PROFIT!
Вопросы о безопасности
Основная идея инструмента состоит в том, чтобы дать пользователю возможность пробовать совершенно любые конфигурации Nginx. На фоне этого легко представить, сколько всего нехорошего можно натворить на сервере. Я хотела сделать инструмент бесплатным для пользователей, при этом не слишком тратиться на хостинг. В идеале — обойтись одним сервером для обслуживания всех запросов.
Подобные нюансы часто останавливали меня на пути к запуску похожих проектов, но конкретно с этим всё должно было получиться.
Подход к безопасности: немного изоляции и YOLO (!)
Вот, к чему я пришла по поводу безопасности после обсуждения с другом:
- Нужно разместить фронтенд отдельно от бэкенда — где-то в CDN (если бэкенд и будет взломан, то хотя бы на фронте никто не разместит какой-нибудь малварь).
- Не использовать базу данных, только localStorage браузера (невозможно взломать БД, если её нет).
- Изолировать Nginx в собственном сетевом неймспейсе, запретить выходить в глобальную сеть.
- Использовать бесплатный план на fly.io: слабые сервера, зато изолированные виртуальные машины, которые я при желании могу убивать и пересоздавать хоть каждый час.
- Попросить людей быть лапочками в разделе FAQ (это и есть "YOLO" :) )
Самое плохое, что может случиться при всех этих ограничениях:
- кто-то сможет получить доступ к чужому конфигу Nginx, если два человека пользуются сайтом одновременно;
- подменит бэкенд так, что последующие клиенты будут получать всякие гадости в качестве ответа сервера вместо ожидаемого лога с ошибками или результата выполнения curl;
- какой-нибудь хитрец попробует майнить биткоин на этом крошечном виртуальном сервере (1 shared CPU, 256 MB RAM).
Не думаю, что перечисленное выглядит так уж плохо в свете потенциальной пользы от сайта. Впрочем, возможно я упускаю что-то ещё более важное. Например, кто-то уже показал, что можно украсть содержимое файла /etc/passwd
(было занятно, но в нём всё равно ничего важного не было).
Можно было бы запускать Nginx в контейнере (вместо изоляции только с помощью сетевого неймспейса), но это сильно ударит по производительности. И без этого работает не то чтобы очень быстро.
К слову о производительности.
Кое-что о производительности
Как упомянуто ранее, бэкенд работает на довольно слабом железе (1 shared CPU, 256 MB RAM). По этому поводу имею сказать следующее:
- Фронтенд расположен в CDN, поэтому нагрузка бэкенда начинается только тогда, когда пользователь нажимает кнопку "Run". Уже только это значительно облегчает серверу жизнь.
- Если верить логам, запросы занимают порядка 400 миллисекунд. Неплохо!
- Сейчас проект живёт на сервере, расположенном в Торонто. Полагаю, что для удалённых от Торонто пользователей всё происходит ещё медленнее. Так что можно развернуть больше таких маленьких серверов и в других уголках планеты.
- Я применила написанный на Go клон httpbin вместо оригинального (написанного на Python), потому что эта версия показалась более лёгкой и быстрой.
- Производительность на стороне фронтенда слегка хромает: стили и скрипты разделены на несколько файлов. Я не хотела собирать это всё командой
npm build
, потому что я не очень сильна в JavaScript, и поэтому переживала, что в случае возникновения проблем в скриптах это будет дополнительным препятствием перед исправлением ошибок и обновлениями — просто будет лень. - Добавила маленькую гифку с летящей ракетой — её видит пользователь, пока выполняется запрос.
Самая глупая ошибка, связанная с производительностью, была вызвана тем, что для остановки Nginx я отправляла SIGKILL
. Это останавливало только главный процесс, а воркер-процессы продолжали работать, и в какой-то момент в системе заканчивалась свободная память. Когда я переделала всё так, чтобы отправлялся сигнал SIGTERM
, ситуация улучшилась.
Дизайн
Внешне повторяет дизайн JSFiddle и CodePen.
В частности, у JSFiddle есть кое-что занятное: они вычисляют высоту рабочей области как calc(100vh - 60px)
, а для заголовка установлена высота 60px
. Я бы не догадалась сама, а работает это хорошо.
Я использую CodeMirror в качестве редактора с подсветкой синтаксиса, потому что именно его применяют JSFiddle и CodePen. Настроить его было достаточно просто. К тому же, там есть подсветка синтаксиса для конфигов Nginx и shell. Всё, о чём только можно мечтать :)
Была головная боль с этим CodeMirror только из-за того, что я собиралась использовать готовый пакет vue-codemirror
, но были проблемы совместимости с Vue 3. Я решила, что в этом нет необходимости, и просто написала свою крошечную интеграцию, которая обновляет Vue при обновлении текстового поля.
Вы можете взглянуть на код в файле script.js, его там не много.
Что бы сделать ещё: больше примеров конфигов
Я до сих пор не понимаю, какими именно должны быть эти примеры конфигурации, но не лишним было бы добавить парочку разных типичных конфигов в качестве стартовых.
Это было легче, чем казалось
Сделать этот инструмент было гораздо проще, чем я думала. И этот успех побуждает делать ещё больше подобных песочниц для других программ, хотя я и не выбрала следующую. HAProxy кажется хорошим кандидатом.
Комментарии (21)
bBars Автор
14.10.2021 23:27+1Проба пера в переводах. Понравилась тема, привлекла простота изложения и перевода - чем не повод опубликовать на Хабре? Суть вроде бы передал. Где-то есть дискуссионные моменты, а кое-чего вообще не понял: например, про "vue-codemirror ... интеграцию, которая обновляет Vue при обновлении текстового поля" и про "стили и скрипты разделены на несколько файлов".- тут не моя область, пожалуйста, поправьте, если знаете, что имелось в виду.
Сейчас лично мне такая песочница без надобности, но ещё каких-то лет 5 назад я и мечтать о такой штуке не мог (даже в голову не приходило, если честно). Вдруг кому-то пригодится, а может кого-то подстегнёт запилить что-то своё.
Loggus66
14.10.2021 23:46У Yandex когда-то был проект gixy, который, честно говоря, распространения не увидел, но это интересный проект, который позволил поправить пару ошибок, со сбросом X-Frame-Options, ЕМНИП.
Здесь же ничего особо интересного, потому что - ну, это просто Nginx. Методом тыка можно сделать что-нибудь, как и на обычной его установке. Очень не хватало профайлера, который пошагово объяснял бы, куда идёт запрос на каждом следующем шаге, со ссылками на документацию для каждой из директив. Вот это было бы мощно.
Smashrock
15.10.2021 14:59Да я бы не сказал, что не увидел распространения. На многих зарубежных сайтах на него ссылаются, но да, сам проект не развивается с 2019, если не ошибаюсь. А штука реально классная.
SilverFire
15.10.2021 00:04Идея очень классная и реализация простая.
За счёт netns есть изоляция по сети, но файловая система открыта и можно делать что-то типа
curl file:///etc/passwd
Фильтровать директивы, как предложили в ветке выше - дело неблагодарное в итоге: не зная всех нюансов использования, можно потратить уйму времени и ничего не решить.
SilverFire
15.10.2021 00:07+1Как вариант, запускать в контейнере с ограничением памяти и таймаутом на работу, но не могу оценить насколько от этого пострадает быстродействие по сравнению с текущей реализацией.
bBars Автор
15.10.2021 00:16Да, главная стратегия защиты от злоумышленников в том, что защиты почти нет, но и ценного ничего нет тоже.
А про контейнеры автор писала, что мысли были, но производительность бы слишком пострадала. Хотя и мне кажется, что это нормальный способ. Сейчас вообще многое на каждый чих запускается в контейнерах или даже виртуалках. Но это ресурсоёмкая затея, а сервис этот попроще, обычный инди-проект
myz0ne
15.10.2021 00:39Оно и так в контейнере судя по содержимому на файловой системе (autoindex on). Просто каждый запуск не изолирован от других. Сходу как минимум пара идей на RCE (и потенциально с выход из докера, т.к. скорее всего есть нужные capabilities), но проверять лень. Тот же докерхаб вообще дает из коробке RCE (сборка докер образов) и никто в этом трагедии не видит.
Smashrock
15.10.2021 14:59Я просто использую systemd sandboxing - дешево, сердито и эффективно. Ну и AppArmor для надёжности.
ertaquo
15.10.2021 10:12+1Как вариант, можно парсить конфиг и проверять, нет ли чересчур больших значений для каких-нибудь параметров, которые могут уронить сервер. Есть пара библиотек для Go: [1], [2].
Сам когда-то давно писал парсер конфигов, только на сях. Написано криво-косо, но работает до сих пор :)
hzone
15.10.2021 10:38+1нормальный конфиг nginx состоит из множества файлов.
посему рекомендую допилить песочницу до использования инклюдов.
в качестве инклюдов инклюдить не файлы, а табы, парся их в файлы, уже на стороне апи.
bBars Автор
15.10.2021 14:40А что вы выносите в отдельные файлы? Типа, отдельно ssl, отдельно gzip, отдельно каждый локейшн?
Smashrock
15.10.2021 15:02+1А вот как в этом генераторе - мне нравится, как он раскидывает конфиги. NGINXConfig | DigitalOcean
dmitriym09
15.10.2021 16:54-1А если собрать nginx webassembly и запускать его прямо у клиента в браузере в песочнице?
alexxxst
Ок.
worker_processes 10000000;
bBars Автор
Да, это проблема. Думаю, я бы не успокоился, пока не сделал свой простенький парсер для обрезания таких директив перед стартом. Попробовал - сервис лёг на какое-то время, но спустя минуты 3 опять встал.
Видимо, на этот счёт автор ограничился просьбой жить дружно в FAQ :)
nochkin
Или лучше сразу урезать ресурсы конкретного процесса или пользователя на уровне ОС. Иначе для "простенького парсера" всё предугадать достаточно сложно.
Oxyd
Или плодить процессы в отдельных cgroups с выставлением ограничений по количеству процессов, потребляемой памяти и этому вот всему.