멀티테넌트 앱에서 데이터 누출 방지
Source: Dev.to
왜 애플리케이션 로직만으로는 부족한가: 데이터베이스 수준 행 수준 보안(RLS)의 필요성
견고한 멀티‑테넌트 SaaS를 구축했습니다. 모든 쿼리에 tenant_id 필터를 적용하고, 미들웨어 검증을 추가했으며, 단위 테스트도 작성했습니다. 하지만 애플리케이션 계층 보안은 취약합니다: 필터를 놓치거나, 악의적인 스크립트가 실행되거나, 직접 데이터베이스에 연결하면 전체 방어망을 우회할 수 있습니다.
보안 경계를 코드가 아닌 데이터베이스 자체로 옮기세요.
짧은 요약
이번 심층 분석에서는 Row‑Level Security (RLS)가 단순히 “Postgres 기능”이라는 신화를 깨고, 이를 SQLAlchemy ORM 및 Alembic 마이그레이션 워크플로에 매끄럽게 통합하는 방법을 보여줍니다. 이론을 넘어 실제 운영 환경에 적용 가능한 구현 세부 사항까지 다룹니다.
WHERE tenant_id = ?만으로 코드를 작성하는 것이 왜 시간 폭탄인지
- 필터는 애플리케이션 계층에 존재하므로 누락되거나 재정의될 수 있습니다.
- 직접 데이터베이스에 접근하는 경우(예: 관리자 도구, ad‑hoc 쿼리) 이러한 필터가 적용되지 않습니다.
- 한 번의 실수만으로 모든 테넌트의 데이터를 노출할 수 있습니다.
CI/CD 파이프라인을 깨뜨리지 않고 RLS를 활성화하고 정책을 정의하는 단계별 Alembic 마이그레이션 스크립트
-
마이그레이션 생성 –
tenant_id컬럼이 없을 경우 추가하고, 대상 테이블에 RLS를 활성화합니다.# alembic revision script from alembic import op import sqlalchemy as sa def upgrade(): op.execute("ALTER TABLE orders ENABLE ROW LEVEL SECURITY;") op.execute(""" CREATE POLICY tenant_isolation ON orders USING (tenant_id = current_setting('app.current_tenant')::uuid); """) -
정방향/역방향이 가능한 다운그레이드 – 정책을 삭제하고 RLS를 비활성화합니다.
-
마이그레이션 실행 – 일반 CI/CD 프로세스의 일부로 실행합니다; 명령은 멱등(idempotent)하며 프로덕션에서도 안전합니다.
contextvars와 이벤트 리스너를 사용해 SQLAlchemy 세션에 동적 테넌트 컨텍스트 주입하기
import contextvars
from sqlalchemy import event
from sqlalchemy.orm import Session
# 현재 테넌트 UUID를 보관하는 컨텍스트 변수
current_tenant = contextvars.ContextVar("current_tenant")
def set_tenant(tenant_id: str):
current_tenant.set(tenant_id)
@event.listens_for(Session, "before_flush")
def apply_tenant_setting(session, flush_context, instances):
tenant = current_tenant.get(None)
if tenant:
session.execute(
f"SET LOCAL app.current_tenant = '{tenant}'"
)
- 각 요청 시작 시(
FastAPI미들웨어 등)set_tenant()를 호출합니다. - 이후 실행되는 모든 ORM 쿼리는 자동으로 테넌트 컨텍스트를 상속받으며, 데이터베이스가 RLS 정책을 강제합니다.
주요 함정
- 관리자 우회 전략: 슈퍼유저 역할이 자동으로 RLS를 비활성화하지 않도록 하고, 필요하다면 관리자 계정을 위한 명시적 정책을 생성합니다.
- 성능 인덱싱:
tenant_id컬럼(및 RLS 프레디케이트에 사용되는 컬럼)에 인덱스를 생성해 전체 테이블 스캔을 방지합니다. - 정책 유형:
- PERMISSIVE 정책은 허용된 행에 추가합니다.
- RESTRICTIVE 정책은 접근 가능한 정확한 행 집합을 정의합니다. 보안 모델에 맞는 유형을 선택하세요.
개발자가 필터를 잊어버릴 것이라고 기대하지 마세요. 가장 중요한 곳, 즉 데이터 레이어에서 보안을 강제하십시오.
전체 기술 가이드와 완전한 코드 예제, 마이그레이션 템플릿, 테스트 전략은 https://www.adrianovieira.eng.br/en/posts/architecture/row-level-security-sqlachemy-alembic-guide/에서 확인하세요.