감사 테이블 append-only 강제 (MySQL)
감사 로그 테이블에 BEFORE UPDATE / BEFORE DELETE 트리거를 걸고 SIGNAL로 예외를 던지면, append-only가 애플리케이션의 약속이 아니라 스토리지 계층의 제약이 된다.
언제 찾게 되는가
- 감사 로그를 테이블에 남기긴 했는데, 감사 대응에서 “이 로그가 사후에 수정되지 않았음”을 증명해야 할 때.
- “수정 API를 안 만들었다”를 불변성의 근거로 제시했다가 반박당했을 때.
핵심 답
애플리케이션 코드에 수정 경로가 없다는 것은 증명이 아니다. DB에 직접 접속하면 우회된다. DB 자체가 거부하게 만들어야 한다.
CREATE TRIGGER audit_log_no_update BEFORE UPDATE ON `audit_log`
FOR EACH ROW SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'audit_log is append-only';
CREATE TRIGGER audit_log_no_delete BEFORE DELETE ON `audit_log`
FOR EACH ROW SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'audit_log is append-only';SQLSTATE '45000'은 “사용자 정의 예외”를 뜻하는 관례적 값이다. 트리거가 예외를 던지면 해당 문장은 실패하고, 트랜잭션 안이라면 롤백된다. INSERT는 트리거가 없으므로 그대로 동작한다.
✅ 실측 (DBFlow, MySQL 8, 2026-07)
배포 시 주의점
- 권한 — 트리거 생성에는 대상 테이블에 대한
TRIGGER권한이 필요하다. 마이그레이션 실행 계정이 앱 계정과 다르면 여기서 먼저 막힌다. 📄 문서 기반 (미검증) - DEFINER와 바이너리 로그 — 트리거는
DEFINER계정 권한으로 실행된다. 바이너리 로깅이 켜진 환경에서 제한된 계정으로 생성하면 복제·백업 복원 시점에 DEFINER 계정이 존재하지 않아 문제가 되므로, DEFINER를 명시하고 그 계정이 모든 환경에 존재하는지 확인한다. 셀프호스팅 배포 문서에 이 조건을 남겨야 한다. ✅ 실측 (DBFlow 셀프호스팅 설정, 2026-07) - 마이그레이션 도구와의 순서 — 트리거를 건 뒤에는 그 테이블을 재생성하는 마이그레이션이 트리거를 조용히 날린다. 테이블 재생성 계열 변경 뒤에는 트리거 재생성이 따라와야 한다.
한계 — 이것이 막지 못하는 것
트리거는 일반 접근을 막는 방어선이지 불변성의 증명은 아니다. TRIGGER 권한을 가진 계정이라면 DROP TRIGGER 후 수정하고 다시 만들 수 있고, DBA/root는 언제나 이 경로를 쓸 수 있다.
한 단계 더 필요하다면:
| 수단 | 막는 대상 | 비용 |
|---|---|---|
| 트리거 (위 방식) | 앱·일반 계정의 UPDATE/DELETE | 거의 없음 |
| 권한 분리 (감사 테이블에 INSERT/SELECT만 부여) | 앱 계정의 스키마 조작 | 계정 운영 부담 |
| 해시 체인 (행마다 이전 행 해시 포함) | 사후 수정의 은폐 | 쓰기 경로 복잡도 |
| 외부 WORM 스토리지로 이중 적재 | DBA 권한을 가진 내부자 | 인프라·비용 |
대부분의 조직에서는 트리거 + 권한 분리가 비용 대비 실효선이다. 해시 체인 이상은 위협 모델에 내부자가 명시적으로 포함될 때 검토한다.
Related
Last updated on