LINUX.ORG.RU
ФорумTalks

SQL-клиент не на ноутбуке, а на своём сервере: четыре проблемы, о которых не предупреждают

 , , , ,


0

2

Настольный клиент к БД предъявляет два требования, которые обычно нигде не записаны. Первое: с каждого ноутбука разработчика должна быть сетевая достижимость до продовой сети. Второе: на каждом ноутбуке должна лежать копия всех учётных данных. Оба требования несущие, и именно из-за них клиент к базе оказывается последним инструментом, который команда заводит за SSO.

Если перенести сам клиент на сервер рядом с данными, оба требования исчезают. Пароль перестаёт жить на шестидесяти ноутбуках и живёт в одном месте. Доступ по сети перестаёт быть выдачей VPN на человека и становится вопросом топологии развёртывания: до базы ходит один хост, до этого хоста ходят люди, каждый со своей ролью.

Размен того стоит, но он не бесплатный. Появляются четыре проблемы, которых у настольного клиента не было, потому что клиент теперь многопользовательский сетевой сервис, держащий все подключения команды. Мы прошли через все четыре, пока делали LibreDB Studio, самостоятельно размещаемую IDE для баз данных, работающую в браузере. Ниже конкретная форма каждой проблемы, включая две, которые мы сначала решили неправильно.

Что это в терминах установки

Это обычный сервис на своём железе, без внешних зависимостей и без обращения куда-либо наружу:

  • .deb и .rpm со своим приватным Node внутри, юнит systemd, никаких требований к системному node;
  • AppImage и Snap, если нужно поднять на рабочей станции или на машине без пакетного репозитория;
  • tarball для linux amd64 и arm64;
  • образ ghcr.io/libredb/libredb-studio и Helm chart с оператором, если хозяйство уже в Kubernetes;
  • npm-пакет, если удобнее запускать своим рантаймом.

Состояние (подключения, вкладки, история запросов) хранится либо в файлах, либо в SQLite, либо в PostgreSQL, выбирается одной переменной окружения. Аутентификация: локальные пароли или OIDC через PKCE. Лицензия MIT, сборки воспроизводятся из тега, к релизу прикладывается SBOM.

Дальше про то, что ломается при переносе.


1. Граница доверия переезжает внутрь вашего же процесса

На ноутбуке границей авторизации является пользователь операционной системы. На сервере все подключения держит один процесс, и кто-то внутри этого процесса должен решать, кто именно спрашивает.

Очевидный ответ: аутентифицирующий край, то есть middleware, которое отрабатывает перед каждым маршрутом, проверяет cookie сессии и заворачивает остальных. Такое у нас есть. Стоит проговорить вслух другое: это не граница.

Middleware в src/proxy.ts действительно заворачивает неаутентифицированные запросы, но это оптимизация, а не граница авторизации. Дырка в матчере (матчер освобождает любой путь, в котором есть точка) или обход на уровне фреймворка открывают все маршруты, которые полагаются только на него.

Поэтому каждый маршрут, который дотягивается до базы или до провайдера модели, проверяет вызывающего повторно, через один общий guard. Край существует, чтобы типовой случай был дешёвым, а не чтобы ему доверяли.

Разница становится видна в первый же момент, когда легитимный вызов не может предъявить сессию. У нашего агентного рантайма есть колбэк, который просит сервер подхватить долгоиграющую сессию запроса. Вызывающая сторона это транспорт, а не человек, и cookie у неё нет по построению. Соблазнительное решение: добавить путь в список публичных. Оно неверно структурно, потому что исключение по пути имеет форму пути, а значит внутрь попадает всё, что смогло достучаться до порта.

Вместо этого вызывающая сторона предъявляет удостоверение, которое сервер выпустил сам: годное одну минуту, называющее ровно один запуск, не дающее ничего кроме продолжения этого запуска, и подписанное ключом, производным от JWT-секрета, а не самим JWT-секретом. Это не сессия, и превратить его в сессию нельзя. Middleware пускает такой путь только при валидном удостоверении, а маршрут всё равно проверяет его второй раз.


2. CSRF становится настоящим, а обратный прокси молча ломает защиту

