명령어/DB

[PostgreSQL] RLS로 테넌트별 행을 세션 변수 하나로 격리한다

jykim23 2026. 7. 30. 22:18
반응형

설치·접속: PostgreSQL 설치와 접속

부제: 한 테이블에 여러 고객사(테넌트) 데이터가 섞여 있을 때, 애플리케이션이 WHERE를 빠뜨려도 남의 행이 절대 안 보이게 DB에서 강제할 때

CREATE TABLE m9_tenant_docs (id serial PRIMARY KEY, tenant text, body text);
INSERT INTO m9_tenant_docs (tenant, body) VALUES
 ('acme','acme 계약서'), ('acme','acme 매뉴얼'),
 ('globex','globex 보고서'), ('globex','globex 회의록');

ALTER TABLE m9_tenant_docs ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_iso ON m9_tenant_docs
  USING (tenant = current_setting('app.tenant', true));

GRANT SELECT, INSERT, UPDATE, DELETE ON m9_tenant_docs TO m9_app;

-- 앱유저로 전환 후, 세션 변수로 "지금 이 고객사"를 지정
SET ROLE m9_app;
SET app.tenant = 'acme';
SELECT * FROM m9_tenant_docs;
 id | tenant |    body
----+--------+-------------
  1 | acme   | acme 계약서
  2 | acme   | acme 매뉴얼
(2 rows)

app.tenantglobex로 바꾸면 같은 쿼리가 globex 행 2개만 반환한다. 테이블 데이터는 그대로인데, RLS 정책이 매 쿼리에 WHERE tenant = current_setting('app.tenant')를 자동으로 덧붙이는 효과다. 애플리케이션은 로그인 직후 세션 변수 하나만 세팅하면 되고, 이후 어떤 쿼리를 던지든 다른 테넌트 행은 물리적으로 결과에서 빠진다. 실수로 WHERE 조건을 빼먹어도 격리가 무너지지 않는 게 핵심이다. current_setting의 두 번째 인자 true는 변수가 없을 때 에러 대신 NULL을 돌려줘, 변수 미설정 시 아무 행도 안 보이게 안전하게 닫힌다.

이렇게도 쓴다

읽기뿐 아니라 INSERT도 막는다. 남의 테넌트로 데이터를 심으려 하면 거부. (조합: WITH CHECK)

CREATE POLICY tenant_iso ON m9_tenant_docs
  USING (tenant = current_setting('app.tenant', true))
  WITH CHECK (tenant = current_setting('app.tenant', true));
-- SET app.tenant='acme' 상태에서 globex 행 INSERT -> 정책 위반 에러

 

테이블 소유자도 정책을 우회 못 하게 강제한다. (조합: FORCE ROW LEVEL SECURITY)

ALTER TABLE m9_tenant_docs FORCE ROW LEVEL SECURITY;

 

동작(SELECT/INSERT/UPDATE/DELETE)별로 다른 정책을 건다. (조합: FOR 절)

CREATE POLICY read_all ON m9_tenant_docs FOR SELECT USING (true);
CREATE POLICY write_own ON m9_tenant_docs FOR INSERT
  WITH CHECK (tenant = current_setting('app.tenant', true));

 

특정 관리자 롤에만 정책을 적용해 일반 롤은 전체를 보게 한다. (조합: TO 롤)

CREATE POLICY tenant_iso ON m9_tenant_docs TO m9_app
  USING (tenant = current_setting('app.tenant', true));

 

정책이 어떻게 걸렸는지 확인한다. (조합: \d 상세)

SELECT polname, polcmd, pg_get_expr(polqual, polrelid) FROM pg_policy;

 

임시로 RLS를 꺼서 전체를 본다(소유자·슈퍼유저 점검용). (조합: DISABLE)

ALTER TABLE m9_tenant_docs DISABLE ROW LEVEL SECURITY;
반응형