Files
vpd-permission-poc/docs/design/654-poc4-mcp-ai-console-vpd-audit

설계서: PoC4 MCP AI Console 역설계 및 VPD/FGA 감사 운영 현행화 (#654)

상태: Implemented snapshot documented · follow-up contract pending 작성: [AI] Architect · 최종수정: 2026-07-14 추적성 — Redmine: #654 · 관련 ADR: 없음 · 구현 커밋: 5bdd242, 33399f9, 1f6d974 · 현재 소스: ai-web-agent-console/ · 현행 구현 파일: ai-web-agent-console/app.py, ai-web-agent-console/ai_web_agent_console/mcp_tool_router.py, ai-web-agent-console/ai_web_agent_console/oci_genai_sdk.py, ai-web-agent-console/ai_web_agent_console/model_registry.py, ai-web-agent-console/config/mcp_servers.json · 테스트: Python 3.11 compileall/unittest, mvn -f vpd-backoffice/pom.xml test

이 문서의 PoC4 명칭과 원격 archive 경로는 2026-07-14 당시의 이력이다. 현재 저장소 구조는 #742 설계서를 기준으로 한다.

1. 목적 (Why)

PoC4 MCP AI Console이 KB VPD MCP를 사용자별 Bearer 토큰으로 안전하게 호출하면서도, 환경별 URL·전송 방식·오류 처리를 하나의 명확한 계약으로 관리한다.

현재 UI의 기본 호출 흐름은 동작한다. 다만 kb_mcp 설정에는 endpoint_urlbase_url_env가 함께 있고, UI는 endpoint_url을 우선 사용한다. 따라서 KB_MCP_BASE_URL을 바꿔도 실제 호출 대상이 바뀌지 않는다. 또한 custom_python은 로컬 8500 MCP용 이름이므로 외부 KB VPD MCP의 통신 계약을 설명하지 못한다.

1.1 현행 구현 역설계 요약

이번 문서는 목표 설계를 먼저 정한 것이 아니라, 배포 서버에서 이미 동작하던 PoC4 MCP AI Console 스냅샷을 저장소로 가져온 뒤 구현을 역으로 읽어 작성했다. 따라서 아래 표가 현재 동작의 기준이다.

영역 현행 구현
저장소 위치 ai-web-agent-console/ 단일 폴더에 격리
실행 진입점 직접 실행은 streamlit run ai-web-agent-console/app.py --server.address 0.0.0.0 --server.port 8622
Python 런타임 Python 3.11 이상. 배포 서버 검증 런타임은 /home/opc/poc_4/.python-runtime/cpython-3.11.15+20260610/bin/python3.11
Streamlit 화면 ai-web-agent-console/app.py 단일 대형 UI. KB 테마, 포털 로그인, 대화 이력, MCP discovery/call, evidence 수집, 답변 합성을 포함
MCP registry ai-web-agent-console/config/mcp_servers.json
KB 정형 MCP https://kb.cloud-handson.com/mcp, 기본 tool ords.query.kb_select_ai_vpd
KB 벡터 MCP http://127.0.0.1:9978/mcp, allowlist hybrid_rerank_search
토큰 preset ai-web-agent-console/config/vpd_token_presets.json. 실제 토큰 원문은 포함하지 않고 placeholder만 둠
대화 저장소 기본 data/poc4_mcp_chat.sqlite3, 환경변수 POC4_CHAT_DB_PATH로 변경 가능
DB evidence 연결 기본 env file /home/opc/kbmcp/.env, wallet fallback /home/opc/wallet/kbaipoc
OCI GenAI OCI_AUTH_TYPE=config_file, ~/.oci/config, DEFAULT profile 기반. model profile은 ai-web-agent-console/config/model_profiles.json에서 로드
보안 원칙 wallet, DB password, wallet password, 실제 VPD token, 대화 DB는 저장소에 포함하지 않음

현행 데이터 흐름은 다음과 같다.

Streamlit 사용자 로그인
  ↓
VPD 사용자 preset 또는 직접 Bearer token 선택
  ↓
config/mcp_servers.json 로드
  ↓
각 MCP 서버에 initialize → initialized notification → tools/list
  ↓
질문 유형에 따라 single 또는 agent 실행 계획 선택
  ↓
tools/call 호출
  - Authorization: Bearer <현재 VPD token>
  - JSON-RPC body에는 tool name과 arguments만 포함
  ↓
정형 MCP / 벡터 MCP 결과 수집
  ↓
필요 시 /home/opc/kbmcp/.env + /home/opc/wallet/kbaipoc 로 Oracle evidence 조회
  ↓
OCI GenAI로 최종 답변 합성
  ↓
SQLite 대화 이력 저장

1.2 현행 구현 컴포넌트

파일 책임
ai-web-agent-console/app.py Streamlit 화면, 포털 로그인, MCP discovery/call, agent loop, evidence 수집, 답변 합성, 대화 이력 저장
ai-web-agent-console/ai_web_agent_console/presentation.py 공통 UI theme와 화면 표현 보조 코드
ai-web-agent-console/ai_web_agent_console/mcp_tool_router.py 발견된 MCP tool descriptor를 기반으로 LLM router가 server/tool을 선택하고 tool arguments를 구성
ai-web-agent-console/ai_web_agent_console/oci_genai_sdk.py OCI Generative AI 호출 경계. MCP/VPD token을 알지 않는 최소 completion client
ai-web-agent-console/ai_web_agent_console/model_registry.py 모델 profile registry 로드, region/endpoint 해석, 환경 override 처리
ai-web-agent-console/ai_web_agent_console/questions.py 데모 질문 목록
ai-web-agent-console/config/mcp_servers.json MCP 서버 registry
ai-web-agent-console/config/model_profiles.json OCI GenAI model profile registry
ai-web-agent-console/config/vpd_token_presets.json 데모 사용자 token preset 구조. 실제 토큰은 배포 환경에서 교체

현재 저장소에서는 cd ai-web-agent-console && streamlit run app.py를 독립 실행 진입점으로 사용한다.

1.3 현행 보안/비밀정보 경계

  • VpdTokenPreset.token은 dataclass에서 repr=False이며, UI는 token을 정규화한 뒤 Authorization header에만 넣는다.
  • _NoRedirectHandler는 redirect 시 Authorization header가 다른 endpoint로 전달되는 것을 막는다.
  • ai_web_agent_console.mcp_tool_router의 router는 질문과 tool descriptor만 받으며 bearer token 또는 provider credential을 받지 않는다.
  • ai_web_agent_console.oci_genai_sdk.env에서 OCI_AUTH_TYPE, OCI_CONFIG_FILE, OCI_GENAI_COMPARTMENT_ID, OCI_PROFILE만 읽는다.
  • Oracle audit/business evidence 조회는 /home/opc/kbmcp/.env에서 ORACLE_DB_USER, ORACLE_DB_PASSWORD, ORACLE_DSN, ORACLE_WALLET_PASSWORD, ORACLE_WALLET_DIR를 읽는다.
  • wallet 기본 fallback은 /home/opc/wallet/kbaipoc이다.
  • 저장소에는 실제 .env, wallet, SQLite 대화 DB, 실제 VPD bearer token 원문을 포함하지 않는다.

1.4 현행 구현과 후속 목표의 차이

항목 현행 구현 후속 목표
KB MCP provider/transport 명칭 provider=custom_python, transport=http kb_vpd_streamable_http, streamable_http처럼 외부 KB VPD MCP 계약을 명확히 표현
endpoint 해석 endpoint_url이 있으면 이를 우선 사용하고, 없으면 env 값을 사용 KB MCP는 KB_MCP_BASE_URL + /mcp 단일 계약으로 해석
tool routing route가 여러 개면 LLM router/agent mode를 사용할 수 있음 KB 단일 tool 호출에서는 router 없이 direct 선택
401/403 표시 현재 둘 다 “MCP 인증에 실패” 계열 안전 메시지로 축약 토큰 없음/만료/권한 부족을 UI에서 명확히 분리
start script 원본 /home/opc/poc_4 runtime wrapper 의존 스냅샷 폴더 안에서 독립 실행 가능한 wrapper 추가
회귀 테스트 원본 서버 Python 3.11 py_compile, Java mvn -q test PoC4 전용 단위 테스트와 실제 MCP HTTP smoke test 추가

2. 범위 (Scope)

  • 포함:
    • ai-web-agent-console/app.py의 KB MCP endpoint, 인증 헤더, JSON-RPC, 오류 처리 정비
    • config/mcp_servers.json 및 sample의 KB MCP 선언 정비
    • KB MCP의 단일 도구 ords.query.kb_select_ai_vpd 호출 계약 문서화
    • VPD Backoffice /mcp과의 HTTP 상태·프로토콜 버전 호환성 점검 및 필요한 최소 보완
  • 제외 (out of scope):
    • 기존 kb_vector_mcpcustom_python 8500 RAG MCP의 변경
    • VPD 권한 규칙, Select AI SQL 생성 규칙, ORDS 비즈니스 로직 변경
    • OAuth Authorization Server, 사용자 로그인/토큰 발급 UI 재설계
    • VPD 토큰 원문을 환경변수·레지스트리에 저장하는 방식

3. 인수조건 (Acceptance Criteria)

아래 체크박스는 최종 목표 기준이다. 2026-07-14 스냅샷 반입 상태에서는 일부만 충족한다.

  • KB MCP URL은 KB_MCP_BASE_URL 하나에서만 해석되고 /mcp path가 안전하게 결합된다.
  • initialize, notifications/initialized, tools/list, tools/call의 모든 HTTP 요청에 현재 선택된 사용자의 Authorization: Bearer <VPD token>만 전송된다.
  • Bearer 토큰은 JSON-RPC arguments, SQLite 대화 이력, UI 결과, 애플리케이션 로그에 저장·출력되지 않는다.
  • 호출 가능한 도구는 ords.query.kb_select_ai_vpd 하나이며, 인자는 promptlimit만 허용된다.
  • 단일 도구 구성에서는 LLM router를 호출하지 않고 허용 도구를 직접 선택한다.
  • 토큰 없음/위조/만료는 HTTP 401, 유효 토큰의 권한 부족은 HTTP 403으로 UI에 구분 표시된다.
  • 잘못된 Origin의 브라우저 요청은 MCP 서버에서 거부되며, 허용 Origin 및 Origin 없는 네이티브 MCP client 정책이 문서화된다.
  • 기존 kb_vector_mcp 및 로컬 8500 MCP 회귀 테스트가 통과한다.
조건 현행 판단 근거/메모
URL 단일 해석 부분 충족 _mcp_endpoint()/mcp 결합과 URL 검증은 수행하지만 config/mcp_servers.jsonendpoint_url이 env보다 우선한다.
요청별 Authorization header 충족 _jsonrpc_exchange()_jsonrpc_notification()Authorization: Bearer <token>을 header에 설정한다.
JSON-RPC arguments token 미포함 충족 build_mcp_tool_arguments()prompt, limit, query, question 계열만 구성한다.
SQLite token 원문 미저장 부분 충족 chat turn에는 선택 사용자/label, question/answer/details를 저장한다. token 원문 필드는 없지만 details_json의 evidence payload는 후속 보안 점검 필요.
단일 도구 direct routing 부분 충족 route가 1개면 single로 처리하지만, 복수 route에서는 LLM router/agent mode를 사용할 수 있다.
401/403 구분 미충족 현재 401, 403 모두 안전한 인증 실패 메시지로 축약한다.
Origin 검증 서버 범위 UI client는 redirect 차단을 수행한다. Origin 정책은 VPD Backoffice MCP 서버 설정에서 확인해야 한다.
벡터 MCP 회귀 미검증 소스 반입 시 Java 테스트와 Python 문법 검증만 수행했다. 실제 kb_vector_mcp smoke test는 후속이다.

4. 컨텍스트 & 제약

  • KB VPD MCP endpoint: https://kb.cloud-handson.com/mcp
  • 보호 대상: ords.query.kb_select_ai_vpd는 ORDS를 거쳐 VPD 컨텍스트가 적용된 Select AI 조회를 실행한다.
  • 토큰 주체: VPD 권한은 정적 서비스 계정이 아니라 현재 선택된 KB_STAKEHOLDERS 사용자 토큰에 의해 결정된다.
  • UI의 VPD token preset 파일은 데모 편의 기능일 뿐이다. 운영에서는 OS 소유자 전용 권한(0600)으로 관리하고 형상관리·로그·SQLite에서 제외한다.
  • UI가 현재 사용하는 2025-11-25 MCP protocol version과 서버의 지원 버전은 handshake에서 협상해야 한다. 지원하지 않는 버전을 무조건 강제하지 않는다.
  • 현재 서버는 stateless JSON-RPC POST 호출로도 동작한다. 서버가 Mcp-Session-Id를 발급하면 client는 이후 요청에만 그 값을 포함한다.

5. 아키텍처 개요

I/O는 PoC4 MCP AI Console의 HTTP transport와 VPD Backoffice /mcp에 한정한다. URL 결합, 허용 도구 검증, 요청·응답 검증, 안전한 오류 변환은 순수 함수로 분리해 네트워크 없이 테스트한다.

VPD 사용자 선택 / 토큰 입력
        │  (원문은 요청 메모리에만 존재)
        ▼
PoC4 MCP AI Console
  ├─ KB_MCP_BASE_URL + "/mcp"
  ├─ tool allowlist 검증
  └─ Authorization: Bearer <current VPD token>
        │
        ▼ HTTPS JSON-RPC / Streamable HTTP
VPD Backoffice MCP (/mcp)
  ├─ Origin·토큰 검증
  ├─ tools/list: metadata only
  └─ tools/call: ords.query.kb_select_ai_vpd
        │
        ▼
ORDS Select AI API → VPD context → Oracle ADB

6. 데이터 모델

6.1 KB MCP registry 선언

config/mcp_servers.json의 KB 선언은 다음 계약을 따른다. 값은 예시이며, 토큰 값은 넣지 않는다.

{
  "id": "kb_mcp",
  "enabled": true,
  "provider": "kb_vpd_streamable_http",
  "transport": "streamable_http",
  "base_url_env": "KB_MCP_BASE_URL",
  "endpoint_path": "/mcp",
  "auth_delivery": "per_request_vpd_bearer",
  "timeout_seconds_env": "POC3_MCP_TIMEOUT_SECONDS",
  "default_tool": "ords.query.kb_select_ai_vpd",
  "tool_allowlist": ["ords.query.kb_select_ai_vpd"],
  "router_mode": "direct",
  "description": "KB VPD Select AI MCP; the current user's VPD bearer is sent only in the Authorization header."
}

환경 변수는 아래 두 값만 필요하다.

KB_MCP_BASE_URL=https://kb.cloud-handson.com
POC3_MCP_TIMEOUT_SECONDS=90

endpoint_url, token_env, POC3_MCP_TOKEN, BACKOFFICE_MCP_ACCESS_TOKEN은 KB MCP 선언에 두지 않는다. URL은 registry에 하드코딩하지 않고, VPD 토큰은 사용자별 요청에서만 받는다.

6.2 MCP 요청

모든 요청은 다음 헤더를 사용한다.

Accept: application/json, text/event-stream
Content-Type: application/json
MCP-Protocol-Version: <negotiated version>
Authorization: Bearer <current-user-vpd-token>

tools/call body의 arguments는 아래와 같이 제한한다.

{
  "name": "ords.query.kb_select_ai_vpd",
  "arguments": {
    "prompt": "담당 고객의 보험료 상세를 보여줘",
    "limit": 50
  }
}

경계 검증 규칙:

  • tool name은 정확히 allowlist 값 하나와 일치해야 한다.
  • prompt는 문자열이며 서버와 동일한 최대 길이를 적용한다.
  • limit은 정수 1..100으로 clamp한다.
  • 토큰은 공백·Bearer prefix를 정규화한 뒤 헤더에만 넣는다.
  • redirect는 허용하지 않는다. 다른 origin으로 Authorization이 전달되어서는 안 된다.

7. 함수 명세 (Function Specs)

함수 책임(1줄) 시그니처(잠정) 입력 출력 에러/실패 복잡?
resolve_kb_mcp_endpoint base URL과 고정 path를 안전하게 결합 (server, environ) -> str registry, env HTTPS MCP URL 누락/비정상 URL 단순
validate_kb_mcp_server KB 선언의 provider·transport·allowlist를 검증 (mapping) -> McpServer registry row typed server 계약 위반 단순
mcp_headers 요청별 VPD bearer 헤더 생성 (token, version, session_id) -> dict 사용자 토큰 안전한 headers 토큰 형식 오류 단순
discover_kb_tools initialize 및 tools/list 후 단일 도구를 검증 (server, token) -> McpDiscoveryResult endpoint, token tool descriptor auth/protocol 오류 복잡
call_kb_select_ai 고정 도구에 prompt·limit을 전달 (server, token, prompt, limit) -> result 사용자 요청 tool result auth/timeout/JSON-RPC 오류 복잡
to_public_mcp_error HTTP/JSON-RPC 오류를 안전한 UI 메시지로 변환 (exception) -> PublicMcpError 내부 오류 사용자 메시지 원문 노출 금지 단순

8. 흐름 / 알고리즘

  1. UI는 KB_MCP_BASE_URL을 읽고 /mcp만 결합한다. registry의 임의 endpoint_url은 KB 서버에 허용하지 않는다.
  2. 사용자가 VPD token preset 또는 일회성 Bearer를 선택한다. 토큰은 현재 실행 변수에만 유지한다.
  3. client는 initialize를 보내고 서버가 반환한 protocol version과 선택 가능한 session ID를 검증한다.
  4. notifications/initialized를 보낸 뒤 tools/list를 실행한다.
  5. 응답 목록이 정확히 허용 도구를 포함하는지, 해당 input schema가 prompt, limit 계약에 맞는지 확인한다.
  6. KB 서버는 단일 도구이므로 router model을 호출하지 않고 ords.query.kb_select_ai_vpd를 직접 선택한다.
  7. tools/call은 prompt·clamp된 limit만 body에 넣고 VPD Bearer는 Authorization에만 넣는다.
  8. 결과는 화면용 안전 projection만 SQLite에 저장한다. Authorization 헤더와 원문 token은 저장하지 않으며, 진단이 필요하면 단방향 token fingerprint만 별도 보존할 수 있다.

9. 엣지케이스 & 에러 처리

상황 client 처리 서버 기대 동작
토큰 없음 호출 전 안내, 네트워크 요청 없음 해당 없음
토큰 위조·만료 401 → “토큰이 유효하지 않거나 만료됨” WWW-Authenticate 포함 가능
유효하지만 권한 없음 403 → “이 사용자에게 조회 권한 없음” VPD fail-closed 유지
allowlist 밖 도구 호출 전 차단 tools/call에서도 차단
429 안전하게 재시도하지 않고 잠시 후 재시도 안내 rate limit 정책 적용
timeout tools/call 자동 재시도 금지 request ID 기반 감사 추적
session ID 미발급 stateless POST로 진행 session을 요구하지 않음
session ID 발급 이후 요청에 Mcp-Session-Id 포함 세션 소유·만료 검증
redirect 즉시 실패 Authorization 전달 금지
Origin 불일치 브라우저 UI에 일반 오류 표시 403으로 거부

10. 테스트 계획

  • registry 단위 테스트
    • KB_MCP_BASE_URL만으로 endpoint가 https://kb.cloud-handson.com/mcp가 되는지 검증
    • KB registry에 endpoint_url, token_env, custom_python이 있으면 fail-closed 되는지 검증
    • vector MCP 설정은 기존 형식으로 계속 로드되는지 검증
  • HTTP transport 단위 테스트
    • initialize/tools/list/tools/call 모두 Authorization header가 있고 JSON body에는 token key가 없는지 검증
    • 401, 403, 429, timeout, redirect, malformed JSON-RPC 응답을 안전한 메시지로 변환하는지 검증
    • session header 반환/재전송 및 stateless fallback을 검증
  • 통합 smoke test
    • 허용된 VPD 사용자 토큰으로 tools/listtools/call 성공
    • 잘못된 토큰은 401, 타 사용자 권한은 403
    • 설계사와 지점장 토큰으로 동일 질문을 실행해 VPD 행/컬럼 결과가 서로 다른지 확인
  • 비밀정보 점검
    • chat SQLite, Streamlit log, 예외 메시지에서 토큰 원문 검색 결과 0건

11. 리스크 & 대안 검토

  • 선택: KB MCP 전용 kb_vpd_streamable_http 선언을 도입하고, 로컬 8500용 custom_python과 분리한다. 외부 HTTPS/VPD Bearer 계약을 코드와 운영 화면에서 명확히 할 수 있다.
  • 대안 1 — 기존 custom_python 재사용: 동작은 시킬 수 있으나 provider 이름과 endpoint 제약이 실제 KB 서버와 맞지 않아 로컬 MCP와 외부 VPD MCP가 섞인다.
  • 대안 2 — 고정 MCP access token 사용: 구현은 간단하지만 모든 사용자가 동일 VPD 주체가 되어 데이터 권한 분리가 무너진다. 채택하지 않는다.
  • 대안 3 — 즉시 OAuth 2.1 전환: 표준 상호운용성에는 유리하지만 현재 데모의 VPD token 발급·검증 체계를 대체하므로 별도 인증 서버 설계가 필요하다.
  • 롤백: 새 registry 선언을 비활성화하고 기존 KB 선언을 복원한다. DB VPD 정책·ORDS endpoint·토큰 데이터는 변경하지 않는다.

12. 미해결 질문 (Open Questions)

  • VPD Backoffice MCP endpoint가 현재 지원할 MCP protocol version을 어떤 값으로 공식 고정할지 결정이 필요하다.
  • Streamable HTTP의 GET/SSE 및 Mcp-Session-Id를 완전 지원할지, stateless POST profile로 운영할지 결정이 필요하다.
  • 데모 이후 사용자 VPD bearer를 OAuth 2.1 access token으로 전환할지, 현 토큰을 resource-server token으로 계속 운영할지 결정이 필요하다.
  • chat 대화 이력의 basis_json/details_json에 민감 데이터 보존 기간과 삭제 정책을 별도로 정해야 한다.

13. 2026-07-14 소스 스냅샷 반입

배포 서버의 PoC4 활성 화면 소스를 현재 저장소에 별도 폴더로 반입했다.

  • 원격 실제 위치: /home/opc/poc_4/poc4_active_source_20260714.tar.gz
  • 사용자 제시 경로 /home/opc/poc_4/ai-web-agent-console/poc4_active_source_20260714.tar.gz에는 파일이 없었고, 실제 archive는 /home/opc/poc_4/ 바로 아래에 있었다.
  • 저장소 위치: ai-web-agent-console/
  • 포함 파일: Streamlit UI, MCP router, OCI GenAI client, 모델 profile, token preset sample, 기동/status script, requirements
  • 보안 확인: 실제 .env, 실제 VPD token 원문, 대화 SQLite DB는 포함하지 않았다. vpd_token_presets.json에는 placeholder만 있다.
  • DB 참조 경로: VPD 개발본 배포 서버에서는 /home/opc/kbmcp/.env의 접속 정보를 사용하고, wallet directory는 /home/opc/wallet/kbaipoc를 사용한다. 두 경로의 파일 내용은 저장소에 포함하지 않는다.
  • 런타임 전제: Python 3.11 이상. 현재 개발 서버 기본 python3는 3.6.8이므로 이 소스의 문법 검증에는 맞지 않는다.
  • 검증: 배포 서버 원본 경로에서 PoC4 전용 Python 3.11.15py_compile 통과. 기존 Java 백오피스 mvn -q test 통과.

주의: 이 반입은 소스 스냅샷 보관이다. 아직 본 설계서의 목표 계약인 kb_vpd_streamable_http, KB_MCP_BASE_URL 단일 endpoint 해석, 단일 도구 direct routing을 구현 완료했다는 의미는 아니다.

14. 2026-07-14 역설계 현행화

사용자 요청에 따라 “설계가 먼저 있고 구현을 맞추는 방식”이 아니라, 이미 배포 서버에서 구현·운영 중인 PoC4 MCP AI Console 소스를 기준으로 역설계했다.

현행화한 내용:

  • mcp_discovery_ui.py의 실제 책임 범위를 Streamlit UI, MCP JSON-RPC client, agent loop, evidence collector, answer synthesis, chat store로 분리해 기록했다.
  • mcp_servers.json의 현재 계약이 목표 계약과 다르다는 점을 명시했다. 특히 현행은 endpoint_url 우선이며, provider=custom_python, transport=http이다.
  • /home/opc/kbmcp/.env/home/opc/wallet/kbaipoc를 배포 서버 DB evidence 조회 기준으로 확정했다.
  • 포함된 start/status script가 스냅샷 단독 실행용이 아니라 원본 /home/opc/poc_4 runtime wrapper에 의존한다는 점을 기록했다.
  • 인수조건을 최종 목표와 현행 충족 상태로 분리했다.

검증:

  • 배포 서버 원본 소스 기준 Python 3.11.15 py_compile 통과
  • 백오피스 기존 회귀 mvn -q test 통과
  • 저장소 문서/스냅샷 커밋 및 gitea/main push 완료

15. 2026-07-14 Oracle FGA 감사로그 적용 확인

15.1 확인 목적

PoC4 MCP AI Console과 VPD 백오피스는 사용자별 Bearer token으로 정형 데이터를 조회한다. 이때 Oracle VPD는 행 접근 predicate를 적용하고, ASO/Data Redaction은 민감 컬럼 원문/마스킹을 결정한다. FGA는 그 결과로 실행된 SELECT를 DB 감사 증적으로 남기는 역할이다.

이번 확인은 “FGA가 설계상 있어야 한다”가 아니라, 배포 서버가 바라보는 실제 ADB에 DBMS_FGA 정책과 감사 이벤트가 존재하는지 SQLcl로 직접 확인한 결과다.

15.2 SQLcl 확인 환경

배포 서버에서 아래 기준으로 접속했다. 비밀번호와 wallet password는 출력하거나 저장소에 기록하지 않는다.

항목
접속 서버 161.33.6.45
DB 접속 env /home/opc/kbmcp/.env
wallet directory /home/opc/wallet/kbaipoc
SQLcl /home/opc/tools/sqlcl/bin/sql
접속 DB 사용자 ADMIN
감사 대상 schema POC_2

SQLcl 실행 구조는 다음과 같다.

set -a
. /home/opc/kbmcp/.env
set +a
export TNS_ADMIN="${ORACLE_WALLET_DIR:-/home/opc/wallet/kbaipoc}"

/home/opc/tools/sqlcl/bin/sql -s /nolog
connect ${ORACLE_DB_USER}/"<ORACLE_DB_PASSWORD>"@${ORACLE_DSN}

15.3 적용된 FGA 정책

DBA_AUDIT_POLICIESDBA_AUDIT_POLICY_COLUMNS를 SQLcl로 조회한 결과, POC_2 schema에는 FGA SELECT 정책 4건이 활성화되어 있다.

객체 정책명 감사 컬럼 활성 SELECT 감사
KB_CLAIMS KB_FGA_CLAIMS_AMT CLAIM_AMT YES YES
KB_CLAIMS KB_FGA_CLAIMS_PAID_AMT PAID_AMT YES YES
KB_CUSTOMERS KB_FGA_CUSTOMERS_PII CUST_NM, RRN_MASKED YES YES
KB_EXTERNAL_HOLDINGS KB_FGA_EXT_HOLDINGS EXT_INSURER, EXT_PRODUCT_GRP, EXT_PRODUCT_TYPE YES YES

요약 쿼리 결과:

POLICY_COUNT = 4
ENABLED_COUNT = 4
SELECT_POLICY_COUNT = 4

현재 DB의 정책명 계열은 모두 KB_FGA_*다. 저장소의 범용 적용 스크립트 database/adb/42_agent_ords_fga_execution_audit.sqlCB_VPD_EXEC_AUDIT_<object_id> 형태의 정책을 만들도록 작성되어 있으나, 현행 ADB에는 이 계열이 아니라 KB_FGA_* 정책이 적용되어 있다. 따라서 운영 확인 시에는 “스크립트 파일명/예상명”보다 DBA_AUDIT_POLICIES의 실제 정책명을 기준으로 봐야 한다.

15.4 감사 이벤트 저장 위치

ADB 현행 환경에서는 FGA 이벤트가 UNIFIED_AUDIT_TRAIL에 기록된다.

SQLcl 확인 결과 UNIFIED_AUDIT_TRAIL에는 FGA 조회에 필요한 아래 컬럼이 있다.

컬럼 용도
EVENT_TIMESTAMP, EVENT_TIMESTAMP_UTC 감사 발생 시각
DBUSERNAME SQL을 실행한 DB 사용자
CLIENT_IDENTIFIER ORDS/MCP 요청 식별자
OBJECT_SCHEMA, OBJECT_NAME 감사 대상 객체
ACTION_NAME 실행 작업. 현재는 SELECT
FGA_POLICY_NAME 트리거된 FGA 정책명
SQL_TEXT DB가 감사한 실제 SQL
RLS_INFO Oracle이 기록한 VPD 정책명과 predicate
RETURN_CODE 실행 결과 코드. 0이면 성공

최근 7일 FGA 이벤트 요약:

UNIFIED_AUDIT_TRAIL event_count_7d = 525
oldest_event = 2026-07-09 06:15:04
newest_event = 2026-07-14 05:09:14

반면 DBA_FGA_AUDIT_TRAIL 기준 최근 7일 이벤트는 0건이었다.

DBA_FGA_AUDIT_TRAIL event_count_7d = 0

해석은 다음과 같다.

  • FGA가 미적용이라는 뜻이 아니다.
  • 현재 ADB에서는 FGA 감사 행이 Unified Audit Trail에 기록되고 있다.
  • 앱 구현도 이 전제를 반영해 UNIFIED_AUDIT_TRAIL을 먼저 조회하고, 실패할 때 DBA_FGA_AUDIT_TRAIL로 fallback한다.

15.5 최근 감사 이벤트 예시

최근 이벤트는 모두 CB_ORDS DB 사용자로 기록되었고, CLIENT_IDENTIFIER에는 요청별 UUID가 들어가 있었다. 이 값으로 특정 ORDS/MCP 요청과 DB 감사 행을 연결한다.

확인된 최근 이벤트 예:

발생시각(KST) DB 사용자 객체 정책 작업 결과
2026-07-14 14:09:14 CB_ORDS KB_EXTERNAL_HOLDINGS KB_FGA_EXT_HOLDINGS SELECT 0
2026-07-14 14:06:31 CB_ORDS KB_EXTERNAL_HOLDINGS KB_FGA_EXT_HOLDINGS SELECT 0
2026-07-14 14:06:31 CB_ORDS KB_CLAIMS KB_FGA_CLAIMS_AMT SELECT 0
2026-07-14 14:06:31 CB_ORDS KB_CLAIMS KB_FGA_CLAIMS_PAID_AMT SELECT 0

RLS_INFO도 같이 기록된다. 예를 들어 KB_CLAIMS 이벤트에는 KB_KB_CLAIMS_ROW_POLICY와 해당 VPD predicate가 포함되어 있었다. 즉 FGA는 “어떤 SQL이 실행됐는가”뿐 아니라 “그 SQL에 어떤 VPD 정책이 붙었는가”를 사후 증적으로 확인하는 데 사용할 수 있다.

15.6 소스 구현과 화면 연결

PoC4 스냅샷의 ai-web-agent-console/app.py는 감사로그 탭에서 다음 두 쿼리를 사용한다.

함수 조회 대상 역할
_load_fga_inventory() DBA_AUDIT_POLICIES, DBA_AUDIT_POLICY_COLUMNS, POC_2.KB_SECURITY_POLICY_CATALOG 현재 활성 FGA 정책과 관리 카탈로그를 표시
_load_fga_audit_events() UNIFIED_AUDIT_TRAIL 최근 FGA 이벤트, SQL 원문, 사용자, 정책명, 결과 코드를 표시

VPD 백오피스의 검증 화면은 OrdsProbeService에서 다음 순서로 특정 요청의 FGA 증적을 찾는다.

  1. UNIFIED_AUDIT_TRAIL
  2. DBA_FGA_AUDIT_TRAIL

조회 조건은 객체 owner/name, ACTION_NAME = 'SELECT', CLIENT_IDENTIFIER = <probe request id>, FGA_POLICY_NAME IS NOT NULL이다.

따라서 운영자가 봐야 할 기준은 다음이다.

  • “정책이 적용됐는가?” → DBA_AUDIT_POLICIES.ENABLED = YES
  • “민감 컬럼 접근이 실제 발생했는가?” → UNIFIED_AUDIT_TRAIL.FGA_POLICY_NAME IS NOT NULL
  • “어떤 VPD predicate가 붙었는가?” → UNIFIED_AUDIT_TRAIL.RLS_INFO
  • “어떤 요청과 연결되는가?” → CLIENT_IDENTIFIER

15.7 확인용 SQL

운영 확인 시 사용할 수 있는 최소 SQL은 다음과 같다.

SELECT policy.object_schema,
       policy.object_name,
       policy.policy_name,
       policy.enabled,
       policy.sel,
       LISTAGG(policy_columns.policy_column, ',') WITHIN GROUP (
         ORDER BY policy_columns.policy_column
       ) AS policy_columns
FROM dba_audit_policies policy
LEFT JOIN dba_audit_policy_columns policy_columns
  ON policy_columns.object_schema = policy.object_schema
 AND policy_columns.object_name = policy.object_name
 AND policy_columns.policy_name = policy.policy_name
WHERE policy.object_schema = 'POC_2'
GROUP BY policy.object_schema,
         policy.object_name,
         policy.policy_name,
         policy.enabled,
         policy.sel
ORDER BY policy.object_name, policy.policy_name;
SELECT *
FROM (
    SELECT TO_CHAR(
             event_timestamp AT TIME ZONE 'Asia/Seoul',
             'YYYY-MM-DD HH24:MI:SS'
           ) AS event_time,
           dbusername,
           client_identifier,
           object_name,
           fga_policy_name,
           action_name,
           return_code,
           DBMS_LOB.SUBSTR(sql_text, 500, 1) AS sql_text_sample,
           DBMS_LOB.SUBSTR(rls_info, 500, 1) AS rls_info_sample
    FROM unified_audit_trail
    WHERE object_schema = 'POC_2'
      AND fga_policy_name IS NOT NULL
    ORDER BY event_timestamp DESC
)
WHERE ROWNUM <= 20;

15.8 후속 정리 필요사항

현재 FGA 정책 자체는 정상 적용되어 있고 감사 이벤트도 쌓이고 있다. 다만 관리 카탈로그 POC_2.KB_SECURITY_POLICY_CATALOG에는 KB_CUSTOMERS, KB_EXTERNAL_HOLDINGS 중심의 5개 row만 확인되며, 실제 활성 정책에 있는 KB_CLAIMS.CLAIM_AMT, KB_CLAIMS.PAID_AMT 정책 메타데이터는 카탈로그 조회 결과에 없었다.

따라서 화면이 DBA_AUDIT_POLICIES를 직접 조회하는 현재 구조에서는 정책이 보이지만, 카탈로그를 운영 기준으로 삼으려면 KB_CLAIMS FGA 메타데이터도 KB_SECURITY_POLICY_CATALOG에 맞춰 등록하는 정리가 필요하다.