У настольного клиента нет cookie и нет фоновых полномочий. У браузерного есть и то и другое, поэтому изменяющим состояние запросам нужен второй слой поверх сессионной cookie. Мы сравниваем Origin запроса с собственным хостом развёртывания.

Два решения внутри этой проверки стоит скопировать, и оба они не про атакующего, а про ложные срабатывания.

Сравнение только по хосту, схема игнорируется. Прокси, который терминирует TLS и передаёт дальше обычный HTTP, не выставив x-forwarded-proto, приводит к тому, что браузер шлёт Origin: https://db.example.com, а приложение вычисляет http://db.example.com. Сравнение схем в этом месте запирает администратора снаружи его собственной формы логина. Мы теряем защиту от страницы по http на том же хосте, постящей в приложение по https, а для этого нужен активный сетевой атакующий, который уже сломал транспорт. Это меньшая угроза, чем целый класс развёртываний, который иначе перестаёт работать.

Запрос без Origin и без Referer принимается, но только если content type равен application/json. Выглядит как выключатель, протащенный под чужим именем. Это не так, по двум независимым причинам. HTML-форма умеет отправляться только как x-www-form-urlencoded, multipart/form-data или text/plain, потому что четвёртого значения у enctype нет, то есть классический вектор CSRF структурно неспособен породить такую форму запроса. А межсайтовый fetch() такой content type выставить может, но application/json не входит в CORS-safelist, значит он вынуждает preflight, а развёртывание, которое нигде не отвечает заголовком Access-Control-Allow-*, никогда не отвечает на него утвердительно. Остаются curl и вызовы сервер-сервер, а это не CSRF: CSRF это именно ничего не подозревающий браузер, несущий полномочия, которые он не выбирал отправлять.

Теперь про ошибку, которую мы сначала выкатили. Матчер middleware исключал api/db/health, чтобы пробы балансировщика не проходили весь конвейер. По тому же пути живёт POST /api/db/health, детальная проверка конкретного подключения, закрытая сессией. Исключение пути исключило и метод, и единственным маршрутом без проверки origin оказался тот, который называется health. Исключения по пути слепы к методу, и это тот же урок, что и в первой проблеме, только в другом костюме.

Второе, чему научил этот слой: блокировка обязана уметь диагностировать сама себя. Прокси, который переписывает Host, не выставив x-forwarded-host, даёт несовпадение на каждом изменяющем состояние запросе, включая логин, и администратор видит работающую страницу, которая молча отказывает во всём. Поэтому тело ответа 403 называет починку: выставьте ALLOWED_ORIGINS в публичный origin развёртывания. Ошибка, объясняющая собственную причину, здесь ценнее короткой.


3. Логирование отказов превращается в поверхность отказа в обслуживании

Каждая ветка выше пишет строку в лог и событие в аудит. В интернете это означает, что неаутентифицированный сканер способен забить том с логами контейнера запросами, про которые он заранее знает, что они не пройдут, и вытеснить настоящие события из кольцевого буфера фиксированного размера, который читает административный интерфейс.

Поэтому отказы лимитируются. Сам отказ не лимитируется никогда, лимитируется только его запись.

Что нас удивило: ограничение количества строк не ограничивает размер строки. В строку лога попадает путь запроса и наблюдаемый хост, оба под контролем атакующего и оба произвольной длины. Лимит на количество без лимита на размер это та же атака в другом масштабе, поэтому каждое поле ещё и обрезается.

Есть и тонкость с ключом. Для отказа вида «аутентифицированный пользователь постучался в административный маршрут» лимитировать по IP неправильно: наличие токена ограничивает количество личностей, дошедших до этой ветки, а не количество запросов от каждой, и одна сессия способна долбить в цикле. Эта корзина ключуется по имени пользователя, так что ротация X-Forwarded-For лишних строк не приносит.


4. Состояние тоже переезжает, и это самая честная часть

Раньше хранилищем был браузер. Подключения, вкладки и история запросов жили в localStorage, что является правильным умолчанием для одного разработчика и бесполезно в ту же секунду, когда развёртывание делят двое. Серверный режим включается одной переменной окружения: чтение по-прежнему идёт из localStorage как из сквозного кэша, а изменения уезжают в серверное хранилище, разделённое по пользователям, с шифрованием учётных данных подключений на диске.

