Skip to content

[Security] PostgreSQL RLS 기반 사업장 2차 격리 안전 도입 #34

Description

@hywznn

한 줄 요약

현재의 ActorContext + company_id 범위 Repository + DB 제약을 유지하면서, PostgreSQL이 다른 사업장의 행을 한 번 더 차단하도록 RLS(Row Level Security)를 안전하게 도입합니다.

이 작업은 현재 MVP 구현을 막지 않는 P1 보안 고도화입니다. RLS를 성급하게 켜서 로그인과 공개 링크가 중단되지 않도록 선행조건을 먼저 갖춥니다.

쉽게 설명하면

지금도 서버는 사용자의 사업장을 확인하고 해당 사업장 데이터만 조회합니다. RLS는 서버 코드에 실수가 생겨도 PostgreSQL이 다른 사업장 데이터의 조회·저장을 한 번 더 막아주는 DB의 두 번째 자물쇠입니다.

JWT 또는 보안 링크
→ Server가 신뢰할 수 있는 company_id 결정
→ Transaction에 company_id 설정
→ Repository의 company_id 조건
→ PostgreSQL RLS가 같은 company_id 행만 허용
→ FK·UNIQUE·CHECK가 교차 사업장 연결 차단

RLS가 기존 권한 검사와 Repository 범위 조건을 대신하지는 않습니다.

배경

PR #32에서는 company, user_account, worker, task, ticket, document에 RLS 정책을 먼저 추가했지만, Spring Boot가 transaction마다 tenant context를 설정하는 코드와 로그인·Refresh Token·Worker Link의 bootstrap 흐름이 없었습니다.

또한 공식 식별자인 company_id 대신 company_name을 tenant UUID처럼 사용했고, local/test에서는 flyway.target: 1로 migration 자체를 건너뛰고 있었습니다. 이 상태로 운영하면 모든 조회가 막히거나 connection pool에서 잘못된 tenant context가 사용될 위험이 있었습니다.

PR #33에서는 MVP 기준선을 복구하고, ADR-0002에 아래 RLS 도입 조건을 기록했습니다. 이 이슈는 그 후속 구현을 담당합니다.

시작 전 확정할 결정

  • RLS 도입·배포·롤백 순서를 새 ADR로 Proposed → Accepted 처리합니다.
  • migration 계정과 애플리케이션 계정을 분리합니다.
  • 애플리케이션 계정은 SUPERUSER, BYPASSRLS, table owner 권한을 갖지 않습니다.
  • PostgreSQL 전용 migration과 H2 local migration을 분리하는 방식을 정합니다.
  • 로그인·Refresh Token·Worker Link가 tenant를 찾기 전 수행할 제한된 bootstrap 조회 방식을 정합니다.

구현 범위

1. DB Role 분리

  • Flyway migration 전용 계정과 Spring Boot 실행 계정을 분리합니다.
  • 애플리케이션 계정에 필요한 table 권한만 부여합니다.
  • 운영 설정과 .env.example에는 변수 이름만 기록하고 credential은 저장하지 않습니다.

2. Transaction tenant context

  • 인증된 요청의 ActorContext.companyId를 신뢰 원본으로 사용합니다.
  • tenant가 필요한 transaction 시작 시 SET LOCAL app.company_id = ...를 실행합니다.
  • Request body와 query parameter의 company_id를 tenant context로 신뢰하지 않습니다.
  • transaction 밖에서 tenant 테이블을 접근하려 하면 fail-closed 되도록 합니다.

3. Bootstrap 흐름

  • 로그인은 email로 사용자를 찾은 뒤 안전하게 company_id를 확정할 수 있어야 합니다.
  • Refresh Token은 원본이 아닌 token hash 조회로 사용자와 company_id를 확정합니다.
  • Worker Link는 token hash로 허용된 worker link와 company_id만 확정합니다.
  • bootstrap 조회 권한으로 일반 근로자·업무·서류를 조회할 수 없어야 합니다.

