OpsMate Local 공개 데모 위협 모델
문서 상태와 범위
- 상태:
implemented,tested-component; 외부 배포 통제는 별도 gate - 대상: Spring MVC + Thymeleaf + HttpSession 공개 데모, PostgreSQL, 승인된 사설 GPU 모델 연결
- 보호 목표: 공개 방문자 격리, 결정적 업무 통제, credential 비공개, 모델·DB 비노출, 모델 의존 초안 경로의 장애 시 fail-closed
이 문서는 공개 데모의 위협, 구현된 통제와 아직 외부 환경에서 검증해야 할 gate를 구분합니다. 2026-08-04 전체 clean verify에서 54개 테스트가 성공했고 실패·오류·건너뜀은 0개였습니다. 문서나 스크립트가 존재한다는 사실만으로 실제 배포 통제가 적용됐다고 판단하지 않습니다.
보호할 자산
- 업무 상태의 무결성
DRAFT,PENDING_APPROVAL,APPROVED,REJECTED,ORDERED전이- 승인되지 않은 발주와 중복 발주 방지
- demo workspace 격리
- 방문자별 구매 요청, 발주, 감사 이벤트
- 인증과 운영 비밀
- 세션 식별자, DB credential, model proxy token, tunnel credential
- 승인된 사설 GPU 모델 호스트와 네트워크
- 승인된 계산 자원, 내부 경로와 연결 정보
- 서비스 가용성과 비용
- 제한된 GPU 메모리, 모델 처리 시간, DB와 애플리케이션 자원
- 공개 증거의 신뢰성
- 실제 구현·테스트·배포 상태와 문서 표현의 일치
공개 데모에는 실제 조직 데이터, 고객 데이터, 내부 정책, 사설 URL과 운영 계정을 저장하지 않습니다.
신뢰 경계
flowchart LR
U["신뢰하지 않는 브라우저 입력"] --> EDGE["Public edge"]
EDGE --> WEB["Spring web security boundary"]
WEB --> CONTEXT["DemoPrincipal + workspace boundary"]
CONTEXT --> DOMAIN["Service + domain invariants"]
DOMAIN --> DB[("Private PostgreSQL")]
DOMAIN --> LIMIT["Model invocation guard"]
LIMIT --> TUNNEL["Approved private tunnel"]
TUNNEL --> MODEL["Private model proxy + Ollama"]
MODEL -. "구조화된 제안만 반환" .-> DOMAIN
신뢰하지 않는 입력
- 자연어 구매 요청과 반려 사유
- URL path의 UUID
- persona 전환 요청
- idempotency key
- 모든 HTTP header와 proxy 전달 header
- 모델이 생성한 JSON과 정책 ID
제한적으로 신뢰하는 구성요소
- edge가 전달한 요청은 TLS가 적용됐더라도 사용자 입력으로 취급합니다.
- HttpSession은 서버가 생성한 값만 신뢰하고 브라우저가 전달한 actor/workspace는 사용하지 않습니다.
- 모델은 사설 서버에 있더라도 업무 권한을 부여하지 않습니다.
- DB 고유 제약과 transaction은 애플리케이션 사전 조회보다 강한 최종 불변식입니다.
보안 가정
- 공개 데모는 Spring 애플리케이션 한 인스턴스로 운영합니다.
- 모델 동시 실행은 전체 한 건으로 제한합니다.
- 사설 GPU 모델 호스트는 명시적으로 승인된 기간과 용도에서만 사용합니다.
- 모델과 DB는 인터넷에서 직접 접근할 수 없습니다.
- demo persona 전환은 체험 기능이며 실제 조직 IAM을 증명하지 않습니다.
- 입력 데이터는 합성 예시를 전제로 하며 만료 뒤 삭제합니다.
가정이 깨지면 관련 통제의 유효성도 다시 검토해야 합니다. 특히 애플리케이션을 여러 인스턴스로 늘리면 in-memory single-flight, rate limit과 semaphore는 충분하지 않습니다.
위협과 통제
| 위협 | 영향 | 설계 통제 | 필수 검증 |
|---|---|---|---|
| 공개 입력의 prompt injection | 모델이 승인, DB 접근 또는 외부 호출을 주장 | 모델에 도구·DB·임의 URL 권한을 주지 않고 서버가 조회한 합성 정책만 전달 | 공격 문자열에서도 모델 출력이 제안에 머물고 업무 쓰기는 서버 명령으로만 발생 |
| 모델의 fabricated policy ID | 허위 근거로 초안 생성 | 조회한 policy ID 부분집합인지 서버에서 다시 검증 | 존재하지 않는 ID, 다른 분류와 초과 금액을 모두 거부하고 해당 초안 DB 쓰기 0건 |
| malformed·unknown-field·과대 JSON | 예기치 않은 필드가 업무 명령처럼 해석되거나 heap 소진 | 엄격한 schema, unknown field 거부, 출력 token·응답 byte 상한과 구조 검증 후 entity 생성 | 빈·깨진·누락·추가·과대 응답에서 fail-closed |
| 모델 서버 장애·timeout | 요청 지연, 불완전 초안 저장 | timeout, 제한된 queue, fallback 없음, 초안 저장 전 실패 | timeout·5xx·연결 불가에서 MODEL_UNAVAILABLE과 해당 초안 DB 쓰기 0건 |
| 모델 endpoint 조작과 SSRF | 내부망 탐색 또는 외부 유료 API 호출 | 고정 base URL, host allowlist, 고정 /api/chat, 요청에서 URL을 받지 않음 |
비허용 host/path/query/userinfo 설정 시 시작 실패 |
| 모델 포트의 인터넷 노출 | 무단 GPU 사용과 내부 서비스 공격 | loopback/private bind, tunnel, host firewall, model proxy 인증 | 외부 네트워크에서 Ollama 포트 연결 실패 |
| DB 포트·과도한 권한 | 합성 데이터 변조, schema 변경, migration credential 탈취 | host port 없음, admin·one-shot migration·runtime 역할 분리, runtime DDL·Flyway history 거부 | Testcontainers 역할 검증과 외부 네트워크 DB 연결 실패 |
| 세션 탈취 | 다른 방문자의 workspace 접근 | TLS, Secure·HttpOnly·SameSite=Lax cookie, 짧은 timeout, 로그에서 session ID 제외 | HTTP 접근 차단, cookie 속성 확인, 세션 ID가 응답 본문·로그에 없음 |
| session fixation | 공격자가 정한 세션 재사용 | 데모 시작 시 새 세션 ID 발급·회전 | 기존 세션 ID가 start 이후 유지되지 않음 |
| CSRF | 방문자 세션으로 승인·발주 수행 | demo web chain에서 CSRF 활성화, form token 검증 | token 없는 모든 웹 쓰기 거부 |
| 공개 Basic API 활성화 | 공유 credential 공격 또는 UI 우회 | demo 프로필에서 stateless Basic chain 비활성·deny | demo 배포에서 Basic header로 API 호출 실패 |
| workspace IDOR | 다른 방문자의 요청·감사 노출 | workspace를 DemoPrincipal에서만 얻고 repository query에 항상 포함 | 두 세션 교차 UUID와 목록 조회에서 데이터 노출 0건 |
| persona 요청 위조 | 임의 권한 문자열 주입 | 서버 enum allowlist, 한 persona에 정확히 한 역할, workspace 고정 | 잘못된 persona와 복수 역할 요청 거부 |
| persona 기능의 과장 | 실제 IAM 구현으로 오해 | UI와 문서에 공개 체험 기능이라고 명시 | 포트폴리오 문구 검토 |
| UUID 존재 여부 probing | 객체 열거 | workspace 범위 조회 후 비감사 공개 역할에 일관된 거부 응답 | 존재·비존재 외부 UUID의 응답 차이로 정보를 얻지 못함 |
| idempotency key 재사용 | 다른 입력이 기존 결과를 덮음 | workspace·actor·key와 fingerprint 확인, DB 고유 제약 | 같은 key/같은 입력은 같은 결과, 다른 입력은 conflict |
| 최초 동일 key 동시 요청 | 모델 중복 호출과 GPU 낭비 | (workspaceId, actor, key) single-flight |
동시 요청에서 모델 호출 1회, 저장 1건 |
| 다수 workspace의 모델 폭주 | GPU OOM과 서비스 거부 | workspace·전체 고정 시간창 quota, 전체 concurrency 1, 제한 queue·follower | 제한 초과 시 busy/rate 오류와 GPU 동시 실행 1건 |
| 익명 session/start 폭주 | session heap·DB row 고갈 | 시작 전 전역 admission limit, active workspace 상한, 거부 시 session 미생성 | 동시 시작에서 상한 초과 생성 없음과 거부 요청 session 없음 |
| 무제한 목록·본문 | 메모리·DB 자원 고갈 | 본문 크기, 문자열 길이, page size와 정렬 allowlist 제한 | 최대값 초과 요청 거부, 조회 결과 상한 확인 |
| 감사 API의 전역 조회 | 방문자 간 정보 노출 | workspace_id 조건과 최대 건수 적용 |
AUDITOR persona도 자신의 workspace만 조회 |
| 만료 세션 데이터 잔존 | 불필요한 입력 보존 | workspace TTL, scheduled cleanup, close 시 전체 합성 데이터 삭제 | 만료·reset·close 뒤 관련 row 0건 |
| 사용자 입력의 개인정보 | 로그·DB에 불필요한 정보 보존 | 입력 경고, 짧은 보존, 로그에서 원문 제외, 운영 배포의 백업 제외를 외부 gate로 요구 | 요청 원문이 로그·감사 metadata·검증 산출물에 없고 실제 백업 정책 증거가 있음 |
| credential 커밋·로그 | 모델·DB·tunnel 탈취 | 환경 secret만 사용, 예시에는 placeholder만, .env* ignore, 장기 앱에서 migration credential 제외, bounded log와 Caddy access log 미활성 |
저장소·이미지 layer·로그 secret scan 통과 |
| proxy header 위조 | scheme/IP 기반 정책 우회 | 신뢰 proxy 범위 제한, edge 외 직접 origin 차단 | origin 직접 접근과 임의 forwarded header가 정책을 우회하지 못함 |
| 애플리케이션 예외 상세 노출 | 내부 클래스·경로·호스트 유출 | ProblemDetail 일반화, stack trace 비공개 | 4xx·5xx 응답에 내부 정보 없음 |
| supply-chain 취약점 | 애플리케이션 또는 이미지 장악 | wrapper checksum, app·PostgreSQL·Caddy·Ollama image full digest, 승인 모델 tag·content ID gate | dependency/image scan과 실제 digest·model ID 일치 검토 |
| 사설 GPU 모델 호스트 무단 사용 | 정책·법적·운영 문제 | 승인·실측·라이선스 gate, 한 GPU 선택, 승인 철회 시 즉시 중단 | 승인과 자산 확인 전 model 연결 단계 실행 금지 |
| host egress 우회 | 앱 컨테이너에서 모델 외 목적지 접근 | 고정 app base URL·host allowlist, 배포 host egress allowlist를 open gate로 요구 | 실제 host firewall/egress 정책 증거와 우회 실패 확인 |
| edge 요청 폭주 | 앱·DB·GPU 가용성 저하 | 앱 admission/model quota와 별도로 edge/WAF 익명 rate limit을 open gate로 요구 | 실제 edge 정책 증거와 외부 제한 동작 확인 |
| close 뒤 앱 또는 model 잔존 | 비의도 공개와 자원 사용 | 앱·모델 호스트별 normal/emergency close와 closed verifier | 두 호스트의 외부 HTTPS, origin, DB, model endpoint 접근 불가 확인 |
| reopen 시 버전 drift | 검증하지 않은 코드·모델 실행 | app image full digest와 모델 content ID 고정, migration·smoke 선행 | edge 공개 전에 같은 digest·model ID의 E2E smoke 통과 |
workspace 격리 규칙
workspace 격리는 역할 정책보다 먼저 적용합니다.
DemoPrincipal에서 현재 workspace를 얻습니다.- repository query에 workspace를 필수 조건으로 넣습니다.
- 개별 구매 요청은
PurchaseRequestReadPolicy가 조회 결과의 workspace를 방어적으로 다시 확인합니다. - 그 뒤 역할, 소유권과 상태를 확인하고, 발주-요청 관계는 DB의 workspace 복합 제약으로도 보호합니다.
- audit event와 order 생성에도 같은 workspace를 서버가 주입합니다.
요청 본문이나 query parameter의 workspace는 권한 판단에 사용하지 않습니다. 데이터베이스 고유 제약에도 workspace를 포함해 서로 다른 방문자가 같은 idempotency key를 사용할 수 있게 합니다.
persona 전환의 신뢰 모델
공개 방문자는 한 workspace 안에서 네 persona를 모두 선택할 수 있습니다. 따라서 공개 데모의 persona 전환 자체는 신원을 증명하지 않습니다.
이 기능이 증명하는 것은 다음으로 제한합니다.
- 각 요청에서 하나의 역할만 활성화됨
- endpoint와 service method의 역할 검사
- 역할과 상태별 작업함 조회
- 자기 요청 승인 금지 같은 도메인 규칙
- 승인 전 발주 금지와 감사 이벤트 일관성
실제 운영에서 필요한 OIDC/SSO, 조직 권한 원장, 관리자 승인과 직무 분리는 범위 밖이며 문서에 그렇게 표시합니다.
데이터 분류와 보존
| 데이터 | 허용 내용 | 보존 원칙 |
|---|---|---|
| 자연어 요청 | 공개 데모용 합성 문장 | 짧은 workspace TTL 동안만 저장하고 로그에서 제외. 운영 백업 제외는 배포 전 별도 검증 |
| 정책 | 독립적으로 만든 합성 카탈로그 | 코드와 공개 문서에 포함 가능 |
| 모델 출력 | 구조 검증을 통과한 합성 초안 | workspace와 함께 만료·삭제 |
| 감사 이벤트 | actor persona, action, 비민감 metadata | 요청 원문·prompt·credential 제외, workspace와 함께 삭제 |
| 운영 로그 | request ID, 상태, 처리 시간 | 세션 ID, 원문, token, 내부 주소 제외 |
| 검증 증거 | 날짜, artifact 식별자, 일반화한 결과 | 사설 host·계정·IP·내부 URL 제외 |
운영 배포에서는 공개 방문자 데이터를 백업 대상에서 제외하고 실제 정책 증거를 보관해야 합니다. 이 저장소는 백업 인프라를 구성하지 않으므로 해당 항목은 외부 배포 gate이며 아직 미검증입니다. 데모 중단 시에는 합성 데이터를 삭제하고 삭제 여부를 확인합니다.
보안 release gate
다음 항목이 모두 통과하기 전에는 edge를 live origin으로 전환하지 않습니다. 자동화 테스트가 있는 항목과 실제 배포 환경에서만 확인할 수 있는 항목을 모두 포함합니다.
- 사설 GPU 모델 호스트 승인과 실측 완료
- 공개 프로필에서 API Basic 비활성 확인
- CSRF, cookie와 session fixation 검증
- 두 독립 세션의 cross-workspace 테스트 통과
- PostgreSQL migration과 DB 제약 테스트 통과
- 동일 key 동시 요청 single-flight 테스트 통과
- workspace·전체 호출량, follower·queue와 global concurrency 1 확인
- 실제 모델 정상·malformed·timeout·unavailable E2E 통과
- 외부에서 DB와 Ollama 포트 차단 확인
- app host의 모델 목적지 egress allowlist와 edge/WAF 익명 rate limit 증거 확인
- secret, dependency와 container scan 검토 완료
- 앱·모델 양쪽 호스트의 normal/emergency close와 same-digest reopen rehearsal 통과
즉시 중단 조건
다음 사건에서는 신규 세션을 먼저 차단하고 공개 서비스를 닫습니다.
- 사설 GPU 모델 호스트 사용 승인이 없거나 철회됨
- 다른 workspace의 데이터가 한 건이라도 노출됨
- Ollama 또는 PostgreSQL이 공인망에서 접근됨
- credential이 응답, 로그, 이미지 또는 저장소에 노출됨
- 모델 장애에서 해당 초안 데이터가 저장됨
- 전체 모델 동시 실행 제한이 지켜지지 않음
- 검증하지 않은 artifact나 migration이 운영됨
중단 뒤 원인을 확인하고 credential 회수, 합성 데이터 삭제, 네트워크 차단과 재검증을 마칠 때까지 재개하지 않습니다. 유료 모델 API로 자동 전환하지 않습니다.
현재 검증 경계
- 웹 보안 체인, workspace 격리·TTL, model guard, bounded gateway와 DB 역할 분리는 구현했고 자동화 검증 경로가 있습니다.
- Docker/Caddy, full-digest image,
restart: no, 제한 로그, 분리 network, normal/emergency close 자산은 구현했습니다. 2026-08-04전체clean verify54개 성공, 실패·오류·건너뜀 0개입니다.- 실제 승인 모델 E2E, 공개 URL, 외부 모바일 smoke, 외부 DB·모델 포트 차단은 미검증입니다.
- host egress allowlist와 edge/WAF 익명 rate limit은 외부 배포에서만 확인할 수 있는 미검증 gate입니다.
- 앱·모델 양쪽 호스트의 normal/emergency close와 same-digest reopen rehearsal은 미검증입니다.
잔여 위험과 범위 밖
- 공개 demo persona는 실제 사용자의 신원이나 조직 직무 분리를 증명하지 않습니다.
- 애플리케이션 감사 이벤트는 DB 관리자까지 막는 변조 불가능 원장이 아닙니다.
- in-memory 호출 조정은 단일 애플리케이션 인스턴스에만 유효합니다.
- 소규모 합성 정책은 실제 ERP 정책의 복잡성을 대표하지 않습니다.
- 승인된 사설 GPU 모델 호스트의 가용성은 자산 운영 일정과 네트워크에 영향을 받습니다.
- 공개 데모는 실제 발주 시스템이나 외부 ERP에 쓰지 않습니다.
이 잔여 위험은 숨기지 않고 README와 공개 사례 설명에서 한계로 유지합니다.