- Зачем: - Закрепить реалистичную границу доступа BI и убрать двусмысленность для неизвестных кодов. - Что: - Пояснены доступ к ODS, пояс счётчика и порядок DDL. - Проверка равенства строк отделена от размера конкретной загрузки. - Проверка: - `git diff --check`. - `make config-test` не стартовал: Snap требует работающий `snapd.apparmor`.
210 lines
18 KiB
Markdown
210 lines
18 KiB
Markdown
# ADR 0007. Доступ к ClickHouse: пользователи файлом, доверие нод секретом
|
||
|
||
Дата: 9 августа 2026 года. Статус: принято; роль `monitoring_reader`
|
||
и пользователь `grafana` добавлены 21 августа 2026 года (#114), права
|
||
`bi_reader` расширены до ODS, DDS и DM 25 августа 2026 года (#150).
|
||
|
||
## Решение
|
||
|
||
У кластера семь пользователей и шесть ролей.
|
||
|
||
- `default` — с паролем, только служебный: проверки здоровья нод и работа
|
||
изнутри контейнеров. Приложения им не ходят.
|
||
- `etl` с ролью `etl_writer` — чтение и запись во всех слоях. Им ходят рабочие
|
||
даги Airflow.
|
||
- `lifecycle` с ролью `lifecycle_owner` — создание и удаление только
|
||
прикладных баз и объектов. Им даги жизненного цикла применяют DDL и
|
||
пересоздают мир ([ADR 0013](0013-world-lifecycle.md)).
|
||
- `bi` с ролью `bi_reader` — чтение слоёв ODS, DDS и DM, а также справочников
|
||
`dic`. Им ходит Superset; его запросы обращаются к публичным `_v`, хотя права
|
||
шире.
|
||
- `analyst` с ролью `analyst_reader` — чтение всех слоёв и справочников. Им
|
||
человек подключается снаружи, из своего клиента.
|
||
- `grafana` с ролью `monitoring_reader` — чтение слоёв, таблиц базы `dic` и
|
||
системных таблиц, `CLUSTER` и `REMOTE`. Им ходит Grafana, и больше никто.
|
||
- `dict` с ролью `dict_reader` — чтение одной базы `dic`, пароля нет. Им
|
||
словарь читает свою подложку, и больше он никем не используется
|
||
([ADR 0012](0012-dictionary-home.md)).
|
||
|
||
Роль здесь шире прав на слои: образ ClickHouse требует отдельного разрешения
|
||
на запросы `ON CLUSTER`, на чтение системных таблиц и на каждый движок
|
||
внешнего источника. Без права на системные таблицы клиент вовсе не открывает
|
||
соединение.
|
||
|
||
Пользователи, роли и права объявлены файлами настройки сервера, а не
|
||
запросами SQL. Файл монтируется на обе ноды и лежит в git. Пароли живут
|
||
в `.env` и попадают в настройку подстановкой по имени переменной; значения
|
||
собраны по конвенции «слово-число-слово», потому что пароль `analyst`
|
||
человек набирает руками.
|
||
|
||
Ноды доверяют друг другу по общему секрету, объявленному в описании
|
||
кластера. Учётные данные по репликам не расписываются.
|
||
|
||
Сетевых ограничений на `default` нет: порты стенда и так открыты только на
|
||
`127.0.0.1`, а внутри сети Compose дверь заперта паролем.
|
||
|
||
Раздел README «Состав и доступ» описывает, кто каким пользователем ходит и где
|
||
живут пароли; здесь остаются решение и доводы. Отдельного справочника по
|
||
доступу в `docs/architecture/` нет: точные права видны в `access.xml`, конвенция
|
||
чтения через `_v` описана в доке хранилища.
|
||
|
||
## Почему
|
||
|
||
Стенд учит не администрированию доступа, а чтению отказа в правах: человек
|
||
должен узнать в бою картину, которую видел здесь, и понимать, чего просить.
|
||
Отсюда критерий отбора — модель должна читаться из текста ошибки, а не из
|
||
описания. Поэтому пользователей мало, имена у них говорящие. В бою BI обычно
|
||
читает витрины и модельный слой. ODS ему могут открыть для расследования или
|
||
сверки. Стенд оставляет этот доступ постоянным, но сохраняет две крупные
|
||
границы: `bi` отделён от STG и от записи. Один пользователь может сверить всю
|
||
цепочку DM → DDS → ODS, не получая доступа к сырью и изменению данных.
|
||
Справочники видны всем читателям и никому на запись: зона входит в хранилище
|
||
сбоку, и её содержимым владеет файл репозитория, а не слой.
|
||
|
||
Ограничить `bi` одними `_v` поверх `Distributed` можно только отдельным
|
||
маршрутом, учётной записью или копией данных. Эта механика не даёт нового урока.
|
||
Публичное представление остаётся договором чтения; границу прав оно не задаёт
|
||
([исследование](../research/2026-08-25-clickhouse-distributed-view-security.md)).
|
||
|
||
**Беспарольный `dict`.** Пароль пришлось бы вписать в текст DDL словаря, то
|
||
есть положить секрет в git. Ходить этой учёткой некуда: право у неё одно —
|
||
`SELECT` на `dic`, и хост источника локальный.
|
||
|
||
**Своя роль дашбордам.** Панели мониторинга живут не на витринах, а на
|
||
`system.query_log`, частях MergeTree и очереди распределённых DDL — и читают
|
||
их через `clusterAllReplicas`, потому что системные таблицы у каждой ноды свои
|
||
([ADR 0002](0002-monitoring-scope.md)). Для этого нужны `SELECT` на `system.*`
|
||
и `REMOTE`; контракт роли также включает `CLUSTER` для запросов `ON CLUSTER`.
|
||
Такого набора не было ни у одного читателя: у обеих читающих ролей из
|
||
системных таблиц только `system.settings`. Догрузить
|
||
недостающее в `analyst_reader` отвергнуто: аналитик получил бы системные
|
||
таблицы и `REMOTE` заодно, а в `system.query_log` запросы Grafana стали бы
|
||
неотличимы от человеческих — ровно там, где дашборд «Запросы» и должен их
|
||
различать. Пускать Grafana под `etl` отвергнуто сразу: читатель не получает
|
||
права писать. Осталась своя роль по здешней общей схеме — у каждого
|
||
потребителя она своя.
|
||
|
||
**Секрет, а не учётные данные по репликам.** У каждой реплики в описании
|
||
кластера своя учётка — вписанная явно или подразумеваемая, и тогда это
|
||
беспарольный `default`. Подключение к соседней ноде идёт от неё, а не от
|
||
того, кто задал вопрос. Значит, стоит `default` получить пароль — и без
|
||
правки описания кластера межнодовые запросы встают. Починить это можно двумя
|
||
способами, и они различаются не удобством. С учётными данными по репликам вся
|
||
межшардовая работа идёт от одной общей учётки: на своей ноде права
|
||
применяются, на соседней — нет. Ролевая модель тогда наполовину декорация, а
|
||
это хуже её отсутствия: она утверждает разграничение, которого не существует.
|
||
Общий секрет передаёт на соседа личность инициатора, стоит одной строки в
|
||
описании кластера и не размножает пароль по репликам. Шифрование тут ни при
|
||
чём; в соседнем `clickhouse-learning-cluster` выбран как раз путь с учётными
|
||
данными по репликам, и там пароль вписан в каждую реплику каждого из трёх
|
||
кластеров.
|
||
|
||
Довод подтверждён замером 13 августа 2026 года: на соседнем шарде сохранился
|
||
пользователь-инициатор, поэтому там применились его права. Результат замера —
|
||
в разделе «Что проверено».
|
||
|
||
**Файлы, а не SQL.** Объявленное файлом не оставляет состояния в томах:
|
||
источник истины — файл в git. Стенд считает `access.xml` и переданные через
|
||
`.env` значения настройкой при создании контейнера; отдельного способа
|
||
применять их на живом стенде нет, потому что уроки не меняют базовую модель
|
||
доступа. Порядок применения изменений — в разделе README «Состав и доступ».
|
||
Оба варианта с SQL — локальное хранилище доступа с `ON CLUSTER` и
|
||
реплицированное в keeper — делают пользователей состоянием тома: `.env` на
|
||
них больше не влияет, и появляется расхождение того же сорта, которое README
|
||
уже описывает для Postgres и Grafana. Цена выбора известна: объявленных файлом
|
||
пользователей нельзя менять запросами. Она принята как упрощение — сломать
|
||
модель случайно труднее.
|
||
|
||
**Что ещё отвергнуто.** Оставить всё на беспарольном `default`
|
||
— цена нулевая, но и рассказать по итогам нечего. Раздать всем пароль без
|
||
ролей — дёшево и показывает ровно тот антипаттерн, за который ругают в бою:
|
||
все под одним админом. Вынести доступ в лекцию, как репликационную
|
||
эксплуатацию, — довод «отдельный операционный домен» здесь не работает:
|
||
у репликации это живые процессы, учения и runbook, а тут несколько десятков
|
||
строк настройки и ноль эксплуатации. Запереть `default` ещё и по сети —
|
||
второй замок на ту же дверь.
|
||
|
||
**Отложено явно**: TLS и защищённые межсерверные соединения, настоящее
|
||
хранилище секретов, внешние поставщики учётных записей, квоты и профили под
|
||
пользователя, политики строк, аудит запросов.
|
||
|
||
## Следствия
|
||
|
||
Новые таблицы ODS, DDS и DM не требуют отдельного гранта для `bi_reader`.
|
||
Явный договор `_v` защищает потребителя от устройства хранения и случайных
|
||
новых колонок.
|
||
|
||
DDL для матвью приёма выполняется от имени `lifecycle`. Это видно в журнале
|
||
запросов, а при создании ClickHouse проверяет у этой учётки чтение источника и
|
||
вставку в цель. Так отдельная учётка показывает границу между редким
|
||
управлением структурой и повседневной записью `etl`, не выдавая рабочим дагам
|
||
право удалять базы.
|
||
|
||
Лаба «сломать права» вариантом с файлами не закрывается: создать через SQL
|
||
новую роль и нового пользователя ничто не мешает, и новую роль разрешено
|
||
выдавать даже пользователю, объявленному файлом. Недоступно обратное —
|
||
править объявленных файлом. Всё, что лаба создаст, живёт в томе и убирается
|
||
`make clean`.
|
||
|
||
## Что проверено
|
||
|
||
Сверено 9 августа 2026 года по документации ClickHouse через MCP Context7 и
|
||
на живом стенде 26.3.17.56.
|
||
|
||
- В `system.clusters` у каждой реплики своя колонка `user`; на стенде там
|
||
`default`. Отсюда весь довод про общий секрет.
|
||
- Секрет кластера аутентифицирует серверы хешем от соли, нонса, секрета и
|
||
запроса и позволяет назначить пользователя, от которого запрос выполняется
|
||
на соседней ноде.
|
||
- Роли и права объявляются в файле настройки пользователей наравне с самими
|
||
пользователями; SQL-хранилище доступа для этого не требуется.
|
||
- Хранилище пользователей из файла — только для чтения: попытка изменить
|
||
такого пользователя запросом даёт `ACCESS_STORAGE_READONLY`.
|
||
- Роль, созданную через SQL, разрешено выдавать пользователю из файла —
|
||
отдельное улучшение ClickHouse 2025 года, в 26.3 оно есть.
|
||
- Подстановка значений из переменных окружения — общий механизм файлов
|
||
настройки ClickHouse.
|
||
- Весь DDL репозитория идёт через `IF NOT EXISTS` (файлы `sql/ddl/`).
|
||
|
||
Права роли `monitoring_reader` сверены 21 августа 2026 года по документации
|
||
ClickHouse через MCP Context7 (#114).
|
||
|
||
- Системные таблицы выдаются обычным `SELECT`: в документации это
|
||
`GRANT SELECT ON system.query_log`, значит `SELECT ON system.*` и есть «все
|
||
системные таблицы». Отдельного права на них нет.
|
||
- `GRANT CLUSTER ON *.*` — документированная форма права на запросы
|
||
`ON CLUSTER`; текущий запрос `clusterAllReplicas` его не использует.
|
||
- `clusterAllReplicas` документация держит в одном ряду с `remote`,
|
||
`remoteSecure` и `cluster`: это табличная функция, которая берёт имя
|
||
кластера из `remote_servers` и идёт на каждую его реплику. Отсюда `REMOTE` —
|
||
без права на удалённое обращение запрос дальше своей ноды не пойдёт, а
|
||
панели ADR 0002 читают `system.*` именно так.
|
||
|
||
Набор проверен на живом стенде 21 августа 2026 года (#114): пользователь
|
||
`grafana` из Grafana выполнил
|
||
`SELECT count() AS replicas FROM clusterAllReplicas('clickstream_cluster', system.one)`
|
||
и получил 2 — прав хватает, чтобы прочесть системную таблицу с обеих нод.
|
||
|
||
Проверено на живом стенде 13 августа 2026 года, ClickHouse 26.3.17.56.
|
||
|
||
- Секрет кластера принимается внутри `remote_servers` в виде
|
||
`<secret from_env="CLICKHOUSE_CLUSTER_SECRET"/>`. Запрос от `etl` через обе
|
||
ноды выполнился на каждой под пользователем `etl`.
|
||
- `from_env` работает у элемента `password`: `etl`, `lifecycle`, `bi` и
|
||
`analyst` вошли с паролями из окружения и получили свои роли.
|
||
- При запросе от `bi` через Distributed на второй ноде `system.query_log`
|
||
показал `user = bi` и `initial_user = bi`. Значит, на соседнем шарде
|
||
применяются права инициатора, а не общей учётной записи.
|
||
- Права на отсутствующие базы `dds` и `dm` приняты. До создания баз они видны
|
||
в `SHOW GRANTS FOR etl_writer`; временная таблица в `dm` подтвердила право
|
||
`bi` на чтение после появления базы.
|
||
|
||
Расширение `bi_reader` проверено на живом стенде 25 августа 2026 года (#150):
|
||
|
||
- `SHOW GRANTS FOR bi_reader` содержит `SELECT` на ODS, DDS и DM;
|
||
- `bi` прочитал одинаковое число строк из `ods.event_v` и `dds.event_v`;
|
||
- чтение `stg.hits_raw_rep` завершилось ошибкой `ACCESS_DENIED`;
|
||
- представление над `Distributed` с `SQL SECURITY DEFINER` на удалённой ноде
|
||
сохранило исходного пользователя `bi`. Причина и отклонённые варианты
|
||
разобраны в [исследовании](../research/2026-08-25-clickhouse-distributed-view-security.md).
|