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이 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건에 그칩니다.
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한 뒤 정책을 정의하고, 보안이 민감한 테이블은 소유자까지 걸리도록 FORCE를 더합니다. 슈퍼유저·BYPASSRLS 역할·(FORCE가 없을 때의) 소유자는 정책을 우회하고, SECURITY DEFINER 함수도 우회 통로가 되므로 주의합니다.
USING= 읽기 조건,WITH CHECK= 쓰기 조건- 다중 정책: PERMISSIVE는 OR, RESTRICTIVE는 AND
- 정책 조건이 자주 걸리는 컬럼에는 인덱스 필수