22. RBAC в БД
Введение
В реальном мире никому не придет в голову выдать мастер-ключ от всех сейфов банка каждому стажеру или водителю инкассаторской машины. Инженерный допуск строго разграничен: кассир имеет доступ к операционной кассе, кладовщик — к инвентарному складу, а доступ к главному хранилищу требует совместного присутствия двух старших офицеров. В архитектуре программного обеспечения этот фундаментальный принцип разделения полномочий называется управлением доступом на основе ролей (Role-Based Access Control, RBAC).
В микросервисной архитектуре на Go десятки независимых сервисов взаимодействуют с общей реляционной СУБД. Подключение всех микросервисов под единой учетной записью с широкими правами — это колоссальный архитектурный просчет. Если злоумышленник находит уязвимость типа SQL-инъекции (21. SQL Injection) в сервисе формирования чеков или компрометирует его токены в Kubernetes, он автоматически получает доступ ко всей корпоративной базе данных: финансовым транзакциям, персональным данным клиентов и системным каталогам.
RBAC превращает базу данных в многосекционную герметичную конструкцию с отсеками (Bulkhead Pattern). Если один сервис скомпрометирован, радиус поражения (blast radius) строго ограничен рамками его роли: сервис заказов может читать и обновлять таблицу orders, но физически не имеет прав на чтение хэшей паролей в таблице users; сервис аналитики читает исключительно реплику базы данных в режиме read-only; а опасные операции изменения структуры таблиц (DDL) разрешены только специализированной роли migrator, запускаемой изолированно в пайплайне CI/CD.
В этой главе мы разберем:
- Как устроена ролевая модель RBAC и механизмы наследования привилегий.
- Внутреннее устройство подсистемы прав PostgreSQL (
pg_catalog, списки контроля доступаrelacl, фазы планировщикаsetrefs.c). - Механику фильтрации строк на уровне ядра СУБД с помощью Row-Level Security (RLS).
- Безопасную интеграцию с Go: паттерны разделения пулов
ROPool/RWPool, проведение миграций и динамическое переключение ролей. - Цену безопасности на аппаратном уровне (Mechanical Sympathy): кэширование ACL и накладные расходы предикатов RLS.
Модель RBAC: роли, привилегии и наследование
Классическая модель RBAC в реляционных базах данных строится на трех базовых концепциях:
- Пользователь / Субъект (User / Role): Учетная запись, под которой клиентское приложение или DBA устанавливает TCP-соединение с сервером СУБД. В современных версиях PostgreSQL понятия «пользователь» и «группа» унифицированы: любая учетная запись создается единой командой
CREATE ROLE, а флагWITH LOGINопределяет, разрешено ли под ней аутентифицироваться. - Роль (Role): Именованный контейнер привилегий. Роли можно объединять в иерархические деревья, назначая одни роли другим.
- Привилегия (Privilege): Атомарное право на выполнение конкретной операции над объектом базы данных (таблицей, представлением, схемой, функцией или генератором последовательностей
SEQUENCE). Ключевые привилегии:SELECT,INSERT,UPDATE,DELETE,EXECUTE,USAGE.
Механизм наследования ролей (INHERIT) позволяет строить элегантные, масштабируемые графы прав: базовая роль readonly получает права SELECT на схемы, роль readwrite наследует readonly и добавляет права модификации данных, а конечные сервисные аккаунты наследуют нужный уровень доступа.
flowchart TD
Admin["admin<br>полный доступ"] --> AppOwner["app_owner<br>владелец схемы"]
AppOwner --> AppReadWrite["app_readwrite<br>чтение и запись"]
AppOwner --> AppReadOnly["app_readonly<br>только чтение"]
AppReadWrite --> SvcOrders["svc_orders<br>сервис заказов"]
AppReadOnly --> SvcAnalytics["svc_analytics<br>сервис аналитики"]
classDef admin fill:#1e293b,stroke:#f43f5e,stroke-width:1px,color:#f8fafc;
classDef role fill:#1e293b,stroke:#818cf8,stroke-width:1px,color:#f8fafc;
classDef svc fill:#0f172a,stroke:#34d399,stroke-width:1px,color:#f8fafc;
class Admin admin;
class AppOwner,AppReadWrite,AppReadOnly role;
class SvcOrders,SvcAnalytics svc;
RBAC в PostgreSQL: синтаксис и подкапотная работа
PostgreSQL обладает одной из самых мощных и гибких реализаций ролевого доступа в индустрии.
Создание ролей и предоставление привилегий
Настроим эталонную иерархию прав для производственной базы данных:
-- 1. Создаем абстрактные роли без права прямого логина (группы)
CREATE ROLE readonly;
CREATE ROLE readwrite;
CREATE ROLE app_owner WITH LOGIN PASSWORD 'strong_owner_password' INHERIT;
-- 2. Базовые права для чтения
GRANT CONNECT ON DATABASE mydb TO readonly;
GRANT USAGE ON SCHEMA public TO readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly;
-- КРИТИЧЕСКИ ВАЖНО: Права по умолчанию для будущих таблиц миграций!
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO readonly;
-- 3. Роль записи наследует чтение и получает права DML
GRANT readonly TO readwrite;
GRANT INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO readwrite;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT INSERT, UPDATE, DELETE ON TABLES TO readwrite;
-- 4. Сервисный аккаунт приложения
CREATE ROLE svc_orders WITH LOGIN PASSWORD 'service_secret' INHERIT;
GRANT readwrite TO svc_orders;
Row-Level Security (RLS)
Для максимальной изоляции можно использовать RLS — политики, ограничивающие видимость строк на основе логического выражения. Например, сервис арендатора tenant_A должен видеть исключительно строки, где tenant_id = 'A':
-- Активируем политику RLS для таблицы документов
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
-- Создаем предикат изоляции строк
CREATE POLICY tenant_isolation ON documents
FOR ALL
TO app_user
USING (tenant_id = current_setting('app.tenant_id'));
Перед выполнением запросов клиентское Go-приложение задает локальный параметр сессии:
SET app.tenant_id = 'A';
При выполнении любого запроса ядро СУБД прозрачно внедряет условие проверки в дерево плана запроса. Это работает аналогично неявному предикату WHERE, но с абсолютной гарантией: даже если небрежно написанный код выполнит прямой SELECT * FROM documents, планировщик вернет исключительно строки арендатора 'A', физически исключая утечку чужих данных!
Роли и привилегии для функций и расширений
Нередко приложению требуется вызывать специализированные хранимые процедуры или генерировать автоинкрементные ключи:
-- Разрешить выполнение функции расчета скидки
GRANT EXECUTE ON FUNCTION calculate_discount TO readwrite;
-- Доступ к генераторам последовательностей для SERIAL / IDENTITY колонок
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO readwrite;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE, SELECT ON SEQUENCES TO readwrite;
-- Использование расширений (например, pgcrypto для хэширования)
GRANT USAGE ON SCHEMA extensions TO app_user;
Go и управление RBAC
Инженерная дисциплина в Go требует строгого разделения учетных записей по функциональным назначениям:
- Роль миграций (
migrator): Обладает правами DDL (CREATE,ALTER,DROP). Используется исключительно инструментами миграций (golang-migrate,goose) в момент деплоя. - Сервисная роль рантайма (
svc_orders): Обладает только DML-правами (SELECT,INSERT,UPDATE,DELETE) на нужные таблицы. - Роль аналитики (
svc_analytics): СтрогоSELECT, часто подключается к выделенной читающей реплике с агрессивными таймаутами выполнения запросовstatement_timeout.
Динамическая смена роли в Go
Некоторые архитектуры используют временное переключение роли внутри открытой сессии с помощью команды SET ROLE:
package database
import (
"context"
"database/sql"
"fmt"
_ "github.com/jackc/pgx/v5/stdlib"
)
func initDB(ctx context.Context) (*sql.DB, error) {
db, err := sql.Open("pgx", "postgres://migrator:secret@host:5432/mydb?sslmode=require")
if err != nil {
return nil, err
}
// Понижаем привилегии сессии до сервисной роли
if _, err := db.ExecContext(ctx, "SET ROLE svc_orders"); err != nil {
db.Close()
return nil, fmt.Errorf("set role failed: %w", err)
}
return db, nil
}
Однако в пулах соединений (database/sql, pgxpool) использование динамического SET ROLE — опасный антипаттерн! Если соединение возвращается в пул без очистки роли (RESET ROLE), следующая горутина случайно унаследует чужой контекст безопасности.
Золотое правило: подключайтесь сразу под целевой сервисной ролью:
config, err := pgxpool.ParseConfig("postgres://svc_orders:password@host/db?sslmode=require")
if err != nil {
return nil, err
}
pool, err := pgxpool.NewWithConfig(ctx, config)
Миграции из Go
Запуск миграций выполняется под отдельной строкой подключения с правами владельца схемы:
package migrations
import (
"errors"
"log"
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
)
// запуск миграций под отдельными креденшелами
func runMigrations(migrationsURL string) error {
m, err := migrate.New("file://migrations", migrationsURL)
if err != nil {
return err
}
defer m.Close()
if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) {
return err
}
return nil
}
func ApplyMigrations() {
migratorDSN := "postgres://migrator:super_secret@host:5432/db?sslmode=require"
if err := runMigrations(migratorDSN); err != nil {
log.Fatalf("Migration failed: %v", err)
}
}
pgxpool и несколько пулов для разных привилегий
Если бэкенд сочетает регулярные операции пользователей с фоновыми задачами архивирования или аналитики, правильный архитектурный шаблон — разделение пулов соединений:
package repository
import "github.com/jackc/pgx/v5/pgxpool"
type DB struct {
ROPool *pgxpool.Pool // readonly (подключение svc_analytics к реплике)
RWPool *pgxpool.Pool // readwrite (подключение svc_orders к мастеру)
}
Это исключает взаимное влияние нагрузок и гарантирует соблюдение прав доступа на сетевом уровне пулов (2. Connection pool).
Mechanical Sympathy: цена проверок прав
С точки зрения кремния и архитектуры СУБД, RBAC и политики доступа не являются бесплатными:
- Кэш списков контроля доступа (ACL Cache): Серверный бэкенд PostgreSQL не обращается к дисковым таблицам
pg_catalogна каждый SQL-запрос. При первом обращении права компилируются в оптимизированную хэш-таблицу в локальной памяти процесса backend'а. Вызовы командGRANTилиREVOKEвызывают отправку широковещательного сигнала инвалидации кэша (Shared Invalidation Message), заставляя все бэкенды перечитывать каталоги. Поэтому частая смена прав в рантайме недопустима. - Накладные расходы Row-Level Security (RLS): Условие политики RLS физически вычисляется для каждой просматриваемой строки. Если предикат политики содержит тяжелый вызов
current_setting('app.tenant_id')или подзапросы, это создает заметную нагрузку на CPU. Чтобы минимизировать деградацию, обязательно создавайте индекс по колонке селектора (например,CREATE INDEX idx_docs_tenant ON documents(tenant_id)), чтобы планировщик применял RLS на этапеIndex Scan, отсекая 99% строк без чтения табличных страниц в память! - Аутентификация при старте соединения: Разрешение членства в ролях и проверка пароля (SCRAM-SHA-256) требуют нескольких тысяч тактов CPU. Повторное использование TCP-сокетов через пулы соединений (
pgxpool) полностью амортизирует эти расходы.
Разграничение доступа к колонкам и схемам
PostgreSQL позволяет ограничивать доступ с точностью до отдельных полей:
GRANT SELECT (id, username, created_at) ON users TO public_reader;
Это полезно, когда сервис должен читать публичный профиль, но не имеет права видеть колонки password_hash или phone_number.
Однако учтите инженерный нюанс: если Go-сервис использует ORM или наивный запрос SELECT * FROM users, СУБД немедленно вернет ошибку permission denied for column password_hash. При использовании колоночного RBAC клиентский код обязан явно перечислять только разрешенные колонки в операторе SELECT.
RBAC и CI/CD: автоматизация контроля прав
Конфигурация ролей и привилегий — это неотъемлемая часть инфраструктурного кода (Schema as Code). Права обязаны версионироваться вместе с DDL в миграциях:
-- +migrate Up
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
tenant_id VARCHAR(64) NOT NULL,
amount NUMERIC(12, 2) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Выдаем права на чтение и запись
GRANT SELECT, INSERT, UPDATE ON orders TO readwrite;
GRANT USAGE, SELECT ON SEQUENCE orders_id_seq TO readwrite;
-- +migrate Down
DROP TABLE IF EXISTS orders;
Типичные вопросы с собеседований
Итог
Управление доступом на основе ролей (RBAC) в базах данных — это фундаментальный каркас безопасности, превращающий систему из монолитной уязвимой мишени в эшелонированную крепость.
Для Go-разработчика понимание RBAC охватывает:
- Проектирование изолированных сервисных учетных записей с минимально достаточными правами.
- Разделение пулов соединений
ROPoolиRWPoolдля защиты транзакционного мастера от аналитических нагрузок. - Использование Row-Level Security для гарантированной мультитенантной изоляции данных на уровне ядра СУБД.
- Автоматизацию привилегий через
ALTER DEFAULT PRIVILEGESв миграциях CI/CD.
Мы прошли огромный путь: от физического устройства дисковых страниц и структур индексов B-Tree до масштабирования, шардирования, наблюдаемости (19. Observability БД) и безопасности.
В финальной статье раздела мы сведем все полученные знания в единую архитектурную панораму: 23. Итоги раздела. Полная картина работы с базами данных.