# Кабинет клиента: безопасность, связь без публичного адреса, покрытие данных Отчёт на 17.09.2026. Всё перечисленное выкачено на astramost и влито в `main` (коммит 2a7442c). ## 1. Как кабинет подключается к ядру Настройки → «Личный кабинет» → «Кабинет клиента на своём сервере»: 1. создать ключ доступа (на странице он показан замаскированным, целиком попадает только в архив); 2. выбрать способ связи; 3. скачать архив, распаковать на сервере кабинета, выполнить `sudo ./install.sh`; 4. опубликовать через nginx с сертификатом (пример в README архива). Инструкция с шагами — «Инструкции» → «Личный кабинет» (`/admin/help/lk`). Два способа связи: | Способ | Когда | Что нужно | |---|---|---| | Кабинет обращается к ядру (`BJ_MODE=direct`) | у ядра есть адрес, доступный серверу кабинета | адрес ядра в настройках, только https | | Ядро подключается к кабинету (`BJ_MODE=agent`) | у ядра нет адреса в интернете | адрес кабинета в настройках ядра; входящих портов не нужно | Во втором способе кабинет копит запросы клиентов, ядро само дозванивается до кабинета по его доменному имени (4 параллельных подключения), выполняет запрос тем же обработчиком `/api/lk/*` и возвращает ответ. Канал в обе стороны закрыт ключом модуля; ядро выполняет только пути `/api/lk/…`. Состояние канала видно в настройках: подключено ли, время последнего обмена, число обслуженных запросов. ## 2. Проверка безопасности Аудит нашёл 5 критичных, 12 важных и 9 мелких замечаний. Исправлено: **Критичные** - перебор пароля без ограничений → лимит по адресу посетителя (10) и по логину (8) на 15 минут, одинаковая задержка ответа; проверено вживую: блокировка на 8-й попытке; - живой токен сотрудника лежал в куке, читаемой скриптами → все куки `HttpOnly`; - CSRF отсутствовал, `/impersonate` срабатывал по ссылке → токен на всех формах, вход сотрудника в кабинет клиента только POST, `SameSite=Strict`; - куки без `Secure` → ставится всегда, кроме обращения на localhost; - сессии и паспортные данные клиентов лежали на диске ядра открытым текстом → в файле теперь отпечаток токена (sha256) и ссылка на учётную запись, персональных данных нет. **Важные** - выход гасит все сессии, включая сохранённую сессию сотрудника; возврат из кабинета клиента гасит сессию просмотра в ядре; - карточка клиента читается из БД на каждый запрос: блокировка и снятие прав действуют сразу; - абсолютный срок сессии 24 ч (скользящий — 12 ч), привязка к браузеру; привязка к сети — настройкой, по умолчанию выключена (мобильных клиентов иначе выбрасывает); - вход сотрудника в кабинет клиента пишется в `lk.audit` и поднимает оповещение; - заголовки CSP / X-Frame-Options / nosniff / Referrer-Policy / HSTS, запрет кэширования страниц; - таймауты сервера кабинета, ограничение тела запроса 1 МБ, перехват паники; - предупреждение в журнале, если адрес ядра задан по открытому http; - ключ модуля и токен сессии: при отказе источника случайных чисел — ошибка, а не суррогат; - ключ на странице настроек замаскирован. **Мелкие** - одинаковое время ответа для несуществующего логина (защита от перечисления логинов); - пароль от 10 символов, буквы + цифра или знак; - сообщения и ошибки больше не едут в адресе страницы (не попадают в журнал nginx); - шрифты вшиты в программу: страница не обращается ни к одному стороннему сайту; - экранирование `% _ [` в выборках по номеру договора; - `/health` без ключа отдаёт только `{"ok":true}`. Заодно найдена и исправлена ошибка: текст «неверный логин или пароль» клиенту не показывался вовсе (шаблон ждал поле `Error`, обработчик передавал `Flash`). **Осталось осознанно** - боевой ключ лежит внутри скачиваемого архива — иначе установка не одной командой. В README и на странице настроек стоит предупреждение удалить архив после установки. Вариант на будущее — одноразовый код привязки вместо ключа; - список клиентов у сотрудника ищется через `GET /clients?search=…` — ФИО попадает в журнал nginx сервера кабинета. Лечится настройкой nginx (не писать query) либо переводом поиска на POST; - файл сессий переписывается целиком при входе/выходе — при тысячах одновременных сессий стоит перейти на отложенную запись. **Что уже было сделано правильно:** bcrypt, выборка данных строго по договору из серверной сессии (подменой запроса чужие данные не получить), право сотрудника проверяется в ядре, сравнение ключа за постоянное время, параметризованный SQL, `html/template` без небезопасных вставок, поручения сверяются с фактическим остатком, кабинет ничего не хранит на диске. ### Тесты | Набор | Что проверяет | Результат | |---|---|---| | `go test ./...` | весь проект | проходит | | `go test -race ./modules/lk ./internal/lkgateway` | гонки в очереди режима ожидания и сессиях | проходит | | `lksecurity_test.go` | файл сессий без токена и ПДн, права 0600, привязка к устройству, абсолютный срок, только просмотр у сотрудника, API без ключа/с чужим ключом/без сессии, перебор, пароли, LIKE | 6 из 6 | | `lkagent_test.go` | настоящий кабинет + настоящий цикл ядра; чужой путь через канал не выполняется | 2 из 2 | | `modules/lk` | CSRF, заголовки, запрет GET `/impersonate`, режим ожидания, страницы | проходит | | `scripts/lk-sec-smoke.sh` | работающий кабинет снаружи: заголовки, куки, страницы без входа, формы без токена, обход каталога, большое тело, поддельные куки | 34 из 34 | | живые (`-tags live`, только чтение) | история, цены, карточка бумаги, сверка с DEPO, покрытие данных | проходит | Запуск проверки снаружи: `scripts/lk-sec-smoke.sh https://адрес-кабинета`. ## 3. Какие данные кабинет показывает сейчас Выборка — 60 реальных договоров, операции и доходы за 2 года: | Раздел | Источник | Данные есть | |---|---|---| | Портфель (остатки) | DEPO `OD_RESTS` | 60 из 60 клиентов, 143 позиции | | Оценка по рынку | экспорт QUIK `Securities`+`Params` | 77 из 143 позиций (54 %) | | Операции по счёту депо | DEPO `U_OP_INV_*` | 58 из 60, 1278 строк | | Доходы и налог | DEPO `U_OP_INF_INCOME`, `OD_NDFL` | 26 из 60, 350 строк | | Корпоративные действия | DEPO `U_CA_ACTS` | 2 из 60 | | Готовые выписки | DEPO `U_OP_INF_EXTRACT` | 10 из 60, 29 документов | Почему оценено только 54 % позиций — это не ошибка запроса: - 31 позиция — иностранные бумаги (ISIN US/XS/GB/GG), на Мосбирже не торгуются; - 35 позиций — внебиржевые выпуски классов `FONT_OUT` / `FONT_OUT2` (в основном бумаги группы Гарант-Инвест), рыночной цены у них нет. Предложение: для облигаций без рыночной цены показывать оценку по номиналу с пометкой «по номиналу», для иностранных — строку без оценки с пояснением. Решение о способе оценки — за депозитарием. ## 4. Что показывают кабинеты брокеров и что из этого нам уже доступно В экспорте QUIK лежат данные, которые кабинет пока не использует. Код клиента QUIK сопоставляется с договором напрямую: `BR746` ↔ `БР-746`, `BRIIS514` ↔ ИИС, суффикс `OUT` — внебиржевой раздел. | Что обычно есть у брокеров | Есть ли у нас данные | Источник | Оценка работы | |---|---|---|---| | Денежные средства по валютам | **да**: рубли у 998 клиентов, юани у 18, золото у 6 | `MoneyLimits` | малая | | Средняя цена покупки, прибыль/убыток по позиции | **да**: заполнено у 2439 из 2528 позиций (96 %) | `DepoLimits.AwgPositionPrice` | малая | | Стоимость портфеля одной цифрой | **да**: у 1104 клиентов | `Portfolio.PortfolioValue` | малая | | Структура портфеля (акции/облигации/фонды, валюты, эмитенты) | **да**, считается из уже загруженного | позиции + цены + CFI | малая | | Календарь выплат (купоны, оферты, погашения по своим бумагам) | **да** | DEPO `OD_COUPONS`, `OD_OFFERS` | малая | | Сделки за день | **да**: 1692 сделки, 87 клиентов | `Trades` | малая | | Активные заявки | **да**: 1323 заявки, 94 клиента | `Orders` | малая | | История сделок за период | **частично**: в экспорте только текущий день | `Trades` + накопление, как сделано для НПР | средняя | | График стоимости портфеля во времени | **нет истории**: нужно начать копить ежедневный срез | новая таблица срезов | средняя | | Маржинальные показатели (НПР1/НПР2, УДС, плечо) | **да** | `Portfolio`, схема `npr` | малая | | Признак ИИС, статус квалифицированного инвестора | **да** | префикс договора, DEPO `IS_QUAL` | малая | | Налоги: удержанный НДФЛ | **уже показываем** | DEPO `OD_NDFL` | — | | Справки и отчёты | **уже показываем** + заявка на документ | DEPO, `lk.doc_requests` | — | | Доходность в процентах годовых | **нет**: нужны вводы/выводы денег за период | SPECTRE или накопление | большая | | Ввод/вывод денег, пополнение | нет — это операции брокера, не депозитария | — | вне модуля | | Торговля из кабинета | нет — для этого есть QUIK | — | вне модуля | Рекомендуемый порядок: деньги → средняя цена и прибыль/убыток → структура портфеля → календарь выплат → сделки за день. Всё это — малые работы на уже включённых пакетах экспорта. Для денег и сделок нужно решение: показывать ли брокерские данные в кабинете депозитария (технически готово, вопрос в продукте и в том, какие клиенты это должны видеть). ## 5. На что посмотреть завтра - настройки → «Личный кабинет»: новый блок связи, замаскированный ключ, кнопка архива; - кабинет `http://…:8099`: вход (теперь с сообщением об ошибке), «Документы», блок корпоративных действий на «Доходах»; - после выкатки все клиентские сессии сброшены (сменился формат хранения) — войти заново; - пароли короче 10 символов завести больше нельзя; существующие учётные записи продолжают работать; - в корне репозитория лежит старый `s.html` (сохранённая страница настроек от 14.08) — в коммит не включён; в таких страницах бывают ключи, файл стоит удалить.