4. RLS 정책

  • 테이블을 소유한 기능 Issue의 migration과 분리해 후속 migration으로 정책을 추가합니다.
  • SELECT, INSERT, UPDATE, DELETE 모두 같은 사업장 행만 허용합니다.
  • tenant context 누락·빈 값·잘못된 UUID는 우회가 아니라 차단으로 처리합니다.
  • RLS와 별도로 tenant-aware 복합 FK·UNIQUE 제약을 유지합니다.
  • Worker·Task·Document 등 아직 구현되지 않은 테이블을 이 이슈에서 미리 만들지 않습니다.

5. Connection pool 안전성

  • A 사업장 요청 후 같은 DB connection을 B 사업장 요청이 재사용해도 A의 context가 남지 않습니다.
  • commit·rollback·예외·timeout 뒤에도 tenant context가 다음 요청으로 새지 않습니다.
  • transaction이 없는 잘못된 Repository 호출을 테스트에서 발견할 수 있게 합니다.

6. 관측과 배포

  • RLS 차단 오류를 안전한 내부 error code와 request_id로 추적합니다.
  • SQL, JWT, token, 개인정보를 오류 응답과 일반 로그에 남기지 않습니다.
  • code 배포 → DB Role 적용 → RLS migration → Smoke Test 순서로 staging에서 검증합니다.
  • 문제 발생 시 이전 정책으로 되돌리는 forward-only migration 또는 runbook을 준비합니다.

초보자용 구현 순서

  1. RLS를 바로 켜지 말고 로그인·Refresh Token·Worker Link의 bootstrap 흐름을 먼저 그림으로 정리합니다.
  2. PostgreSQL에 migration용 계정과 실행용 계정을 각각 만듭니다.
  3. Spring transaction 안에서만 SET LOCAL app.company_id가 실행되는 작은 adapter를 구현합니다.
  4. 기존 company, user_account, refresh_token부터 staging 정책을 적용합니다.
  5. A 사업장과 B 사업장 데이터로 조회·생성·수정·삭제 차단 테스트를 작성합니다.
  6. connection을 반복 재사용하는 누수 테스트를 작성합니다.
  7. Worker·Task·Document는 각 테이블이 구현될 때 같은 규칙으로 정책을 확장합니다.
  8. Smoke Test와 롤백 연습 후에만 운영 또는 데모 PostgreSQL에 적용합니다.

완료 조건

  • 애플리케이션 DB 계정은 RLS를 우회할 수 없습니다.
  • A 사업장은 SQL 조건이 실수로 빠져도 B 사업장의 행을 조회·생성·수정·삭제할 수 없습니다.
  • tenant context가 없으면 보호 테이블 접근이 차단됩니다.
  • 로그인·Refresh Token·Worker Link의 정상 흐름은 RLS 적용 후에도 동작합니다.
  • connection pool 재사용에서 사업장 context가 섞이지 않습니다.
  • PostgreSQL 통합 테스트가 일반 애플리케이션 DB 계정으로 실행됩니다.
  • local H2 테스트가 최신 공통 migration을 건너뛰지 않고 통과합니다.
  • ADR, README, 배포 설정, Smoke/rollback runbook이 갱신됩니다.

이번 이슈에서 하지 않는 것

  • Application Service의 역할·사업장 권한 검사 제거
  • Repository의 company_id 조건 제거
  • Worker·Task·Document 테이블 선행 생성
  • AI 또는 Knowledge 저장소의 DB 권한 설계
  • PostgreSQL superuser credential의 애플리케이션 사용

선행·연결 관계

Metadata

Metadata

Assignees

Labels

area:infraServer Dockerfile·DB 설정·CI hook·배포 가능성 영역; 통합 인프라 운영은 infra 저장소와 조율area:serverSpring Boot API·도메인·DB·tenant·Task Workflow 영역; Prompt·모델·Provider 구현 제외priority:P1핵심 작업 다음으로 처리할 중요 작업security:privacy개인정보·접근권한·토큰·보안 영향이 있는 작업status:in-progress담당자가 현재 구현 중인 작업type:chore저장소 설정·의존성·유지보수 작업

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions