# MeshLaw 보안 백서

> 2026-05-21 v0.1 · 법무·IT 팀 검토용
>
> 본 문서는 MeshLaw 의 데이터 처리·격리·암호화·접근 통제 정책을 정리합니다. 구현 사실 (코드·마이그레이션·운영 설정) 기준이며, 검증·실사 시 해당 라인을 함께 제시합니다.

---

## 1. 시스템 구성 요약

| 컴포넌트 | 기술 | 비고 |
|---|---|---|
| 게이트웨이 | Moleculer (Node/Bun) `:4098` | HTTP 입구. JWT 검증 + RLS 컨텍스트 주입 |
| DB | PostgreSQL 16 + pgvector + pg_trgm | 단일 인스턴스 (단일 테넌트 = 단일 DB 인스턴스 가능) |
| AI 백엔드 | vLLM (사내 호스팅) | 외부 API 송신 X — 사용자 데이터가 외부 LLM 사로 안 나감 |
| 메일 | Resend (도메인 verify) | 가입·비밀번호 메일만, 매터 콘텐츠 송신 X |
| 모바일 푸시 | APNs + FCM | 토큰만 전송, 본문은 메타데이터 (id) 만 — 내용 X |

호스팅: Vultr 서울 리전 (현재) — 데이터 reside in Korea.
운영 SSH: key auth only · password auth disabled.

---

## 2. 테넌트 격리

### 2.1 데이터 격리 — PostgreSQL Row-Level Security

**모든 도메인 테이블이 RLS FORCE 활성**:
- 직접 격리 (team_id 컬럼): `matters`, `clients`, `advisories`, `audit_log`, `notifications`, `llm_usage`, `team_token_quota`, `messages`, `device_tokens`, `stripe_webhook_events`, `invoice_receipts`
- JOIN 격리 (matter_id → matters.team_id): `matter_events`, `invoices`, `timesheet_entries`, `writs`, `approvals`, `matter_refs`, `vault_files`, `matter_acl`, `deadlines`

정책: `team_id = app_current_team_id()` (USING + WITH CHECK).
`app_current_team_id()` 는 트랜잭션 GUC `app.team_id` 를 읽음.

### 2.2 ROLE 분리

운영 게이트웨이는 **NOSUPERUSER · NOBYPASSRLS** ROLE 로 DB 접속:
- ROLE: `meshlaw_app` (마이그 036)
- GRANT: SELECT/INSERT/UPDATE/DELETE on ALL TABLES (no DDL)
- 운영 적용: 2026-05-05 (DB 백업 후 `ALTER ROLE meshlaw_app PASSWORD` + 게이트웨이 `MESHLAW_DB_URL` 교체)

dev/마이그레이션 도구는 별도 superuser — 일상 운영 트래픽과 분리.

### 2.3 트랜잭션 GUC 주입

서비스 액션의 모든 도메인 쿼리는 `withTeamScope[Prisma]` wrapper 에서 실행:
```ts
sql.begin(async (tx) => {
  await tx`select set_config('app.team_id', ${teamId}, true)`;
  await tx`select set_config('app.user_id', ${userId}, true)`;
  return fn(tx);
});
```
- `SET LOCAL` 라 트랜잭션 종료 시 자동 해제
- 50/50 도메인 액션이 wrap 적용됨 (잔여 7건은 auth.signup/login 의 password_hash lookup 등 의도적 raw — 데이터 누출 경로 없음)

### 2.4 JWT → ctx.meta.user 매핑

게이트웨이 `onBeforeCall` 이 JWT 검증 → `ctx.meta.user = { id, team, role }` 주입. 도메인 라우트 모두 `AUTH_REQUIRED_PREFIXES` 에 포함되어 토큰 없으면 401.

### 2.5 회귀 보호

테넌트 격리는 e2e §7 (RLS) 에서 매 배포 전 회귀:
- 미인증 호출 → 401
- 잘못된 JWT → 401
- 정상 JWT 의 본인 team 데이터만 200 (`/db/matters/by-code?code=m01`)

---

## 3. 개인정보 보호 (PII)

### 3.1 주민등록번호

- **저장**: pgp_sym_encrypt 암호화 (BYTEA `rrn_encrypted`) + 마지막 4자리 (`rrn_last4`) — 마이그 001
- **노출**: 클라이언트 UI 는 `maskRRN()` 으로 강제 마스킹 → `XXXXXX-XXX{last4}`. 형식 어긋남이면 빈 문자열 (raw 노출 차단)
- 8 단위 테스트 회귀 (`mask-rrn.test.ts`)

### 3.2 비밀번호

- **저장**: bcrypt 해시
- **클라이언트**: 입력 시 4-rule 실시간 시각화 (8자/영문/숫자/특수)
- **전송**: HTTPS only. iOS Release 빌드 `NSAppTransportSecurity` 미설정 (기본 HTTPS-only)