Что покупает это шифрование: украденный файл базы или дамп. Хранилищем секретов оно не является, потому что тот, кто может прочитать окружение сервера, может прочитать и ключ. И браузерная копия ваших учётных данных всё ещё лежит открытым текстом, потому что её шифрование потребовало бы мастер-пароля и процедуры восстановления, а это меняет то, чем является продукт. Именно из-за этого признания меры против межсайтового скриптинга у нас самые важные строки в описании модели безопасности, а не галочка.


Чего это не даёт

Перенос клиента сужает радиус поражения. Он не создаёт модель авторизации, которую вы не написали. В Studio две роли, и пользователь с ролью user по-прежнему может подключиться к любому хосту и порту и выполнить любой оператор: списки разрешённых целей и покомандные ограничения по провайдерам это осмысленное направление, и оно не реализовано. Написать об этом в документации, рядом с тем, что реализовано, оказалось важнее любой отдельно взятой меры.

Страница с описанием модели безопасности проверяется против репозитория на каждой сборке. Строка, называющая несуществующий файл или тест, который не запускается, роняет CI. Проверить, что связанный тест утверждает правду, она не может, и этот остаток мы несём сознательно, и он тоже записан.

LibreDB Studio распространяется под лицензией MIT: https://github.com/libredb/libredb-studio

Перемещено dataman из development

От того, что ты вставишь между клиентом и базой ещё одну прокладку (твой «промежуточный клиент на сервере»), в большинстве случаев никакие аспекты безопасности не поменяются. Клиент (настоящий, расположенный рядом с человеком который им пользуется) в любо случае будет куда-то логиниться и слать туда запросы. Логинится ли он напрямую в базу, или же подключается к твоему посреднику - особо ничего не меняет.

Стену текста не читал, там сплошная вода, возможно ещё и сгенерированная.

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 1)

тело ответа 403 называет починку

На этом месте мой парсер хрюкнул и сдох, соответственно, дальше я не читал.

Nervous ★★★★★
()
Ответ на: комментарий от firkax

возможно ещё и сгенерированная

Не возможно, а однозначно %)

Nervous ★★★★★
()

интересно, какого хрена нынче стало модно писать в стиле нейрослопа? Я вот вижу какие-то слова, вроде даже русские, но как будто бы они не для человека написаны, а для машины. По мне так, даже если и не КГ, то уж во всяком случае АМ.

demidrol ★★★★★
()
Ответ на: комментарий от demidrol

Я вот вижу какие-то слова, вроде даже русские, но как будто бы они не для человека написаны, а для машины

Есть такое. Последнее время, читая ии-шные тексты, ощущаю себя дислексиком.

troizet
()
Ответ на: комментарий от demidrol

Думается, это не «в стиле», а слоп и есть.

firkax ★★★★★
()
Ответ на: комментарий от demidrol

Оба требования несущие

Мне эта фраза бросилась в глаза. По-русски так обычно не пишут. Зато это очень напоминает перевод слова «load bearing». Это слово печально известно тем, что LLM от Anthropic обожает его вставлять к месту и не к месту.

Поэтому полагаю, что текст был сгенерирован с помощью Claude на английском языке и потом был осуществлён машинный перевод на русский язык.

vbr ★★★★★
()

А вообще если кому-то нужен веб-клиент к БД, могу порекомендовать Cloud Beaver. Настраивается очень муторно, но когда настроен - работает нормально, проблем с ним не было.

vbr ★★★★★
()

аутентифицирующий край, то есть middleware

Мне это резануло. Прослойка же.

Но есть ли смысл это печатать, когда сам автор не читал, что пишет.

bbc69
()
Ответ на: комментарий от Nervous

«корзина ключуется по имени пользователя».

thesis ★★★★★
()

перестань ходить в базу клиентами. ходи только миграциями из кода.

bvn13 ★★★★★
()

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

mshewzov ★★★★
()

Ёкарная дребедень!

vM ★★★
()
Вы не можете добавлять комментарии в эту тему: только для зарегистрированных, score>=50.