Skip to content

feat: PostgreSQL RLS 기반 사업장 2차 격리 활성화 - #135

Draft
krestar wants to merge 3 commits into
mainfrom
feat/34-enable-psql-rls
Draft

feat: PostgreSQL RLS 기반 사업장 2차 격리 활성화#135
krestar wants to merge 3 commits into
mainfrom
feat/34-enable-psql-rls

Conversation

@krestar

@krestar krestar commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

현재 상태: Draft / merge 금지

이 PR이 main에 merge되면 배포 workflow가 즉시 실행되고 V42가 live DB에 적용됩니다.
아래 Merge 조건배포 전 절차를 모두 충족한 뒤에만 Ready for review 및 merge합니다.

왜 필요한가요?

현재의 ActorContext + company_id 범위 Repository + tenant-aware DB 제약을 유지하면서,
PostgreSQL이 다른 사업장의 행을 한 번 더 차단하도록 준비된 RLS 정책을 실제로 활성화합니다.

이 PR은 RLS가 애플리케이션 권한 검사나 Repository의 company_id 조건을 대신하게 하지 않습니다.
애플리케이션 코드에 범위 조건이 누락되더라도 runtime DB role에서 다른 사업장 행을 조회·생성·수정·삭제할 수 없도록 DB 차단 계층을 추가합니다.

main merge 즉시 배포와 수동 Smoke Test가 이어지므로, PR merge만으로 Issue를 자동 종료하지 않습니다.
live 검증까지 끝난 후 #34를 수동으로 닫습니다.

무엇이 바뀌나요?

DB migration

  • PostgreSQL 전용 V42__enable_postgresql_rls.sql을 추가합니다.
  • tenant 정책이 준비된 38개 테이블에 ENABLE ROW LEVEL SECURITY를 적용합니다.
  • FORCE ROW LEVEL SECURITY는 적용하지 않습니다.
    • 애플리케이션 runtime role은 table owner가 아니며 RLS 적용 대상입니다.
    • Flyway migration role과 table owner의 migration 수행 경계는 유지합니다.
  • migration transaction에 다음 안전 제한을 둡니다.
    • lock_timeout = 5s
    • statement_timeout = 30s
  • 컬럼·데이터·API 계약·production Java 코드는 변경하지 않습니다.

테스트 계약

  • PostgreSQL schema 테스트에서 다음을 정확히 검증합니다.
    • 정책이 존재하는 테이블: 38개
    • relrowsecurity = true인 테이블: 동일한 38개
    • relforcerowsecurity = true인 테이블: 0개
  • 누락돼 있던 notification schema 검증을 포함합니다.
  • RLS 활성화 이후 timeout 테스트가 tenant context 없이 UPDATE 0 rows로 통과하는 false positive를 막습니다.
    • runtime transaction마다 fixture company를 transaction-local tenant context로 설정합니다.
    • 실제 update affected row가 1인지 검증합니다.

영향받는 테이블

총 38개입니다.

  • 인증·회사: company, company_settings, user_account, refresh_token, user_agreement_consent, password_reset_token
  • 근로자·업무·문서: worker, worker_document, stored_file, task, task_checklist_item, task_transition_history, document_request_draft, document_request_draft_type, approval_request, external_submission, task_evidence, audit_event, workflow_case, document_ocr_run, notification
  • Worker Link·Import: worker_link, worker_response, worker_response_upload, worker_document_upload_idempotency, worker_import_job, worker_import_row, worker_import_commit_idempotency
  • AI: ai_run, ai_attempt, ai_question, ai_candidate, ai_candidate_decision_batch, ai_candidate_decision, ai_candidate_decision_task
  • Background/Outbox: event_publication, event_consumption, outbox_manual_retry

어떻게 검증했나요?

자동 테스트

PostgreSQL 16.14를 사용한 최신 로컬 test report 기준입니다.

test suites: 107
tests: 485
failures: 0
errors: 0
skipped: 0

실행 명령:

