Files
document-haness/docs/clean-architecture-backend-template/tech-log-studio/multitenancy-isolation/case/case-three-ways-rls-does-nothing.md
T

94 lines
5.5 KiB
Markdown

---
kind: CASE
slug: three-ways-rls-does-nothing
title: RLS 검증에서 구분해야 할 우회와 미적용 경로
topic: multitenancy-isolation
project: clean-architecture-backend-template
status: 게시 전
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
rootTreeNode: case:three-ways-rls-does-nothing
evidenceCapturedOn: 2026-09-01
assets:
- key: three-ways-rls-does-nothing
file: ../../../final/evidence/rendered/three-ways-rls-does-nothing.svg
evidence:
- ../../../final/evidence/raw/three-ways-rls-does-nothing.txt
source:
- 원본 분석 절은 final/document.md#4-1 · final/document.md#a05 §13.1, §17 P8 이다.
---
# RLS 검증에서 구분해야 할 우회와 미적용 경로
`RlsPolicyVerifier`는 RLS 활성 여부, 적용 가능한 policy, 런타임 role의 우회 속성, table owner 여부와 `FORCE ROW LEVEL SECURITY` 상태를 확인한다. 이 값들은 하나의 ‘세 전제’가 아니다. 일반 role에서 RLS가 활성화됐는데 적용 가능한 policy가 없으면 PostgreSQL은 default deny를 적용하고, owner는 FORCE 여부에 따라 policy 적용 여부가 갈린다. 검증기 자체에는 보호 대상 테이블의 부재를 성공으로 인정하는 별도 결함이 있다.
## 관계
- **PostgreSQL RLS 적용 여부를 가르는 분기**
이 검증기가 읽는 값들을 PostgreSQL의 실제 적용 규칙에 맞춰 해석한 개념이다.
- **Hibernate filter는 보안 경계가 아니다**
같은 격리 문제를 ORM 기능으로 풀려 할 때의 규칙이다.
- **tenant 내부 유일성과 전역 유일성은 구분한다**
tenant 내부에서만 유일해야 하는 값은 tenant 식별자를 포함하고, 전역 유일성이 요구사항이면 global unique를 유지할 수 있다.
## 문제
행 수준 보안은 조건에 따라 서로 다른 결과를 만든다.
RLS가 비활성화되어 있으면 policy가 적용되지 않는다. RLS가 활성화됐지만 현재 role에 적용 가능한 policy가 없으면 일반 role에는 default deny가 적용된다. superuser나 `BYPASSRLS` role은 RLS를 우회한다. table owner는 기본적으로 policy를 우회하지만 `FORCE ROW LEVEL SECURITY`가 켜지면 policy 대상이 된다.
따라서 이 검증에서 봐야 할 것은 ‘셋 중 하나가 틀리면 모두 허용’이 아니라 현재 role이 어느 분기에 속하고 그 분기에서 policy가 실제로 적용되는지다.
## 결론
검증기가 세 경로를 전부 확인한다.
시스템 카탈로그에서 테이블별로 RLS 활성 여부와 강제 여부를 읽고, 롤의 우회 속성을 확인하며, 소유 관계를 본다.
세 번째가 가장 놓치기 쉽다. 소유자는 기본적으로 자기 정책에서 면제되고, 소유자는 흔히 마이그레이션 롤이며, 그것이 사람들이 테스트하는 롤이다.
같은 축의 문제가 유니크 인덱스에도 있고 마이그레이션 주석이 그것을 적는다. tenant 내부에서만 유일해야 하는 값은 tenant 식별자를 포함해야 한다. 반대로 시스템 전체에서 유일해야 하는 값은 global unique로 둘 수 있다. 따라서 `(value)`만의 unique index가 문제인지는 그 값의 유일성 범위가 tenant 내부인지 전역인지에 따라 판단한다.
다만 검증기 자신에게 결함이 있다. 반드시 보호되어야 할 테이블이 조회 결과에 없을 때 그것을 성공으로 인정한다. 즉 테이블 이름이 바뀌거나 조회 조건이 어긋나면 검증이 통과한다.
## 검증 환경
데이터베이스 : PostgreSQL
확인 방식 : 검증기가 읽는 RLS 분기 값과 마이그레이션 주석 확인
소스 수정 : x
## 재현 조건
1. 정책 검증기의 클래스 javadoc을 읽어 기존 구현이 열거한 세 경우를 확인한다.
2. 검증기가 실행하는 카탈로그 조회를 확인한다.
3. 마이그레이션의 유니크 인덱스 주석을 읽는다.
4. 보호 대상 테이블이 조회 결과에 없을 때의 처리를 확인한다.
## 본문
<!-- body:start -->
`RlsPolicyVerifier`는 RLS 활성 여부와 policy 상태, runtime role의 `BYPASSRLS`, table owner 여부와 `FORCE ROW LEVEL SECURITY`를 함께 본다. 이 값들은 PostgreSQL이 policy를 적용할지 결정하는 서로 다른 분기다.
## RlsPolicyVerifier 참조 위치
:::evidence key="three-ways-rls-does-nothing" alt="코드베이스에서 RlsPolicyVerifier 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RlsPolicyVerifier 코드베이스 검색 — 2줄 · exit 0" zoom="true"
:::
## 세 번째가 가장 놓치기 쉽다
table owner는 기본적으로 RLS policy를 우회한다. `FORCE ROW LEVEL SECURITY`를 켜면 owner도 policy 적용 대상이 된다.
## unique index 에도 같은 축의 주석이 있다
tenant 내부 유일성을 표현하는 uniqueness 요구에는 tenant 식별자를 포함해야 한다. `(value)`만의 unique index는 전역 유일성을 뜻하므로, 요구사항이 tenant 내부 유일성이라면 다른 tenant의 값 때문에 insert가 실패한다. 반대로 전역 유일성이 실제 요구사항이면 global unique 자체가 잘못은 아니다.
## verifier 자신에게도 결함이 있다
`final/document.md#a05` §97이 기록하듯 "반드시 보호돼야 하는 table"의 부재를 성공으로 인정한다.
## 확인하지 못한 것
RLS 비활성, 우회 role, owner/FORCE, applicable policy 부재를 실제 PostgreSQL 환경에서 각각 재현하지 않았다.
<!-- body:end -->