Quipu-Log 교과서
파트 7 · 쓰기·읽기 경로

32 · 권한(RBAC)·필터·메타 감사

"누가 무엇을 기록할 수 있고, 누가 기록을 볼 수 있나"를 제어하는 것이 권한 관리입니다. 그리고 한 단계 더 나아가 — "누가 기록을 열람했는지도 기록해야 하나"는 메타 감사의 질문입니다. 이 챕터에서 두 가지를 모두 살펴봅니다.

한 문장 요약

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/logs/ 메인 감사 로그 머클 체인 + 체크포인트 보존 정책 독립 root/access/ 메타 감사 테이블 누가 무엇을 언제 열람 자체 해시 체인 handle.query(role, q) 메인 로그 조회 조회 append only (자기참조 없음)
메인 로그와 접근 로그는 별도 테이블이다. 접근 로그는 쿼리 경로를 타지 않는 순수 append여서 자기참조 루프가 구조적으로 불가능하다.

메타 감사의 핵심 설계 결정 두 가지를 기억하세요.

① 별도 테이블. 접근 레코드는 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가 더 안전한가요?
② 접근 레코드에 검색어 값을 남기지 않는 이유는 무엇인가요?
③ 접근 로그가 메인 로그와 별도 테이블에 있어야 하는 이유를 보존 정책 관점에서 설명해 보세요.