"누가 무엇을 기록할 수 있고, 누가 기록을 볼 수 있나"를 제어하는 것이 권한 관리입니다. 그리고 한 단계 더 나아가 — "누가 기록을 열람했는지도 기록해야 하나"는 메타 감사의 질문입니다. 이 챕터에서 두 가지를 모두 살펴봅니다.
RBAC로 emit/query/administer를 역할별로 통제하고, 열람 행위 자체도 별도 테이블에 기록합니다 — 감사 로그를 읽은 것도 감사됩니다.
RBAC: deny-by-default로 시작하라
Quipu-Log의 권한 모델은 세 가지 Action으로 간결합니다.
| Action | 무엇을 허용하나 | 누가 필요한가 |
|---|---|---|
Emit | 감사 이벤트 기록 | API 서버, 배치 잡 등 기록하는 쪽 |
Query | 기록된 로그 조회 | 감사관, 컴플라이언스 팀 |
Administer | 스키마 변경, DLQ redrive, 보존 실행, 무결성 검증 | 운영팀, 관리자 |
이 세 가지를 Role별로 매핑합니다. 핵심 설계 선택은 deny-by-default입니다 — 명시적으로 허가하지 않은 역할은 아무것도 못 합니다. 반대의 allow-by-default는 개발 편의용이고, 프로덕션에서는 권장하지 않습니다.
crates/quipu-middleware/src/permissions.rslet policy = PermissionPolicy::deny_by_default()
.grant(Role::new("auditor"), &[Action::Query])
.grant(Role::new("service"), &[Action::Emit])
.grant(Role::new("admin"), &[Action::Emit, Action::Query, Action::Administer]);
// 모르는 역할은 아무것도 못 함
assert!(!policy.is_allowed(&Role::new("intruder"), Action::Query));
PermissionPolicy는 파이프라인 시작 시 한 번 설정하고, 이후 모든 AuditHandle 메서드가 자동으로 검사합니다. 별도 호출이 필요 없습니다.
pre/post 필터: 요청을 면제하거나 이벤트를 보강하라
권한이 "누가"를 통제한다면, 필터는 "무엇을"을 세밀하게 제어합니다. FilterSet에 두 종류의 필터를 등록합니다.
- pre-filter: 요청이 inner service에 도달하기 전에 실행. 감사 불필요한 요청(헬스체크, 내부 ping 등)을
Skip으로 조기에 면제합니다. 바디를 읽기 전에 결정하므로 비용이 낮습니다. - post-filter: 응답이 나온 후 실행. 응답 상태 코드를 보고 결정하거나(
304 Not Modified는 감사 불필요), 이벤트 구조체를 직접 수정해 필드를 추가할 수 있습니다(이벤트 보강).
crates/quipu-middleware/src/filter.rs// post-filter: 응답 보고 이벤트 보강 또는 면제
pub type PostFilter =
Box<dyn Fn(&RequestInfo, &ResponseInfo, &mut AuditEvent) -> FilterDecision + Send + Sync>;
post-filter가 &mut AuditEvent를 받는 게 포인트입니다. 응답을 보고 이벤트에 필드를 덧붙일 수 있습니다(예: "응답이 403이면 custom 필드에 'forbidden' 태그 추가").
메타 감사: 감사 로그를 열람한 것도 기록한다
HIPAA나 금융 규제에서는 "누가 환자 기록을 열람했는지"도 감사 대상입니다. Quipu-Log는 이를 메타 감사(meta-audit)로 지원합니다.
메타 감사의 핵심 설계 결정 두 가지를 기억하세요.
① 별도 테이블. 접근 레코드는 root/access/에 쌓입니다. 메인 로그 테이블(root/logs/)과 독립적이라 보존 정책도 따로 설정할 수 있고, 메인 로그 쿼리 API로는 보이지 않습니다.
② 자기참조 회피. "접근 로그를 기록하는 것"이 "접근 이벤트"를 또 발생시키면 무한 루프가 됩니다. Quipu-Log는 이를 구조적으로 차단합니다 — record_access는 쿼리 경로를 타지 않는 순수 append입니다. 접근 로그를 읽는 것은 접근 이벤트로 기록되지만, 그 기록 자체는 또 다른 접근 이벤트를 만들지 않습니다.
crates/quipu-core/src/access.rs (주석 인용)// No self-reference loop: recording an access is a plain table append —
// it never goes through a query path, so it can never trigger another
// access record. Querying the access log *is* an access and is recorded
// (exactly one record per query), but that recording again is just an
// append: growth is strictly one record per externally-initiated
// operation, never recursive.
접근 레코드에는 검색어 값(probe value)을 절대 저장하지 않습니다. HMAC/RSA로 보호된 필드를 검색할 때, 그 검색어가 접근 로그에 평문으로 남으면 필드 보호의 의미가 사라집니다. 접근 로그에는 "어떤 필드를, 어떤 매칭 모드로, 어떤 시간 범위에서" 검색했는지 형태(shape)만 기록됩니다.
접근 로그 조회는 Action::Administer가 필요합니다 — "누가 무엇을 열람했는지"는 그 자체로 민감한 정보이기 때문입니다.
① deny-by-default와 allow-by-default 중 프로덕션에 왜 deny-by-default가 더 안전한가요?
② 접근 레코드에 검색어 값을 남기지 않는 이유는 무엇인가요?
③ 접근 로그가 메인 로그와 별도 테이블에 있어야 하는 이유를 보존 정책 관점에서 설명해 보세요.