Skip to Content
DatabaseMySQL감사 테이블 append-only 강제

감사 테이블 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 권한을 가진 내부자인프라·비용

대부분의 조직에서는 트리거 + 권한 분리가 비용 대비 실효선이다. 해시 체인 이상은 위협 모델에 내부자가 명시적으로 포함될 때 검토한다.

Last updated on