본문으로 건너뛰기
9.3 행 수준 보안(RLS)

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 정책을 전부 통과해야 함
USINGrow가 보일 조건 (SELECT·UPDATE·DELETE)
WITH CHECKrow가 쓰일 조건 (INSERT·UPDATE)

USINGWITH 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 존재를 누출
카탈로그 SELECTpg_class.reltuples 등은 RLS 안 적용

RLS는 운영 표준이지만 완벽한 보안은 아닙니다. 깊은 분리가 필요하면 DB 자체를 분리하는 게 정석입니다.

멀티 테넌트의 표준 결정: 적은 테넌트(N < 100)면 별 데이터베이스, 많으면 RLS + tenant_id 컬럼. 둘이 섞이면 운영 복잡도 폭증합니다.

정리

  • RLS = row 단위 권한. 재작성 시 WHERE 자동 추가
  • ENABLE 후 정책 정의합니다. 보안 민감 테이블은 FORCE
  • USING = 읽기 조건, WITH CHECK = 쓰기 조건
  • 다중 정책: PERMISSIVE = OR, RESTRICTIVE = AND
  • 인덱스 필수 — 정책 조건이 자주 들어가는 컬럼
  • 슈퍼유저·BYPASSRLS·owner(FORCE 없을 시)는 우회
  • SECURITY DEFINER 함수는 RLS 우회 가능 — 주의

다음 절(9.4)에서는 더 세밀한 권한 — 컬럼·뷰 권한SECURITY DEFINER/INVOKER 차이를 봅니다.