본문으로 건너뛰기
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이 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 존재를 누출
카탈로그 SELECTpg_class.reltuples 등은 RLS 안 적용

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

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

정리

RLS는 row 단위 권한으로, 재작성 단계에서 조건이 자동으로 WHERE에 붙습니다. 테이블에 ENABLE한 뒤 정책을 정의하고, 보안이 민감한 테이블은 소유자까지 걸리도록 FORCE를 더합니다. 슈퍼유저·BYPASSRLS 역할·(FORCE가 없을 때의) 소유자는 정책을 우회하고, SECURITY DEFINER 함수도 우회 통로가 되므로 주의합니다.

  • USING = 읽기 조건, WITH CHECK = 쓰기 조건
  • 다중 정책: PERMISSIVE는 OR, RESTRICTIVE는 AND
  • 정책 조건이 자주 걸리는 컬럼에는 인덱스 필수