Безопасность у нас — не позднее добавленный слой, а центр архитектуры. Изоляция данных, разделение идентичностей и идемпотентная запись впаяны в самый нижний слой кода — это не отключаемая настройка, а способ работы системы.
Изоляция на уровне строк
Идемпотентная запись
Параметризованный запрос
Изоляция loopback
Изоляция данных
Изоляция на уровне строк — fail-closed
В каждой таблице с областью действия арендатора включён режим PostgreSQL Row-Level Security FORCE; политика привязывает каждую строку к условию «tenant_id = current_setting('app.current_tenant')».
Запрос, пришедший без установленного tenant context, возвращает ноль строк — это даже не ошибка, а тихо пусто. Увидеть данные одной клиники под сессией другой архитектурно невозможно.
Тест кросс-арендной изоляции обязательно зелёный в непрерывной интеграции (CI); если красный — код не сливается.
Атомарная запись
Транзакционно-локальная идентичность, атомарная операция
Идентификатор арендатора устанавливается В САМОМ НАЧАЛЕ каждой операции записи и действует только в пределах этой транзакции; он не закрепляется за пулом соединений и не просачивается в другие запросы.
Многосущностные операции (счёт + позиция + платёж + списание со склада) атомарны внутри одной транзакции: либо всё записывается вместе, либо ничего. Не возникает незавершённой, несогласованной записи.
Права
Ролевые права (RBAC) обеспечиваются на сервере
Контроль прав выполняется на уровне API; клиенту или фронтенду никогда не доверяют. Скрыть меню в браузере — это только про внешний вид, реальное решение — на сервере.
Мутации ядра (Фаза 1) защищены по ролям администратора, администратора-регистратора и врача. Даже если конечную точку вызовет неуполномоченная роль, запрос отклоняется.
Защита от повтора
Идемпотентная запись — двойной операции нет
Каждый запрос записи несёт Idempotency-Key; блокировка на стороне сервера предотвращает вторую операцию с тем же ключом.
Даже при обрыве сети или повторной отправке того же запроса клиентом второй платёж или счёт не создаётся. Результат всегда единственный и предсказуемый.
Все запросы к базе данных — полностью параметризованные выражения, сгенерированные во время компиляции с помощью sqlc. В кодовой базе нет ни одной строки, где SQL строится ручной конкатенацией с пользовательским вводом.
Это структурно устраняет целый класс атак SQL injection — опора не на «фильтрацию» или «экранирование», а на сам способ построения запроса.
Сетевая поверхность
Изоляция loopback — ядро закрыто наружу
Основной API прослушивает только 127.0.0.1 (loopback). Попытка привязки к 0.0.0.0 или публичному IP отклоняется ещё при запуске; ядро ни при каких условиях не открывается в интернет.
Единственная публичная поверхность — это тонкий шлюз (BFF): он не хранит бизнес-логику, кроме сессии, рендеринга и CSRF. Браузер никогда не может напрямую увидеть или адресовать основной API.
Идентификация и сессия
Пароль argon2id, opaque ключ сессии
Пароли хешируются с помощью отраслевого стандарта argon2id, устойчивого к атакам по памяти; простое или слабое хеширование не используется.
Ключи сессии — это непрозрачные (opaque) значения длиной 32 байта, сгенерированные криптостойким генератором; они хранятся в Redis и истекают по TTL. В браузер попадает только cookie с флагами HttpOnly + Secure + SameSite; сам ключ никогда не раскрывается JavaScript.
Аудит
Неизменяемый журнал аудита (append-only)
Важные операции записываются в append-only журнал аудита в ТОЙ ЖЕ транзакции, что и запускающая их мутация. Если операция откатывается, запись аудита тоже не пишется — они неразделимы.
Права UPDATE/DELETE у таблицы аудита отозваны; записи невозможно изменить или удалить впоследствии. Прошлое остаётся как есть.
Privacy
Маскирование чувствительных данных
TCKN, пароль, ключ сессии и медицинские данные ни в каком виде не попадают в журналы. Даже в отладочном выводе эти поля маскируются.
При необходимости чувствительные поля шифруются на уровне колонок. Особые медицинские данные защищены правилами, заложенными в проект с самого начала.
Целостность денег
Безопасность денег — numeric, обратная запись
Деньги везде хранятся как numeric(14,2); расчёты с деньгами через float не выполняются. Округление происходит в одной точке, на сервере — не возникает смещения копеек или разницы округления.
Отмена и возврат обрабатываются как обратная запись вместо удаления записи. Расчётный баланс не пишется вручную; он всегда выводится из суммы счетов и платежей. Прошлое не искажается.
Передача
Безопасность передачи и браузера
Весь трафик передаётся по HTTPS/TLS, и HSTS включён; браузер устанавливает соединение только в зашифрованном виде.
Строгая политика безопасности контента (CSP) блокирует inline-скрипты; X-Frame-Options DENY предотвращает clickjacking, а X-Content-Type-Options nosniff — подмену типов. Также применяются Referrer-Policy и Permissions-Policy.
Важно
Разделение уровней идентификации + отдельная плоскость управления
Пользователь клиники, портал пациента и оператор платформы — это ОТДЕЛЬНЫЕ уровни идентификации: отдельный заголовок, отдельное пространство сессии. Ключ одного уровня не работает на другом — границы полномочий строго разделены.
Точки доступа платформы и администрирования (подготовка арендаторов, выставление счетов, управление автоматизацией/плагинами, генерация ключей API) НЕ ПРИСУТСТВУЮТ в публичном API.
Эти функции работают в отдельной, внутренней плоскости управления с иным паттерном идентификации (специальный административный ключ); она недоступна и необнаружима извне. Так поверхность атаки заметно уменьшается.
КАК ЭТО РАБОТАЕТ
Одна транзакция, изоляция fail-closed
Концептуальная схема ниже показывает, как каждая запись привязывается к арендатору и остаётся идемпотентной. Она приведена исключительно для наглядности.
# 1) Контекст арендатора устанавливается В САМОМ НАЧАЛЕ транзакции (только для этой tx)
BEGIN;
SELECT set_config('app.current_tenant', :session_tenant_id, true);
# 2) Блокировка Idempotency-Key — со тем же ключом повторная операция не создаётсяSELECT lock_idempotency(:idempotency_key);
# 3) Запись нескольких сущностей — все в одной tx атомарноINSERT INTO invoices ...; # RLS: tenant_id покрывается автоматическиINSERT INTO payments ...;
INSERT INTO audit_log ...; # append-only, та же tx
COMMIT;
# Если context не установлен, политика RLS не возвращает ни одной строки:SELECT * FROM patients; --> 0 строк (не ошибка, тихо пусто)
Независимо проверяемо
Часть безопасности передачи можно проверить извне: сертификат HTTPS/TLS вашего соединения, а также заголовки безопасности ответа, такие как HSTS и политика безопасности контента, вы можете изучить сами через инструменты разработчика браузера. Наши утверждения опираются на архитектурные механизмы контроля, а не на маркетинговые фразы.
Есть вопрос об архитектуре безопасности?
Наша команда с радостью обсудит с вами технические детали и ваши требования к соответствию.