$env:POSTGRES_TEST_ENABLED = "true"
$env:POSTGRES_TEST_URL = "jdbc:postgresql://localhost:5432/fowoco_test"
$env:POSTGRES_TEST_USERNAME = "postgres"
$env:POSTGRES_TEST_PASSWORD = "<local-test-password>"
./gradlew.bat clean test

주요 PostgreSQL 검증 결과:

  • PostgreSqlMigrationTests: 1/1 통과
  • PostgreSqlRuntimeTimeoutBehaviorIntegrationTest: 3/3 통과
  • PostgreSqlRestrictedRoleHttpE2ETest: 7/7 통과
  • PostgreSqlRlsIsolationTest: 1/1 통과
  • PostgreSqlTenantDatabaseContextTest: 7/7 통과
  • OutboxIntegrationTest: 6/6 통과
  • AuthRefreshPostgreSqlConcurrencyTest: 1/1 통과

restricted-role negative probe에서 기록되는 SQLSTATE 42501은 의도한 access-denied 결과입니다.

이 결과는 현재 branch HEAD 기준입니다. branch가 최신 main보다 #132, #134 두 commit 뒤에 있고,
#125의 migration도 아직 반영되지 않았습니다. #125가 V40/V41로 merge된 최신 main 반영 후
새 PostgreSQL DB/schema에서 V1 → V39 → V40 → V41 → V42 전체 migration과 전체 테스트를 다시 실행해야 합니다.
Gradle clean은 외부 PostgreSQL DB를 초기화하지 않습니다.

수동 RLS 활성화 SQL 검증

아래 검증은 동일한 RLS 활성화 SQL이 임시 V40 번호였을 때 수행했습니다. Seed/RLS 충돌과 runtime 동작을 확인한
증거로는 유효하지만, 최종 V42 migration 순서를 증명하지는 않습니다. #125의 V40/V41 merge 후 새 DB에서
V39 → V40 → V41 → V42 순서를 다시 검증합니다.

  1. PostgreSQL 16 컨테이너에 fowoco_migrationfowoco_runtime을 분리해 생성했습니다.
  2. V38까지만 적용하고 DEMO_SEED_ENABLED=true로 기존 Demo/Test fixture를 생성했습니다.
  3. V38 상태에서 다음을 확인했습니다.
    • Flyway 최신 버전 38
    • RLS policy table 38
    • RLS enabled table 0
  4. infra #11의 bootstrap 함수 7개 EXECUTE 권한을 runtime role에 부여했습니다.
  5. 같은 DB에 RLS 활성화 migration을 적용했습니다.
  6. DEMO_SEED_ENABLED=true 재기동이 아래 예상 원인으로 실패함을 확인했습니다.
    • tenant context 없는 seed 조회가 기존 company 행을 보지 못함
    • seed가 신규 company insert를 시도함
    • RLS WITH CHECK가 SQLSTATE 42501로 차단함
  7. 같은 RLS 활성 DB에서 DEMO_SEED_ENABLED=false, outbox enabled 상태로 정상 기동했습니다.

수동 runtime Smoke Test

  • Demo/Test 회사 계정으로 각각 login 및 /api/v1/auth/me 성공
  • Refresh Token 재발급과 logout 성공
  • 신규 signup 성공
  • Swagger UI에서 인증 흐름 확인
  • Client /documents에서 Demo/Test 계정별 worker/task 격리 확인
  • runtime role의 context 없는 company 조회가 0건임을 확인
  • Demo company context에서는 Demo company 행만 조회됨을 확인
  • Test company context에서는 Test company 행만 조회됨을 확인
  • bootstrap_company_id_by_normalized_email, bootstrap_company_id_by_refresh_token_hash, bootstrap_claim_event_publications 호출에 permission/RLS 오류가 없음을 확인
  • live catalog query와 동일한 방식으로 bootstrap 함수 7개 모두 runtime EXECUTE = true 확인
  • outbox claim scheduler의 반복 호출에 permission denied, SQLSTATE 42501, scheduled-task 오류가 없음을 확인

