# ADR 0007. Доступ к ClickHouse: пользователи файлом, доверие нод секретом Дата: 9 августа 2026 года. Статус: принято; роль `monitoring_reader` и пользователь `grafana` добавлены 21 августа 2026 года (#114), права `bi_reader` расширены до ODS, DDS и DM 25 августа 2026 года (#150). Пользователь `student` и роль `student_writer` добавлены 22 сентября 2026 года. ## Решение У кластера восемь пользователей и семь ролей. - `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` — чтение всех слоёв и справочников. Им человек подключается снаружи, из своего клиента. - `student` с ролями `analyst_reader` и `student_writer` — то же чтение и работа со своими таблицами в `default`: создание, вставка и удаление. Сохранение учебных наблюдений не требует прав технической учётной записи `etl` на изменение слоёв. Таблицы локальные; работа ведётся на одной ноде. - `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`. ## Что проверено Права `student` сверены 22 сентября 2026 года через MCP Context7 и на стенде 26.3.17.56: создание локальных `MergeTree`, `INSERT SELECT` из DDS и DM, чтение и удаление своих таблиц работают. На обеих нодах вставка в STG, ODS, DDS, DM и `dic` завершается `ACCESS_DENIED`. Сверено 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` в виде ``. Запрос от `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).