멀티테넌트 앱에서 데이터 누출 방지

발행: (2026년 3월 19일 AM 04:22 GMT+9)
5 분 소요
원문: Dev.to

Source: Dev.to

왜 애플리케이션 로직만으로는 부족한가: 데이터베이스 수준 행 수준 보안(RLS)의 필요성

견고한 멀티‑테넌트 SaaS를 구축했습니다. 모든 쿼리에 tenant_id 필터를 적용하고, 미들웨어 검증을 추가했으며, 단위 테스트도 작성했습니다. 하지만 애플리케이션 계층 보안은 취약합니다: 필터를 놓치거나, 악의적인 스크립트가 실행되거나, 직접 데이터베이스에 연결하면 전체 방어망을 우회할 수 있습니다.
보안 경계를 코드가 아닌 데이터베이스 자체로 옮기세요.

짧은 요약

이번 심층 분석에서는 Row‑Level Security (RLS)가 단순히 “Postgres 기능”이라는 신화를 깨고, 이를 SQLAlchemy ORM 및 Alembic 마이그레이션 워크플로에 매끄럽게 통합하는 방법을 보여줍니다. 이론을 넘어 실제 운영 환경에 적용 가능한 구현 세부 사항까지 다룹니다.

WHERE tenant_id = ?만으로 코드를 작성하는 것이 왜 시간 폭탄인지

  • 필터는 애플리케이션 계층에 존재하므로 누락되거나 재정의될 수 있습니다.
  • 직접 데이터베이스에 접근하는 경우(예: 관리자 도구, ad‑hoc 쿼리) 이러한 필터가 적용되지 않습니다.
  • 한 번의 실수만으로 모든 테넌트의 데이터를 노출할 수 있습니다.

CI/CD 파이프라인을 깨뜨리지 않고 RLS를 활성화하고 정책을 정의하는 단계별 Alembic 마이그레이션 스크립트

  1. 마이그레이션 생성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);
        """)
  2. 정방향/역방향이 가능한 다운그레이드 – 정책을 삭제하고 RLS를 비활성화합니다.

  3. 마이그레이션 실행 – 일반 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/에서 확인하세요.

0 조회
Back to Blog

관련 글

더 보기 »