Прямой ответ
Audit log — журнал значимых действий с указанием, кто, что, когда и над чем сделал, а также чем это закончилось. Он отличается от логов приложения назначением: логи приложения помогают разработчику понять поведение системы, журнал аудита — восстановить последовательность событий при расследовании и подтвердить соответствие требованиям.
Состав события аудита:
- Кто: идентификатор субъекта (пользователь, сервис, API-ключ) и, при наличии, от чьего имени он действовал.
- Что: действие и объект —
refund:payment/12345,role.grant:admin. - Когда: временная метка в UTC с точностью до миллисекунд.
- Откуда: IP-адрес, user agent, идентификатор сессии.
- Результат: успешно или отказано, причина отказа.
- Контекст: идентификатор запроса для связывания с логами приложения.
Что обязательно попадает в журнал:
- Аутентификация: успешные входы, неудачные попытки, выход, смена пароля, включение и отключение 2FA.
- Управление доступом: выдача и отзыв ролей, создание и отзыв API-ключей — см. RBAC и API-ключи.
- Доступ к чувствительным данным: чтение персональных данных, выгрузки, экспорт отчётов.
- Изменение значимых объектов: суммы, статусы, реквизиты, настройки безопасности.
- Административные операции: изменения конфигурации, массовые операции.
Принципы:
- Журнал пишется до или вместе с операцией, а не «когда получится». Отказ записи аудита для критичных операций должен блокировать саму операцию.
- Неизменяемость. Записи не изменяются и не удаляются приложением. Права на UPDATE/DELETE у приложения нет — только INSERT.
- Журнал не содержит секретов. Пароли, токены, полные номера карт не пишутся ни в каком виде. Персональные данные минимизируются — см. маскирование PII.
- Единый формат. Структурированные события удобнее разбирать; основы — в статье про структурированные логи.
Практика: схема и хранение
Таблица аудита в PostgreSQL:
CREATE TABLE audit_events (
id BIGSERIAL PRIMARY KEY,
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(),
actor_id BIGINT,
actor_type TEXT NOT NULL, -- user | service | api_key
on_behalf_of BIGINT,
action TEXT NOT NULL, -- refund.approve
resource TEXT NOT NULL, -- payment/12345
result TEXT NOT NULL, -- allowed | denied
reason TEXT,
ip INET,
user_agent TEXT,
request_id TEXT,
details JSONB
);
-- приложение не должно иметь прав на изменение и удаление
REVOKE UPDATE, DELETE ON audit_events FROM app_role;
GRANT INSERT, SELECT ON audit_events TO app_role;
CREATE INDEX audit_actor_time_idx ON audit_events (actor_id, occurred_at DESC);
CREATE INDEX audit_resource_idx ON audit_events (resource, occurred_at DESC);
Запись из кода:
def audit(*, actor, action: str, resource: str, result: str,
reason: str | None = None, request=None, details: dict | None = None):
db.insert("audit_events",
actor_id=actor.id, actor_type=actor.type, on_behalf_of=actor.on_behalf_of,
action=action, resource=resource, result=result, reason=reason,
ip=request.client_ip if request else None,
user_agent=request.user_agent if request else None,
request_id=request.id if request else None,
details=redact(details or {}))
Рекомендации:
- Пишите отказ так же тщательно, как успех. Попытки несанкционированного доступа — самый ценный материал для расследования.
- Свяжите аудит с логами приложения через
request_id. Это позволяет перейти от «кто изменил сумму» к полному стеку запроса. - Разделяйте горячее и холодное хранение. Последние 90 дней — в основной БД для быстрых запросов, дальше — в объектном хранилище с проверкой целостности.
- Проверяйте целостность архива. Хеш-цепочка или подпись партии записей делает подмену заметной.
- Ограничьте доступ к журналу. Чтение аудита — привилегированная операция, которая сама логируется.
- Регулярно тестируйте чтение. Раз в квартал проходите типовой сценарий расследования: найти все действия пользователя за период и все изменения конкретного объекта.
- Опишите, как отвечать на типовые вопросы. Журнал проектируется под вопросы, которые придётся задавать: кто менял эту запись, кто имел доступ за период, кто выдал права. Если на вопрос нельзя ответить одним запросом, схема событий неполна.
- Учитывайте объём на старте. Рост числа событий легко превращает аудит в проблему производительности. Партиционирование по времени и отдельная схема под журнал дешевле, чем перенос данных под нагрузкой.
Практический сценарий: расследование подозрительного возврата
Контекст: финансовый отдел заметил серию возвратов на небольшие суммы, одобренных разными операторами.
- Первый запрос. Все события
refund.approveза месяц с разбивкой по операторам. Журнал дал список из 47 возвратов, из них 19 — одним оператором за три дня. - Углубление. По
request_idперешли к логам приложения и увидели, что возвраты инициированы с одного IP и в нехарактерное время. По журналу доступа выяснилось, что роль оператора была выдана администратором за день до начала серии. - Проверка прав. Событие
role.grantв журнале содержалоgranted_by, но не содержало основания выдачи — процесс согласования не фиксировался. Это стало отдельной находкой. - Действия. Роль отозвана, сессии завершены, возвраты оспорены; в форму выдачи роли добавлено обязательное поле основания.
- Профилактика. Добавлен алерт на аномальное число операций одного типа на пользователя и требование указывать основание при выдаче привилегированных ролей.
Вывод: ценность журнала определяется не объёмом записей, а тем, можно ли по нему восстановить цепочку «кто выдал права → кто ими воспользовался → что изменилось». События без субъекта и результата для этого непригодны.
FAQ
Чем журнал аудита отличается от обычных логов? Назначением и гарантиями. Логи приложения могут ротироваться и теряться, они нужны для отладки. Аудит-журнал неизменяем, имеет ограниченный круг читателей, хранится долго и используется как доказательство. Смешивать их в одном потоке — плохая практика.
Сколько хранить журнал? Определяется требованиями регулятора и договорами: часто от года до нескольких лет. Практика — горячий слой 90 дней для оперативных запросов и холодный архив на весь требуемый срок с периодической проверкой читаемости.
Нужно ли логировать чтение данных, если записей очень много? Для чувствительных категорий — да, иначе невозможно ответить, кто имел доступ к персональным данным. Объём контролируют агрегацией: фиксируют факт выборки с числом затронутых строк, а не каждую строку.
Как избежать записи персональных данных в журнал? Минимизация и маскирование: вместо полного значения — идентификатор и тип, вместо e-mail — хеш или маска. Правило простое: в журнале должно быть достаточно информации для расследования и достаточно мало для того, чтобы утечка журнала не стала утечкой данных.
Источники: OWASP Logging Cheat Sheet, PostgreSQL docs (privileges), Wikipedia (audit trail) — ссылки в шапке.