### 3.3 토큰

- **JWT**: 짧은 TTL (`SESSION_TTL_SEC`, 기본 30분), HS256
- **Refresh token**: rotation + reuse detection (마이그 042 `refresh_tokens` + `services/utils/refresh-token.ts`)
  - 평문은 사용자에게 1회 전달, DB 에는 bcrypt 해시만
  - 사용 시 즉시 revoke + 새 token 발급
  - 재사용 감지 시 전체 chain revoke

### 3.4 의뢰인 정보

- 이메일·전화 평문 저장 (검색 needs). 접근은 RLS team scope.
- 매터별 추가 격리: `matter_acl` 테이블 + `aclGateAllow()` 헬퍼
  - ACL 행 0개면 fallback (team 범위만)
  - ACL 행 1개+ 있으면 명시적 권한자만 (owner/editor/viewer 계층)
  - 메신저 매터 채널이 채택: `messageList` viewer 이상 / `messageSend` editor 이상

---

## 4. 감사 로그

`audit_log` 테이블에 다음 액션이 자동 기록:
- 매터 CRUD, ACL grant/revoke
- 결재 승인/반려/수정요청 (status_before/after, comment)
- 청구서 발행, 수금 receipt (delta_won, total)
- 사용량 → invoice/timesheet rollup
- 참고문서 CRUD

각 row: `actor_name, target_kind, target_id, http_status, matter_id, meta(JSONB), created_at`.

RLS 정책 적용 (team_id) — 다른 테넌트 audit 조회 불가.

---

## 5. 권한 통제

### 5.1 결재 (matter approvals)

서버측 actor 가드 — `computeApprovalChainAdvance(steps, status, currentUserId)`:
- 활성 단계의 `approver_id` 와 일치하는 사용자만 액션
- 불일치 시 403 `NOT_APPROVER`
- 데스크톱 (`approvalPatch`) + 모바일 (`approvalDecideForMobile`) 동일 가드
- 7 단위 테스트

### 5.2 매터 ACL

- 4 역할: `owner` ⊃ `editor` ⊃ `viewer`
- `matter_acl` 테이블 + `aclCheck` 액션
- 메신저 v0.1.6 가 채택 (matter 채널)
- 다른 도메인은 RLS team scope 만 (광범위 — v0.2 에서 matter-level 강제 옵션 도입 가능)

### 5.3 RBAC (시스템 역할)

- `users.role_id` → `roles` 테이블 (admin/partner/associate/staff)
- `auth.invite` 가 역할 부여 (admin 만 발급 가능)

---

## 6. 데이터 처리

### 6.1 AI 모델 호출

- **사내 vLLM** (Tailscale 내부망) — 외부 API 송신 X
- LLM 요청 본문 (매터 컨텍스트·문서) 은 사외로 안 나감
- `llm_usage` 테이블에 호출 토큰·비용 기록 (RLS team scope)

### 6.2 파일 (Vault)

- **저장**: 서버 디스크 (`storage_directory` 매터별 분리)
- **메타데이터**: `vault_files` 테이블 (file_name, mime, size_bytes, text_content)
- **인덱싱**: text 추출 후 `text_content` 저장 → ⌘K 통합검색에 노출 (RLS 적용)
- **PDF 멀티모달**: 모바일·웹 첨부 시 게이트웨이가 mupdf 로 페이지 JPEG 변환 → vLLM 비전 인코더 (외부 X)

### 6.3 메일

- Resend 도메인 `meshlaw.ai` (DKIM/SPF/DMARC verify)
- 콘텐츠: 가입·invite·비밀번호 재설정만 — 매터 내용 X
- env 미설정 시 dev no-op (운영 토큰 누락 시 silent)

---

## 7. 인프라 보안

### 7.1 전송 보안

- 게이트웨이: HTTPS only (Caddy)
- iOS Release: `NSAllowsArbitraryLoads` 미설정 (HTTPS-only). Debug 만 `localhost` 허용
- Android Release: `BASE_URL = https://api.meshlaw.ai`. Debug 만 `10.0.2.2`

### 7.2 모바일 푸시

- APNs JWT (.p8) — Apple Developer Team ID + Key ID
- FCM HTTP v1 — Firebase service account JSON
- 푸시 본문: title/body + `data.type` (라우팅 hint) — **매터 본문 X**
- 디바이스 토큰 등록 시 `device_tokens` 테이블 (RLS team scope)

### 7.3 운영 접근

- SSH 접속: key 기반 인증 (운영 권장 — Vultr 콘솔에서 password 미설정 확인 필요)
- **DB**: docker-compose 가 `127.0.0.1:54322` 로 localhost bind 만 — 외부 인터넷 노출 X. 호스트 내 게이트웨이 컨테이너만 접근 (network_mode: host)
- 백업: `/var/backups/meshlaw-db-*.sql.gz` (현재 수동, 자동화 예정)
- 마이그 적용 시 사전 백업 의무 (운영 규율)