현재 수동 로그의 outbox 검증은 빈 polling 경로까지 확인했습니다.
Merge 전 staging에서는 실제 event 1건의 claim → tenant context 설정 → handler → complete까지 추가로 확인합니다.

별도 관찰 사항

GET /api/v1/notifications를 cursor 없이 호출할 때 PostgreSQL이 nullable cursor placeholder 타입을 추론하지 못하는
SQLSTATE 42P18이 관찰됐습니다.
RLS, bootstrap 권한, outbox와 독립적인 notification 조회 쿼리 문제이며 이 PR에서는 수정하지 않습니다.

Demo Seed 주의사항

이 PR은 Demo Seed 코드나 기존 fixture 데이터를 변경하지 않습니다.

현재 배포 환경은 DEMO_SEED_ENABLED=true이지만, 현재 main의 Demo Seed runner는 RLS 활성 상태와 호환되지 않습니다.
V42는 Flyway transaction에서 먼저 commit되고 그 후 seed runner가 실패하므로,
Pod가 기동하지 못해도 DB는 V42/RLS 활성 상태로 남습니다.
단순 image rollback으로 V42를 되돌릴 수 없습니다.

Seed를 false로 변경해도 기존 PostgreSQL PVC의 Demo/Test 데이터는 삭제되지 않습니다.
다만 재기동 시 fixture를 자동 복구하거나 보충하지 않습니다.

따라서 merge 전에 아래 두 선택지 중 하나를 명시적으로 승인해야 합니다.

  1. Seed 호환 변경 선배포
    • 회사별 tenant context와 transaction 경계를 갖는 Seed 호환 변경을 V42보다 먼저 배포합니다.
    • DEMO_SEED_ENABLED=true + 제한 runtime role + RLS enabled 재기동을 별도 DB에서 검증합니다.
  2. Demo 상황임을 고려한 권장: Seed 비활성 유지
    • V42 merge 전에 live server-envDEMO_SEED_ENABLED=false를 적용합니다.
    • 기존 image를 먼저 재시작해 기존 Demo/Test login과 화면 데이터가 보존되는지 확인합니다.
    • V42 배포 후에도 Seed를 계속 false로 유지합니다.
    • Seed 호환 변경이 배포·검증되기 전에는 다시 true로 바꾸지 않습니다.

팀 요구사항이 “배포 환경에서 Demo Seed 활성 유지”라면 1번이 hard gate입니다.
2번을 선택하려면 Seed 비활성 유지 기간과 fixture 자동 복구가 없는 운영 제약을 담당자가 명시적으로 승인해야 합니다.

열린/최근 PR과 migration 순서

2026-08-11 확인 기준입니다.

PR 현재 상태 이 PR에 미치는 영향
#132 문서 정리 merged 현재 branch가 해당 commit 전이므로 최신 main 반영 필요
#134 Worker Link SMS merged common migration V39를 소유함
#125 이벤트 기반 알림 open draft 기존 V37/V38 migration을 V40/V41로 renumber한 뒤 먼저 merge해야 하는 hard prerequisite
#131 Renewal runtime open draft 신규 migration 없음. V42 이후 task, document_request_draft 쓰기 경로 회귀 테스트 필요

Merge 조건

아래 항목을 모두 충족하기 전에는 merge하지 않습니다.

코드·migration

Seed

  • 위 Seed 선택지 1 또는 2를 담당자가 명시적으로 승인
  • 선택지 1이면 DEMO_SEED_ENABLED=true + RLS enabled 재기동 성공
  • 선택지 2이면 merge 전에 live Seed를 false로 변경하고 기존 image 재기동 성공
  • Seed 비활성 상태에서도 기존 Demo/Test login과 주요 fixture 화면 정상

