The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.
Follows the import procedure in README.md.
source/ the originating repository verbatim — 78 documents, 28 SVGs,
8 manifests, plus .source-revision recording the commit
final/ the SSOT
document.md 729 lines written from the 29 experiment documents, not
concatenated: what was predicted, what was measured, and
where the measurement itself was wrong
evidence/raw 125 outputs, flattened to <experiment>__<file> because
the originals collided (01-baseline.txt appeared three
times) and the audit only globs the top level
evidence/meta one per raw file; command and exitCode are null and the
README says why rather than inventing them
evidence/browser 22 captures
assets/ three diagrams through techviz
.techviz/ their VizSpecs
A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.
Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
96 lines
4.6 KiB
Markdown
96 lines
4.6 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 · analysis/05 §13.1, §17 P8 이다.
|
|
---
|
|
|
|
# RLS가 아무것도 하지 않는 세 가지 방법
|
|
|
|
정책 검증기가 행 수준 보안이 조용히 무력화되는 세 경로를 전부 확인한다. 다만 검증기 자신이 보호되어야 할 테이블의 부재를 성공으로 인정하는 결함을 갖는다.
|
|
|
|
## 관계
|
|
|
|
- **RLS가 성립하기 위한 세 전제**
|
|
이 검증기가 확인하는 세 조건이다.
|
|
- **Hibernate filter는 보안 경계가 아니다**
|
|
같은 격리 문제를 ORM 기능으로 풀려 할 때의 규칙이다.
|
|
- **tenant 컬럼이 있는 테이블의 모든 unique 제약에 그 컬럼이 들어가야 한다**
|
|
같은 마이그레이션이 함께 다룬 축이다.
|
|
|
|
## 문제
|
|
|
|
행 수준 보안은 설정이 올바르게 보이면서 아무것도 하지 않을 수 있다. 그 경로가 셋이다.
|
|
|
|
정책이 없거나 테이블에 RLS 가 활성화되지 않았다
|
|
런타임 롤이 우회 속성을 갖는다
|
|
런타임 롤이 그 테이블을 소유한다
|
|
|
|
셋 다 같은 결과를 만든다. 모든 쿼리가 모든 테넌트의 행을 돌려주면서 정책은 올바르게 설정된 것처럼 보인다.
|
|
|
|
## 결론
|
|
|
|
검증기가 세 경로를 전부 확인한다.
|
|
|
|
시스템 카탈로그에서 테이블별로 RLS 활성 여부와 강제 여부를 읽고, 롤의 우회 속성을 확인하며, 소유 관계를 본다.
|
|
|
|
세 번째가 가장 놓치기 쉽다. 소유자는 기본적으로 자기 정책에서 면제되고, 소유자는 흔히 마이그레이션 롤이며, 그것이 사람들이 테스트하는 롤이다.
|
|
|
|
같은 축의 문제가 유니크 인덱스에도 있고 마이그레이션 주석이 그것을 적는다. 테넌트 격리가 컬럼에 의존하면 그 테이블의 모든 유일성 요구에 그 컬럼이 들어가야 한다. 값만으로 걸린 유니크 인덱스는 다른 테넌트가 그 값을 이미 썼다는 이유로 한 테넌트의 삽입을 실패시키고, 그것은 버그이자 정보 유출이다.
|
|
|
|
다만 검증기 자신에게 결함이 있다. 반드시 보호되어야 할 테이블이 조회 결과에 없을 때 그것을 성공으로 인정한다. 즉 테이블 이름이 바뀌거나 조회 조건이 어긋나면 검증이 통과한다.
|
|
|
|
## 검증 환경
|
|
|
|
데이터베이스 : PostgreSQL
|
|
확인 방식 : 검증기가 검사하는 세 조건과 마이그레이션 주석 확인
|
|
소스 수정 : x
|
|
|
|
## 재현 조건
|
|
|
|
1. 정책 검증기의 클래스 javadoc 을 읽는다. 세 경로가 열거되어 있다.
|
|
2. 검증기가 실행하는 카탈로그 조회를 확인한다.
|
|
3. 마이그레이션의 유니크 인덱스 주석을 읽는다.
|
|
4. 보호 대상 테이블이 조회 결과에 없을 때의 처리를 확인한다.
|
|
|
|
## 본문
|
|
|
|
<!-- body:start -->
|
|
|
|
`RlsPolicyVerifier`가 RLS가 조용히 무력화되는 세 경로를 전부 확인한다 — policy 없음/RLS 미활성 · 런타임 롤이 `BYPASSRLS` 보유 · **런타임 롤이 테이블을 소유**(FORCE 없으면 면제).
|
|
|
|
## RlsPolicyVerifier 참조 위치
|
|
|
|
:::evidence key="three-ways-rls-does-nothing" alt="코드베이스에서 RlsPolicyVerifier 를 검색한 출력 2줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="RlsPolicyVerifier 코드베이스 검색 — 2줄 · exit 0" zoom="true"
|
|
:::
|
|
|
|
## 세 번째가 가장 놓치기 쉽다
|
|
|
|
소유자는 정책을 우회하는 것이 아니라 애초에 적용 대상이 아니다.
|
|
|
|
## unique index 에도 같은 축의 주석이 있다
|
|
|
|
tenant 격리가 컬럼에 의존하면 그 테이블의 **모든 uniqueness 요구**에 그 컬럼이 들어가야 하고, `(value)`만의 unique index는 다른 tenant가 그 값을 썼다는 이유로 insert를 실패시켜 **버그이자 정보 유출**이 된다.
|
|
|
|
## verifier 자신에게도 결함이 있다
|
|
|
|
`analysis/05` §97이 기록하듯 "반드시 보호돼야 하는 table"의 부재를 성공으로 인정한다.
|
|
|
|
## 확인하지 못한 것
|
|
|
|
세 조건을 하나씩 깨서 검증기가 실제로 잡는지 확인하지 않았다. 실제 RLS 환경을 세우지 않았다.
|
|
|
|
<!-- body:end -->
|