### 7.4 Stripe 결제

- Webhook signature 검증 (Stripe lib + 마이그 048 `stripe_webhook_events` 멱등성 가드)
- 카드 정보는 Stripe Vault — 자사 저장 X (PCI 범위 최소화)

---

## 8. 컴플라이언스 자세

| 항목 | 상태 |
|---|---|
| 개인정보보호법 (PIPA) | 통상 처리. 위탁 처리 양식 (DPA) 별도 제공 |
| ISMS-P | 검토 중 (인증 신청 시점 별도 공지) |
| KISA 마크 | 검토 중 |
| GDPR | 적용 대상 외 (국내 한정). 글로벌 출시 시 별도 정책 |
| ZDR (Zero Data Retention) | 정책 옵션 — Pro+ 플랜 계약 시 LLM 학습 비사용 약정 부속. 현재 사내 vLLM 만 사용하므로 외부 학습 노출 자체 없음 |

> ⚠️ ISMS-P / KISA 마크는 검토 단계로, 실제 인증 획득 시점은 별도 공지합니다. 본 백서 현재 버전에서는 "예정" 단계로 표기 — 인증 마크 사용은 획득 후에만 가능합니다.

---

## 9. 사고 대응

### 9.1 토큰 유출

- Refresh token 재사용 감지 시 사용자 전체 chain revoke (자동)
- 사용자 액션: `/auth/logout` 또는 비밀번호 변경 → 모든 refresh revoke

### 9.2 잘못된 데이터 노출

- 발견 즉시 docker-compose 롤백 (이전 이미지 태그):
  ```
  sudo cp /opt/meshlaw/docker-compose.yml.bak-<TS> /opt/meshlaw/docker-compose.yml
  sudo docker compose up -d api-gateway
  ```
- DB 복원: `/var/backups/meshlaw-db-*.sql.gz` 최신본

### 9.3 사용자 통보

- 정책 목표: 72시간 안에 영향 받은 사용자에게 이메일 통보 (개인정보보호법 §34 권고 수준)
- 현재 자동화는 없음 — 발생 시 운영자가 수동으로 영향 범위 산출·일괄 메일 발송
- 통보 내용: 영향 범위·복구 절차·사용자 액션 (비번 변경 등)

---

## 10. 데이터 이전·삭제

### 10.1 export

사용자는 자기 team 의 모든 데이터를 JSON 으로 export 가능 (`/admin` 마이그레이션 탭 역방향 — v0.2 예정).

### 10.2 삭제

- 매터·고객·서면 등 사용자가 UI 에서 직접 삭제 가능
- 계정 삭제: 이메일 요청 → 30일 grace period → cascade delete (FK ON DELETE)

### 10.3 보관 기간

기본 무기한. 사용자 요청 시 일괄 삭제. 법정 의무 보관 (소득세법 5년 등) 은 사용자 책임.

---

## 11. 검증 요청

본 백서의 사실 검증·실사 협조를 원하시면 [contact 페이지](/contact) 또는 영업 담당 이메일로 요청:
- 코드 경로 (라인 번호) 제공
- 마이그레이션 SQL 사본 제공
- 침투 테스트 결과 (수행 후) 제공 — 예정

> 별도 보안 담당 메일박스(`compliance@meshlaw.ai` 등) 는 도메인 verify 및 운영 준비 완료 시 별도 공지.

---

## 부록 A: 주요 코드 경로

| 영역 | 경로 |
|---|---|
| RLS 정책 | `server/db/migrations/031_rls_policies.sql` |
| meshlaw_app ROLE | `server/db/migrations/036_meshlaw_app_role.sql` |
| RLS 강제 helper | `server/services/utils/db.ts withTeamScope` |
| JWT + refresh | `server/services/auth/auth.service.ts` + `services/utils/refresh-token.ts` |
| RRN 마스킹 | `app/packages/app/src/features/client/mask-rrn.ts` |
| ACL gate | `server/services/db/messages-helpers.ts aclGateAllow` + `services/utils/acl.ts checkAccess` |
| 결재 actor 가드 | `server/services/db/workflow-helpers.ts computeApprovalChainAdvance` |
| 감사 로그 | `server/services/utils/audit.ts recordAudit` |
| 게이트웨이 인증 | `server/services/api-gateway/api-gateway.service.ts onBeforeCall` |
| 푸시 송신 (no body) | `server/services/utils/push-apns.ts` + `push-fcm.ts` |

## 부록 B: 마이그레이션 이력

030 (team_id 컬럼) · 031 (RLS 정책) · 036 (meshlaw_app ROLE) · 042 (refresh tokens) · 044 (firm settings) · 045 (invoice receipts) · 047 (device tokens) · 048 (stripe webhook idempotency) · 049 (matter_events standalone) · 050 (messages)