live DB preflight

  • live flyway_schema_history의 V39, V40, V41 success와 V42 미적용 확인
  • 애플리케이션 pool 연결이 fowoco_runtime을 사용하는지 확인
  • runtime role: rolsuper=false, rolbypassrls=false, rolcreaterole=false, rolcreatedb=false
  • runtime role이 38개 보호 테이블의 owner가 아님
  • runtime role이 migration role member가 아님
  • runtime role에 필요한 table DML·sequence 권한만 있고 TRUNCATE, REFERENCES, DDL 권한이 없음
  • bootstrap 함수 7개 모두 runtime EXECUTE=true
  • infra #11은 기존 PVC에서 initdb를 다시 실행하지 않으므로, merge 여부가 아니라 live catalog 결과로 권한 확인
  • V42 적용 전 policy table 38개, RLS enabled table 0개 확인

rollout 준비

  • 배포 담당자와 낮은 트래픽 시간대 확정
  • rollout 시간 동안 다른 main merge 중지
  • DB snapshot/복구 지점 확인
  • V42와 동일한 38개 테이블을 DISABLE ROW LEVEL SECURITY하는 V43 rollback patch 준비 및 리뷰
  • health endpoint만이 아니라 아래 수동 Smoke Test 담당자 배정
  • 실제 outbox event 1건의 claim → handler → complete 검증 방법 준비

배포 절차

server/.github/workflows/deploy.ymlmain push마다 자동 배포합니다.
따라서 merge가 곧 live DB migration 시작 버튼입니다.

1. Merge 전

  1. 다른 main merge를 중지합니다.
  2. DB snapshot 또는 복구 지점을 확인합니다.
  3. infra Wiki 절차에 따라 server-env Secret의 Seed 설정을 결정한 값으로 변경합니다.

임시 비활성 선택 시 예시:

kubectl -n fowoco patch secret server-env \
  --type merge \
  -p '{"stringData":{"DEMO_SEED_ENABLED":"false"}}'

kubectl -n fowoco rollout restart deployment/server
kubectl -n fowoco rollout status deployment/server --timeout=180s
kubectl -n fowoco exec deployment/server -- printenv DEMO_SEED_ENABLED

Secret 변경만으로 기존 Pod 환경변수는 바뀌지 않으므로 V42 merge 전에 반드시 기존 image Pod를 재시작합니다.

  1. feat: 이벤트 기반 알림 생성 로직 #125 배포가 끝난 main/V41 상태에서 health, Demo/Test login, worker/task 화면을 확인합니다.
  2. live catalog preflight 결과를 PR 또는 배포 기록에 남깁니다. Secret 값과 credential은 기록하지 않습니다.
  3. 배포 담당자, Smoke Test 담당자, rollback 담당자가 모두 준비된 뒤 PR을 merge합니다.

2. Merge 및 V42 적용

  1. PR merge 직후 deploy workflow를 단독으로 모니터링합니다.
  2. 동일 시간대에 다른 workflow 재실행이나 main merge를 하지 않습니다.
  3. 새 Pod의 Flyway 로그에서 V41 현재 상태와 V42 적용 성공을 확인합니다.
  4. migration은 38개 table에 lock을 요청합니다. lock_timeout=5s 또는 statement_timeout=30s로 실패하면 원인을 확인한 뒤 별도 시간대에 재시도합니다.
  5. migration transaction이 실패했을 때 부분 활성화가 남지 않았는지 catalog로 확인합니다.

3. 배포 후 Smoke Test

자동 health check 외에 다음을 제한 runtime role로 확인합니다.

  • readiness와 /health 200
  • signup
  • Demo/Test login
  • /api/v1/auth/me
  • refresh 및 logout
  • Demo/Test tenant A/B worker/task/document 격리
  • Worker Link bootstrap·조회·저장
  • context 없는 보호 table 접근 fail-closed
  • actual outbox event claim → handler → complete
  • permission denied, SQLSTATE 42501, RLS policy violation, scheduled-task 오류 없음
  • Flyway version V42, policy table 38, RLS enabled table 38, FORCE table 0

4. Seed 재활성화

현재 Seed 코드로는 재활성화하지 않습니다.

  • Seed 호환 변경이 배포됐고 DEMO_SEED_ENABLED=true + RLS enabled 재기동을 통과한 경우에만 true로 되돌립니다.
  • true로 변경한 뒤에도 Secret 적용을 위해 Pod 재시작과 Demo/Test login Smoke Test가 필요합니다.

