멀티테넌트 DB - RLS(Row-Level Security) 정리

정세현·2026년 7월 21일

NHN AX Internship Notes

목록 보기
1/16

1. 테넌트(Tenant)란?

  • 하나의 서비스를 여러 고객사(회사)가 같이 쓰는 구조에서, 각 고객사를 "테넌트"라고 부름
  • 보통 DB/테이블을 회사별로 나누지 않고, 하나의 테이블에 tenant_id 컬럼으로 소유 회사를 구분해서 같이 저장
  • 핵심 요구사항: A회사가 로그인했을 때 B회사 데이터를 절대 봐서는 안 됨

2. RLS(Row-Level Security)란?

  • "어떤 세션이 어떤 행(row)을 볼 수 있는가"를 애플리케이션 코드가 아니라 DB 자체가 강제하는 PostgreSQL 보안 기능
  • 기존 방식(애플리케이션에서 매번 WHERE tenant_id = ? 작성)은 개발자 실수로 조건을 빠뜨리면 전체 데이터가 노출되는 위험이 있음
  • RLS는 이런 실수에도 DB가 마지막 안전장치 역할을 함 (심층 방어, defense in depth)
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON orders
  USING (tenant_id = current_setting('app.tenant_id'));

3. RLS를 "제대로" 적용하기 위한 4가지 원칙

3-1. BYPASSRLS 없는 전용 계정

  • PostgreSQL 슈퍼유저나 BYPASSRLS 속성이 있는 롤은 RLS 정책을 무시하고 전체 데이터를 다 봄
  • 애플리케이션은 반드시 이 속성이 없는 계정으로 접속해야 RLS가 실제로 동작함
CREATE ROLE app_user LOGIN PASSWORD '...' NOSUPERUSER NOBYPASSRLS;

3-2. FORCE ROW LEVEL SECURITY

  • PostgreSQL은 기본적으로 테이블 소유자(owner)에게는 RLS를 적용하지 않음 (관리자 편의를 위한 설계)
  • 운영/관리 계정이 실수로 소유자 권한으로 접속해 전체 데이터를 긁어가는 사고를 막기 위해 강제 적용 필요
ALTER TABLE orders FORCE ROW LEVEL SECURITY;

3-3. USING vs WITH CHECK

구분적용 시점대상 명령어
USING이미 존재하는 행을 대상으로 작업할 때 ("건드릴 수 있는 행인가")SELECT, DELETE, UPDATE(대상 선정)
WITH CHECK새로 쓰이거나 변경된 결과값을 검증할 때 ("결과물이 유효한가")INSERT, UPDATE(결과 검증)
CREATE POLICY tenant_select ON orders
  FOR SELECT USING (tenant_id = current_setting('app.tenant_id'));

CREATE POLICY tenant_delete ON orders
  FOR DELETE USING (tenant_id = current_setting('app.tenant_id'));

CREATE POLICY tenant_insert ON orders
  FOR INSERT WITH CHECK (tenant_id = current_setting('app.tenant_id'));

CREATE POLICY tenant_update ON orders
  FOR UPDATE
  USING (tenant_id = current_setting('app.tenant_id'))
  WITH CHECK (tenant_id = current_setting('app.tenant_id'));
  • UPDATE는 USING + WITH CHECK 둘 다 필요
    • USING: 원래 내 테넌트 행만 수정 대상으로 삼음
    • WITH CHECK: 수정 후에도 여전히 내 테넌트여야 함 (안 그러면 UPDATE로 tenant_id를 바꿔서 다른 테넌트로 행을 넘겨버릴 수 있음)
  • UPSERT(INSERT ... ON CONFLICT DO UPDATE)는 내부적으로 INSERT + UPDATE 양쪽 체크를 다 받음

3-4. tenant_id 변경 금지 트리거

  • WITH CHECK과 별개로, UPDATE로 tenant_id 자체를 바꿔서 소유권을 이전하는 것을 이중으로 막는 안전장치
CREATE OR REPLACE FUNCTION prevent_tenant_id_change()
RETURNS TRIGGER AS $$
BEGIN
  IF NEW.tenant_id <> OLD.tenant_id THEN
    RAISE EXCEPTION 'tenant_id cannot be changed';
  END IF;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_prevent_tenant_id_change
  BEFORE UPDATE ON orders
  FOR EACH ROW EXECUTE FUNCTION prevent_tenant_id_change();

4. 요약표

요소막는 시나리오
BYPASSRLS 없는 계정앱이 아예 RLS를 우회하는 상황
FORCE RLS테이블 소유자/관리 계정도 RLS 통과하게 강제
USING남의 테넌트 행을 읽거나 삭제
WITH CHECK남의 테넌트로 행을 새로 쓰거나, UPDATE로 넘겨버리기
트리거UPDATE로 tenant_id 자체를 바꿔서 소유권 이전

5. Go/Java 테스트 코드 검증 방향 (다음 단계)

  1. 테스트 DB에 실제 RLS 적용된 스키마 반영 (마이그레이션 스크립트 포함)
  2. SET app.tenant_id = 'tenant-A' 세션에서 tenant-B 데이터 조회/수정/삭제 시도 → 0건 반환 또는 예외 확인
  3. tenant-A 세션에서 tenant_id를 B로 바꾸는 UPDATE 시도 → 트리거 예외 발생 확인
  4. 애플리케이션 커넥션이 BYPASSRLS 없는 계정을 쓰는지 별도 검증
   SELECT rolbypassrls FROM pg_roles WHERE rolname = current_user;
profile
I'm the best

0개의 댓글