9.3 행 수준 보안(RLS)
Row-Level Security(RLS)는 같은 테이블의 row 일부만 사용자에게 보이게 하는 메커니즘입니다. 멀티 테넌트·개인정보 접근 제어·감사 추적 같은 시나리오에 표준입니다. 정책을 정의하면 PostgreSQL이 재작성 단계에서 모든 SELECT/INSERT/UPDATE/DELETE에 자동으로 WHERE 조건을 끼워 넣습니다.
기본 예시 — 멀티 테넌트
CREATE TABLE orders (
id bigserial PRIMARY KEY,
tenant_id int NOT NULL,
user_id int,
amount numeric,
...
);
-- RLS 활성
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- 자기 테넌트만 보이게 하는 정책
CREATE POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.tenant_id')::int);세션 시작 시:
-- 애플리케이션이 접속 직후 설정
SET app.tenant_id = '42';
-- 이제 모든 SELECT는 tenant_id = 42만 자동
SELECT * FROM orders;
-- 옵티마이저가 자동으로 WHERE tenant_id = 42 추가정책 구성 요소
CREATE POLICY <name> ON <table>
[ AS { PERMISSIVE | RESTRICTIVE } ]
[ FOR { ALL | SELECT | INSERT | UPDATE | DELETE } ]
[ TO { role | PUBLIC | CURRENT_USER } ]
USING ( <visibility_expression> )
[ WITH CHECK ( <write_expression> ) ];| 절 | 의미 |
|---|---|
PERMISSIVE (기본) | 정책이 하나라도 통과하면 OK |
RESTRICTIVE | 모든 RESTRICTIVE 정책을 전부 통과해야 함 |
USING | row가 보일 조건 (SELECT·UPDATE·DELETE) |
WITH CHECK | row가 쓰일 조건 (INSERT·UPDATE) |
USING과 WITH CHECK를 분리해 읽기와 쓰기 정책을 다르게 줄 수 있습니다.
ENABLE / FORCE
| 명령 | 효과 |
|---|---|
ALTER TABLE ... ENABLE ROW LEVEL SECURITY | 정책 활성. 그러나 소유자와 슈퍼유저는 정책 우회 |
ALTER TABLE ... FORCE ROW LEVEL SECURITY | 소유자도 정책 적용 |
BYPASSRLS 역할 속성 | 명시적으로 정책을 무시할 권한 |
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE orders FORCE ROW LEVEL SECURITY; -- 소유자도 정책 적용운영 권장: 보안 민감 테이블은 FORCE까지.
슈퍼유저와 BYPASSRLS
CREATE ROLE auditor BYPASSRLS;
GRANT pg_read_all_data TO auditor;BYPASSRLS는 모든 정책을 우회 — 감사·복구·디버깅용. 평소 운영자 계정에는 부여 안 합니다.
다중 정책
여러 정책이 같은 작업에 적용될 때:
- PERMISSIVE: OR 결합 (하나라도 통과)
- RESTRICTIVE: AND 결합 (모두 통과)
-- 사용자가 자기 row만 + 활성 row만 (두 정책 모두 PERMISSIVE)
CREATE POLICY p_own ON profiles
USING (user_id = current_setting('app.user_id')::int);
CREATE POLICY p_active ON profiles
AS RESTRICTIVE
USING (deleted_at IS NULL);위 조합: 자기 row 중 deleted_at IS NULL만 보임.
INSERT의 WITH CHECK
INSERT는 USING이 아닌 WITH CHECK로 검증합니다.
CREATE POLICY p_tenant_insert ON orders
FOR INSERT
WITH CHECK (tenant_id = current_setting('app.tenant_id')::int);다른 tenant_id로 INSERT를 시도하면 ERROR.
UPDATE의 두 조건
UPDATE는 이전 row 가시성(USING)과 변경 후 row의 적합성(WITH CHECK) 둘 다 만족해야 합니다.
CREATE POLICY p_tenant_update ON orders
FOR UPDATE
USING (tenant_id = current_setting('app.tenant_id')::int)
WITH CHECK (tenant_id = current_setting('app.tenant_id')::int);USING만 정의하면 변경 후에도 USING 조건을 재사용.
정책 조회
SELECT schemaname, tablename, policyname, permissive,
roles, cmd, qual, with_check
FROM pg_policies
WHERE schemaname = 'public';qual = USING 표현, with_check = WITH CHECK 표현.
활용 패턴
1. 멀티 테넌트
위 예시. 세션 변수 (current_setting) 또는 JWT 클레임을 PostgreSQL 함수로 매핑.
2. 자기 row만 보기
CREATE POLICY p_self ON notes
USING (owner_id = current_user::regrole::oid::int);3. 감사 로그 — 누구도 수정 못 함
CREATE POLICY p_no_update ON audit_log
AS RESTRICTIVE
FOR UPDATE
USING (false);USING (false)는 어떤 row도 매칭 안 함 → UPDATE 항상 0 row.
4. 시간 기반 접근 제어
CREATE POLICY p_business_hours ON salaries
USING (current_time BETWEEN '09:00' AND '18:00');성능 고려
RLS는 재작성 단계에서 WHERE 추가. 즉, 인덱스를 잘 만들어야 효과입니다.
-- tenant_id에 인덱스 필수 (또는 파티션 키)
CREATE INDEX idx_orders_tenant_id ON orders(tenant_id);EXPLAIN을 보면 자동 추가된 조건이 나타납니다.
Seq Scan on orders
Filter: (tenant_id = 42) <- RLS 정책에서 추가RLS와 SECURITY DEFINER 함수
SECURITY DEFINER 함수는 함수 생성자의 권한으로 실행 — RLS 정책을 우회할 수 있습니다. 잘못 쓰면 보안 구멍.
CREATE FUNCTION get_user(p_id int) RETURNS users AS $$
SELECT * FROM users WHERE id = p_id;
$$ LANGUAGE sql SECURITY DEFINER;
-- 호출하는 누구든 모든 user를 볼 수 있게 됨대안: SECURITY INVOKER (기본). 호출자의 권한·RLS 정책이 적용합니다.
함정과 주의
| 함정 | 대응 |
|---|---|
| 슈퍼유저나 owner는 정책 우회 | FORCE ROW LEVEL SECURITY |
| 인덱스 없는 RLS 조건 | seq scan 폭증 |
| 정책 조건에 함수 호출 | STABLE/IMMUTABLE 함수만 |
| SECURITY DEFINER 함수 | RLS 우회 위험 — SET row_security = on 명시 |
| 단순 SELECT 권한 부여로 끝났다 생각 | RLS가 적용된다는 사실을 개발자가 알아야 |
RLS로 보호 안 되는 것
| 미보호 | 의미 |
|---|---|
| 행 수 (count) 누출 | EXPLAIN으로 row 수 추정 가능 |
| timing attack | 조건 평가 시간 차이 |
| 다른 테이블의 외래 키 | 제3 테이블의 FK 검사가 row 존재를 누출 |
| 카탈로그 SELECT | pg_class.reltuples 등은 RLS 안 적용 |
RLS는 운영 표준이지만 완벽한 보안은 아닙니다. 깊은 분리가 필요하면 DB 자체를 분리하는 게 정석입니다.
tenant_id 컬럼. 둘이 섞이면 운영 복잡도 폭증합니다.정리
- RLS = row 단위 권한. 재작성 시 WHERE 자동 추가
ENABLE후 정책 정의합니다. 보안 민감 테이블은FORCEUSING= 읽기 조건,WITH CHECK= 쓰기 조건- 다중 정책: PERMISSIVE = OR, RESTRICTIVE = AND
- 인덱스 필수 — 정책 조건이 자주 들어가는 컬럼
- 슈퍼유저·BYPASSRLS·owner(
FORCE없을 시)는 우회 - SECURITY DEFINER 함수는 RLS 우회 가능 — 주의
다음 절(9.4)에서는 더 세밀한 권한 — 컬럼·뷰 권한과 SECURITY DEFINER/INVOKER 차이를 봅니다.