장애 대응·rollback

  • Seed 관련 기동 실패면 먼저 DEMO_SEED_ENABLED=false가 실제 새 Pod에 반영됐는지 확인합니다.
  • 이미 적용된 V42 파일을 수정하거나 checksum을 바꾸지 않습니다.
  • 공유 DB에서 flyway clean, schema history 수동 수정, flyway repair로 되돌리지 않습니다.
  • RLS 때문에 핵심 흐름이 중단되면 다음 사용 가능한 Flyway version으로 준비한 rollback migration을 배포합니다.
    • 현재 순서 기준 후보는 V43이지만 merge 시점의 최신 번호를 다시 확인합니다.
    • V42와 동일한 38개 테이블에 DISABLE ROW LEVEL SECURITY를 적용합니다.
    • Repository의 company_id 범위와 tenant-aware 제약은 그대로 유지합니다.
  • 긴급 수동 DISABLE ROW LEVEL SECURITY는 배포 담당자 승인과 실행 기록이 있는 최후 수단으로만 사용하고,
    이후 동일 상태를 표현하는 forward migration을 반드시 추가합니다.
  • rollback 후 health, login/refresh, tenant A/B, outbox Smoke Test를 다시 수행합니다.

보안·개인정보

  • API response, DTO, 로그에 credential·JWT·원본 token·비밀번호를 추가하지 않음
  • runtime role은 RLS 우회 권한이나 table ownership을 전제로 하지 않음
  • tenant context 누락 시 fail-closed
  • 기존 Repository company_id 조건과 tenant-aware FK/UNIQUE를 유지
  • Accepted ADR-0004의 migration/runtime role 분리 원칙 유지
  • API·OpenAPI 계약 변경 없음

API·DB·운영 영향

  • API 변경: 없음
  • 데이터 변환: 없음
  • DB metadata 변경: 38개 table의 RLS 활성화
  • 새 환경변수: 없음
  • 배포 영향: 큼. main merge 즉시 live migration 실행
  • 호환성 주의: 현재 Demo Seed runner는 RLS 활성 상태와 호환되지 않음
  • 복구: 기존 migration 수정이 아닌 forward-only rollback migration 사용

- #125의 V40/V41 다음 V42 migration으로 tenant 보호 테이블 38개의 RLS를 활성화
- migration의 lock timeout과 statement timeout을 설정
- 정책 테이블, RLS 활성 테이블, FORCE 미적용 상태를 schema 테스트로 검증
- RLS 환경에서 runtime timeout 테스트가 실제 tenant context로 UPDATE를 수행하도록 보정
@krestar krestar added the area:infra Server Dockerfile·DB 설정·CI hook·배포 가능성 영역; 통합 인프라 운영은 infra 저장소와 조율 label Aug 11, 2026
@krestar
krestar marked this pull request as draft August 11, 2026 05:10
@BcKmini
BcKmini requested review from BcKmini and removed request for BcKmini August 11, 2026 07:16
@hywznn

hywznn commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Migration 번호 조율 공유드립니다.

제가 진행 중인 담당자 변경 PR #143의 공통 Migration에서 V42를 사용하기로 했습니다. 이 PR의 PostgreSQL 전용 V42__enable_postgresql_rls.sql은 **V43__enable_postgresql_rls.sql**로 변경 부탁드립니다.

권장 병합 순서는 다음과 같습니다.

  1. #143의 공통 V42 병합
  2. 이 브랜치에 최신 main 반영
  3. RLS Migration을 V43으로 변경
  4. 전체 테스트와 PostgreSQL Migration·RLS 테스트 확인 후 feat: PostgreSQL RLS 기반 사업장 2차 격리 활성화 #135 병합

서로 다른 migration 경로라도 Flyway version은 함께 비교되므로 V42가 겹치지 않도록 조정이 필요합니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:infra Server Dockerfile·DB 설정·CI hook·배포 가능성 영역; 통합 인프라 운영은 infra 저장소와 조율

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants