refs #744: standardize overview and cookbook documentation

This commit is contained in:
devmrko
2026-08-05 12:55:05 +09:00
parent 1c8ad8647f
commit fbf4c19c72
6 changed files with 945 additions and 699 deletions

19
AGENTS.md Normal file
View File

@@ -0,0 +1,19 @@
# 문서 작성 공통 규칙
이 저장소에서 새 문서를 만들거나 기존 문서를 크게 고칠 때는 아래 구조를 기본으로 한다.
1. 첫 문서(`README.md`)는 독자가 2~3분 안에 목적, 범위, 결정사항, 전체 구성, 현재 상태를 파악하는 **개요 문서**로 작성한다.
2. README에는 복잡한 절차와 모든 오류 사례를 누적하지 않는다. 아래와 같이 역할별 상세 문서로 분리하고, 개요에서 명확한 링크를 제공한다.
- `architecture.md`: 신뢰 경계, 컴포넌트 책임, 데이터·인증 흐름, 설계 결정
- `cookbook.md`: 준비물, 단계별 적용 명령/화면값, 검증, 롤백
- `troubleshooting.md`: 증상 → 원인 → 확인 방법 → 해결 → 재발 방지
- 필요하면 `operations.md`, `security.md`, `adr/` 등 목적이 드러나는 파일을 추가한다.
3. 그림은 한 장에 모든 세부사항을 넣지 않는다.
- README에는 시스템 경계와 핵심 흐름만 보이는 **개요도**를 둔다.
- 상세 연결·claim·포트·예외 흐름은 architecture 또는 cookbook의 **상세도**로 분리한다.
- 각 그림 아래에는 독자가 알아야 할 결론을 한두 문장으로 적는다.
4. Cookbook은 실제 적용 순서를 따르며, 각 단계에 입력값의 출처, 성공 판정, 실패 시 연결할 troubleshooting 항목을 포함한다.
5. 비밀번호, client secret, access token, 개인 식별 정보는 어떤 문서·그림·명령 출력에도 기록하지 않는다. 위치와 안전한 조회/rotation 방법만 기록한다.
6. README의 문서 지도와 각 상세 문서의 상호 링크를 변경 후 반드시 확인한다.
문서의 독자는 운영자·개발자·검토자다. 제품명과 전문 용어는 필요할 때만 쓰고, 처음 한 번은 평이한 말로 역할을 정의한다.

View File

@@ -0,0 +1,43 @@
# 문서 구조 표준
## 목적
문서를 처음 보는 사람이 전체 구조를 빠르게 이해하고, 필요한 순간에만 적용 방법이나 장애 해결 세부사항으로 내려갈 수 있게 한다.
## 기본 문서 지도
```text
README.md 무엇을, 왜 하는가 / 현재 상태 / 큰그림
├── architecture.md 어떻게 구성되며 누가 무엇을 신뢰하는가
├── cookbook.md 어떻게 적용·검증·되돌리는가
└── troubleshooting.md 무엇이 실패했고 어떻게 진단·해결하는가
```
## README 필수 항목
- 목적·범위·제외 범위
- 현재 상태와 검증된 사실
- 구성요소 5~7개 이하의 개요도
- 핵심 결정 3~5개
- 상세 문서 링크와 독자가 어떤 경우에 읽어야 하는지
## 상세 문서 규칙
| 문서 | 포함할 내용 | 포함하지 않을 내용 |
|---|---|---|
| `architecture.md` | 신뢰 경계, 구성요소 책임, 상세 흐름, 데이터 모델, 설계 근거 | 긴 설치 명령과 오류 이력 |
| `cookbook.md` | 준비물, 단계, 입력값 형식, 성공 판정, rollback, 관련 troubleshooting 링크 | 설계 배경의 반복 |
| `troubleshooting.md` | 증상, 원인, 확인 명령, 해결, 재발 방지 | secret 값, 원인 없는 임시 우회 |
## 그림 규칙
- README 개요도는 경계와 흐름만 보여 주고 화살표는 가능한 한 10개 이하로 유지한다.
- 포트, claim, redirect URI, DB role처럼 세부값이 필요한 내용은 상세도 또는 표로 분리한다.
- 그림 아래에 “이 그림에서 기억할 점”을 적는다.
- Mermaid를 쓸 때는 노드 이름을 짧게 하고, 긴 설명은 표 또는 본문으로 옮긴다.
## 보안과 검증
- 비밀번호·secret·token·cookie는 예시에도 넣지 않는다.
- 각 cookbook 단계는 성공 판정과 다음 조치를 포함한다.
- 문서를 마칠 때는 링크 유효성, Mermaid 문법, 용어 일관성, secret 노출 여부를 점검한다.

View File

@@ -1,720 +1,60 @@
# 설계서: Windows AD 기반 DDS END USER 매핑 테스트 (#744)
# Windows AD → OCI IAM → DDS 권한 매핑 (#744)
> **상태**: Keycloak 기반 검증 완료 · OCI IAM AD Bridge + delegated authentication 전환 진행 중
> **상태**: Keycloak 기반 Alice 허용/Bob 기본 거부 검증 완료 · OCI IAM AD Bridge 전환 진행 중
> **최종수정**: 2026-08-05
> **추적성**: Redmine #744 · 선행 설계: [DDS MCP END USER Context](../617-dds-mcp-end-user-context/README.md)
## 1. 목적과 범위
## 한눈에 보기
Microsoft Entra ID 테넌트가 아직 준비되지 않은 상황에서, OCI 격리 네트워크의 Windows Server AD DS를 이용해 업무 사용자 디렉터리와 DDS END USER 매핑을 먼저 검증한다.
이 환경은 **Microsoft Entra access token을 Oracle DB가 직접 검증하는 환경이 아니다.** AD DS는 사용자·그룹·UPN을 제공하고, 이후 Keycloak 또는 별도 검증 서비스가 AD LDAP을 기반으로 발급·검증한 토큰의 안정적인 subject를 MCP의 업무 사용자로 해석한다. Entra tenant와 app registration이 준비되면 OIDC issuer/audience/JWKS 검증 경로로 교체 또는 병행한다.
### 2026-08-05: OCI IAM AD Bridge 전환 목표
초기 PoC는 AD DS를 OIDC로 연결하기 위해 Keycloak을 사용했다. 이제 OCI Identity Domain의 **Microsoft Active Directory (AD) Bridge**와 **delegated authentication**을 사용해 Keycloak을 대체한다. AD DS는 계속 사용자·비밀번호의 원천이며, OCI Identity Domain이 AD Bridge를 통해 AD 비밀번호를 확인한 후 AIPF/MCP용 OIDC token을 발급한다.
```text
기존: AD DS → Keycloak → Keycloak user JWT → MCP → DDS
전환: AD DS → OCI AD Bridge + delegated authentication
→ OCI Identity Domain user JWT → MCP → DDS
```
이미 운영 중인 OCI database resource application `DDS_ORACLE_DB_TEST`와 DB service client `DDS_MCP_SERVICE_TEST`는 제거하거나 사용자 로그인 client로 재사용하지 않는다. 이들은 MCP가 Oracle DB DDS context를 여는 **machine-to-machine** 신뢰에 계속 사용한다. 사용자 로그인에는 별도의 OCI confidential OAuth application을 만든다.
#### 전환 원칙
1. Keycloak, 기존 mapping, 기존 AIPF source는 OCI 경로 검증 전까지 유지한다.
2. OCI AD Bridge가 AD 사용자·그룹을 동기화하고 delegated authentication으로 `dds-alice`/`dds-bob`의 **AD 비밀번호**를 확인하는 것을 먼저 검증한다.
3. AIPF 전용 OCI OAuth client를 별도로 만든다. callback URI와 refresh token 사용은 기존 AIPF 호환 설정을 유지한다.
4. MCP는 OCI Identity Domain의 discovery/JWKS, issuer, audience로 검증 대상을 바꾼다. Keycloak token과 OCI token을 동시에 허용하지 않는다.
5. OCI token의 검증된 `iss + sub`를 별도 binding으로 Alice=`DDS_U_1`, Bob=`DDS_U_2`에 등록한다.
6. OCI 경로에서 Alice 1건 반환과 Bob default deny를 확인한 뒤에만 Keycloak source와 DNS/VM 정리 여부를 결정한다.
#### 현재 전환 인벤토리
| 항목 | 현재 값/상태 | 전환 시 처리 |
|---|---|---|
| Windows AD DS | `dds.test`, `hmm-ad-dss-test-isolated`, 실행 중 | AD 사용자 원천으로 유지 |
| OCI Identity Domain | `identityAPAC`, 활성 | AD Bridge와 사용자 OAuth client를 이 Domain에 추가 |
| DB resource app | `DDS_ORACLE_DB_TEST` | 유지 |
| DB service client | `DDS_MCP_SERVICE_TEST` | 유지 |
| 사용자 OIDC issuer | Keycloak `https://ad.cloud-handson.com/realms/dds-test` | OCI Domain issuer로 교체 |
| AIPF OAuth client | Keycloak `aipf-dds-mcp` | OCI Domain의 새 confidential client로 교체 |
| MCP public endpoint | `https://hmm-backoffice.cloud-handson.com/mcp/dds/sse` | 유지 |
| DDS local users/data grants | `DDS_U_1` 허용, `DDS_U_2` grant 없음 | 유지 |
#### OCI AD Bridge 구축 절차
1. `identityAPAC`의 **Directory integrations**에서 Microsoft AD Bridge를 생성한다.
2. OCI가 표시하는 Domain URL, Bridge client ID/secret을 기록하고 Bridge client installer를 내려받는다. 이 client secret은 AIPF OAuth client secret과 다르며, Windows Bridge client 설정에만 사용한다.
3. AD domain에 join된 Windows VM에 Bridge client를 설치한다. 운영은 Domain Controller와 별도 member server를 권장하지만, 이 격리 PoC는 기존 AD VM에서 설치 가능 여부를 우선 검증한다.
4. Bridge service account에는 동기화 대상 OU/그룹에 대한 read 권한과 `cn=Deleted Objects` 읽기 권한을 준다. delegated authentication도 사용할 경우 Oracle이 요구하는 password/lockout attribute 권한을 최소 범위로 부여한다.
5. LDAPS(636)를 사용하도록 구성하고, Bridge VM에서 OCI Domain HTTPS 443 및 AD LDAP/LDAPS 연결을 확인한다.
6. `dds-alice`, `dds-bob`이 포함된 OU와 필요한 AD 그룹만 동기화 대상으로 선택한다.
7. OCI console의 **Delegated authentication**에서 Bridge를 시험하고 활성화한다. 성공하면 OCI 로그인 화면의 password 검증은 AD에서 수행한다.
8. AIPF용 OCI confidential OAuth application을 생성하고, AIPF callback URI, authorization-code/refresh-token grant, `openid` scope를 설정한다.
9. OCI discovery document의 issuer/JWKS와 token의 `aud`, `sub`를 확인한 뒤 MCP runtime과 `CB_EXTERNAL_IDENTITY_BINDING`을 교체한다.
#### 전체 구성도: AD 계정 로그인부터 DDS 권한 적용까지
아래 구성에서 AD에 새로 붙는 구성요소는 **OCI IAM AD Bridge Client**다. 이는 AD Domain Controller 내부의 AD DS를 대체하거나 AD 비밀번호를 복제하는 서버가 아니다. Domain-joined Windows에 설치되는 Windows service로서, AD에는 LDAP/LDAPS로 연결하고 OCI Identity Domain에는 HTTPS 443으로 연결한다.
Windows AD의 계정으로 AIPF MCP에 로그인하고, Oracle Deep Sec(DDS)이 사용자마다 다른 데이터를 보이도록 하는 PoC다. 초기에는 Keycloak이 AD 인증을 OIDC token으로 바꿨다. 현재는 Keycloak을 OCI Identity Domain의 **AD Bridge + delegated authentication**으로 대체하는 중이다.
```mermaid
flowchart LR
subgraph USER[사용자·클라이언트]
U["업무 사용자<br/>dds-alice / dds-bob"]
AIPF["AIPF<br/>MCP OAuth client"]
end
subgraph ADNET[OCI 격리 네트워크 · dds.test]
AD["Windows Server 2022<br/>AD DS Domain Controller<br/>계정·비밀번호·그룹 원천"]
BRIDGE["OCI IAM AD Bridge Client<br/>Windows service<br/>사용자/그룹 동기화 + 인증 위임"]
AD -->|"LDAP 또는 LDAPS<br/>사용자·OU·그룹 읽기"| BRIDGE
BRIDGE -->|"delegated authentication<br/>AD 비밀번호 확인 요청"| AD
end
subgraph OCIID[OCI Identity Domain · identityAPAC]
DOMAIN["OCI Identity Domain<br/>사용자 디렉터리·SSO·OIDC issuer"]
OAUTH["AIPF_DDS_AD_TEST<br/>confidential OAuth client<br/>authorization_code + refresh_token"]
DBAPP["DDS_ORACLE_DB_TEST<br/>database resource / integrated app<br/>DB_ACCESS_SCOPE"]
SVC["DDS_MCP_SERVICE_TEST<br/>confidential service client<br/>client_credentials only"]
DOMAIN --- OAUTH
DOMAIN --- DBAPP
DOMAIN --- SVC
end
BRIDGE -->|"HTTPS 443<br/>AD 사용자·그룹 동기화"| DOMAIN
U -->|"1. AIPF에서 MCP 연결"| AIPF
AIPF -->|"2. OAuth authorization code"| OAUTH
OAUTH -->|"3. AD 비밀번호 검증 위임"| BRIDGE
OAUTH -->|"4. OCI user access token<br/>iss + aud + sub"| AIPF
subgraph MCPHOST[HMM Backoffice VM]
MCP["DDS MCP Server<br/>hmm-backoffice.cloud-handson.com<br/>/mcp/dds/sse"]
JWT["JWT 검증<br/>OCI discovery / JWKS<br/>issuer + audience + exp"]
MAP["Identity binding<br/>OCI iss + sub<br/>→ CB_APP_USER → DDS_U_n"]
DBTOKEN["DB service token 획득<br/>client credentials"]
MCP --> JWT --> MAP
MCP --> DBTOKEN
end
AIPF -->|"5. Authorization: Bearer<br/>OCI user access token"| MCP
MCP -->|"6. discovery/JWKS HTTPS"| DOMAIN
DBTOKEN -->|"7. client ID + secret<br/>DB_ACCESS_SCOPE"| SVC
SVC -->|"8. database-access token"| DBTOKEN
subgraph ADB[Oracle Autonomous Database · HMMAIPOC]
APPID["DB application identity<br/>DDS_MCP_SERVICE_TEST 신뢰"]
CTX["ORA_END_USER_CONTEXT<br/>DDS_U_1 또는 DDS_U_2 attach"]
DDS["Oracle Deep Sec<br/>DATA ROLE / DATA GRANT<br/>default deny"]
DATA["CB_VECTOR_SEARCH_DOCUMENTS<br/>보호 데이터"]
APPID --> CTX --> DDS --> DATA
end
DBTOKEN -->|"9. database-access token"| APPID
MAP -->|"10. 선택된 local END USER"| CTX
DATA -->|"11. 허용된 행만"| MCP
MCP -->|"12. tool result"| AIPF
U["AD 사용자<br/>Alice / Bob"] --> A["Windows AD DS<br/>계정·비밀번호 원천"]
A <--> B["OCI AD Bridge<br/>동기화·인증 위임"]
B <--> I["OCI Identity Domain<br/>SSO·OIDC token 발급"]
I --> P["AIPF<br/>MCP OAuth 연결"]
P --> M["DDS MCP<br/>JWT 검증·사용자 매핑"]
M --> D["Oracle DDS<br/>DATA GRANT 집행"]
```
| 번호 | 구간 | 전달되는 것 | 보안 의미 |
|---:|---|---|---|
| 1~4 | 사용자 → AIPF → OCI Domain → AD Bridge → AD | AD 사용자명·비밀번호, OAuth code, OCI user token | AD는 비밀번호 원천으로 남고 OCI Domain이 OIDC issuer 역할 수행 |
| 5~6 | AIPF → MCP → OCI Domain | Bearer user JWT, JWKS/discovery | MCP가 서명·issuer·audience·만료를 검증해 사용자 위조 차단 |
| 7~9 | MCP → OCI Domain → DB | service client credentials, DB 전용 access token | MCP process만 DB context를 열 수 있음; user JWT를 DB service token으로 쓰지 않음 |
| 10~12 | MCP → DB DDS → AIPF | local DDS END USER, 필터된 query 결과 | Alice/Bob 권한은 DB DATA GRANT에서 최종 강제 |
이 그림에서 기억할 점: **AD는 비밀번호를 확인하고, OCI Identity Domain은 로그인 화면과 JWT를 발급하며, Oracle DDS는 최종 데이터 권한을 강제한다.** AD Bridge는 그 사이를 연결하는 Windows 서비스다.
#### AD Bridge가 하는 일과 하지 않는 일
## 현재 검증 결과
| AD Bridge가 하는 일 | AD Bridge가 하지 않는 일 |
| 항목 | 결과 |
|---|---|
| AD OU의 사용자·그룹 변경을 OCI Domain으로 동기화 | AD Domain Controller 또는 LDAP 서버를 대체하지 않음 |
| OCI Domain의 delegated authentication 요청을 AD에 전달해 AD 비밀번호를 검증 | AD 사용자 비밀번호를 MCP나 AIPF에 전달하지 않음 |
| AD 계정 비활성화·그룹 변경을 OCI user 상태/그룹과 동기화 | AIPF OAuth client secret 또는 DB service client secret을 보관하지 않음 |
| OCI Domain과 AD 사이의 제한된 연결을 담당 | Oracle DB의 DDS data grant를 평가하거나 우회하지 않음 |
| Keycloak → MCP → DDS 경로 | 완료 |
| `dds-alice``휴가` 검색 | `휴가 정책 - Alice 전용 테스트` 1건 반환 |
| `dds-bob`의 같은 검색 | DATA GRANT 없음으로 default deny (`ORA-00942` 은닉) |
| OCI AIPF OAuth client | `AIPF_DDS_AD_TEST` 생성 및 OIDC discovery 확인 완료 |
| OCI AD Bridge / delegated authentication | Windows Bridge client 설치·동기화 전 |
Bridge 설치 위치는 운영에서는 AD Domain Controller와 분리한 domain-joined Windows member server가 원칙이다. 이 PoC는 격리된 단일 Windows VM이므로 동일 VM 설치 가능성을 확인하되, 결과 문서에는 이 제약을 남긴다. Bridge VM에는 OCI Domain으로의 outbound HTTPS 443과 AD DS로의 LDAP/LDAPS 389/636만 허용한다.
## 핵심 결정
#### 적용 가이드: OCI AD Bridge와 AIPF OAuth client
1. 사용자 token과 DB service token은 분리한다.
- 사용자 token: 누가 MCP를 호출했는가
- DB service token: MCP service가 DDS context를 열 수 있는가
2. 사람별 권한 키는 검증된 `issuer + sub`이며, UPN/email은 표시·감사용이다.
3. DB 권한은 MCP가 아닌 Oracle DDS DATA ROLE/DATA GRANT에서 default deny로 집행한다.
4. OCI 전환은 기존 Keycloak을 즉시 제거하지 않고, OCI 경로의 Alice/Bob 결과가 같은지 확인한 뒤에만 완료한다.
이 절은 다음 환경에서 재적용할 때 사용하는 runbook이다. 명령의 secret 값은 출력하거나 Git에 넣지 않는다.
## 문서 지도
##### 1. 사전 점검
| 점검 | 기대값 | 실패 시 조치 |
|---|---|---|
| OCI Identity Domain | `identityAPAC` 등 대상 Domain이 `ACTIVE` | Domain 관리자 권한 및 Domain type/AD Bridge 허용량 확인 |
| AD DS | `dds.test` Domain Controller와 DNS·LDAP 정상 | Bridge를 Domain-joined Windows에 설치할 수 있는지 확인 |
| 네트워크 | Bridge VM → OCI Domain TCP/443, Bridge VM → AD TCP/636(LDAPS 권장) | NSG, Windows Firewall, proxy, AD 인증서 점검 |
| AD service account | 동기화 OU 읽기 및 delegated authentication에 필요한 최소 attribute 권한 | Domain Admin을 상시 사용하지 말고 전용 service account 생성 |
| AIPF callback | `https://aipf.cloud-handson.com/agentFactory/v1/tools/mcp/callback``/v1/tools/mcp/callback` | 두 URL 모두 OCI OAuth client redirect URI에 등록 |
현재 PoC의 Windows VM은 `hmm-ad-dss-test-isolated`이며 실행 중이다. OCI에서 확인한 public WinRM HTTPS(5986)는 `Connection refused` 상태다. 따라서 Bridge client 설치는 현재 RDP로 Windows에 접속해 수행하거나, 운영 자동화를 원하면 WinRM HTTPS와 Windows Firewall을 별도 구성해야 한다. 이 WinRM 상태는 AD Bridge 자체의 요구사항이 아니라 원격 설치 방식의 제약이다.
##### 2. OCI AD Bridge 생성과 Windows 설치
1. OCI Console에서 **Identity & Security → Domains → 대상 Domain → Directory integrations → Add → Microsoft Active Directory Bridge**로 이동한다.
2. 화면에서 표시되는 **Identity Domain URL, Bridge client ID, Bridge client secret**을 안전한 password manager/secret store에 보관한다. 이것은 AIPF OAuth client secret 및 DB service client secret과 다른 값이다.
3. Bridge client installer를 내려받아 domain-joined Windows에 설치한다.
4. 설치 UI에서 위 OCI Domain URL/client ID/client secret 및 AD Bridge service account를 입력하고 연결을 시험한다.
5. AD 연결은 **LDAPS 사용**을 선택한다. AD DS 인증서가 Bridge Windows trust store에서 신뢰되지 않으면 먼저 CA/서버 인증서를 배포한다. 설치 후 SSL 선택은 변경이 어렵다.
6. Directory integrations 화면에서 동기화할 사용자 OU, 그룹 OU를 최소 범위로 선택한다. `dds-alice`, `dds-bob`과 필요한 역할 그룹이 포함돼야 한다.
7. Bridge initial sync 후 OCI Domain Users/Groups에 두 사용자가 나타나는지 확인한다.
8. **Security → Delegated authentication**에서 `Test Delegated Authentication`으로 AD 사용자명과 AD 비밀번호를 시험하고 성공한 뒤에만 활성화한다.
`dds-alice`/`dds-bob` 로그인은 계속 AD 계정으로 한다. OCI Domain에 동기화된 user 레코드는 SSO/OIDC subject를 위한 cloud-side representation이고, delegated authentication 활성화 뒤의 비밀번호 검증 원천은 AD DS다.
##### 3. AIPF용 OCI OAuth client 생성
OCI Domain에는 DB service client와 별도로 AIPF용 confidential client가 필요하다. 이 PoC에서 만든 application 이름은 `AIPF_DDS_AD_TEST`이며 다음 정책을 사용한다.
| 항목 | 적용값 |
| 필요한 경우 | 읽을 문서 |
|---|---|
| Application type | Confidential OAuth client (`CustomWebAppTemplateId`) |
| Grants | `authorization_code`, `refresh_token` |
| Refresh token | 허용 (`allowOffline=true`) |
| Redirect URI | AIPF callback 2개 경로 |
| Access token expiry | 3600초 |
| Refresh token expiry | 1209600초(14일) |
| Consent | PoC에서 bypass; 운영 전 사용자 동의/정책 검토 |
| 전체 신뢰 경계, token 두 종류, DB/DDS 매핑 구조를 이해할 때 | [아키텍처 상세](architecture.md) |
| OCI AD Bridge 설치, OCI OAuth client 등록, AIPF/MCP 전환을 수행할 때 | [적용 Cookbook](cookbook.md) |
| 로그인·callback·token·LDAPS·권한 오류를 진단할 때 | [트러블슈팅](troubleshooting.md) |
| 저장소 전체 문서 작성 구조를 확인할 때 | [문서 구조 표준](../../DOCUMENTATION-STANDARDS.md) |
OCI CLI로 생성할 경우 Identity Domain App API의 필수 `basedOnTemplate`을 반드시 포함한다.
## 전환 완료 기준
```json
{
"schemas": ["urn:ietf:params:scim:schemas:oracle:idcs:App"],
"displayName": "AIPF_DDS_AD_TEST",
"basedOnTemplate": {"value": "CustomWebAppTemplateId"},
"active": true,
"isOAuthClient": true,
"clientType": "confidential",
"allowedGrants": ["authorization_code", "refresh_token"],
"redirectUris": [
"https://aipf.cloud-handson.com/agentFactory/v1/tools/mcp/callback",
"https://aipf.cloud-handson.com/v1/tools/mcp/callback"
],
"allowOffline": true
}
```
- OCI AD Bridge가 `dds-alice`, `dds-bob`과 필요한 AD 그룹을 동기화한다.
- OCI Identity Domain login이 두 사용자의 **AD 비밀번호**로 delegated authentication을 통과한다.
- AIPF가 OCI OAuth login 후 MCP에 연결된다.
- OCI JWT의 `issuer + sub`가 각각 `DDS_U_1`, `DDS_U_2`로 매핑된다.
- 동일한 `휴가` 검색에서 Alice는 1건을 받고 Bob은 default deny가 유지된다.
생성된 app ID/client ID/client secret은 local 검증 환경에서는 git-ignore된 `.runtime/aipf-dds-oci-ad-client.env`에만 저장한다. 파일에는 `OCI_AIPF_DDS_APP_ID`, `OCI_AIPF_DDS_CLIENT_ID`, `OCI_AIPF_DDS_CLIENT_SECRET`만 두며 값은 문서·Redmine·채팅·shell 출력에 기록하지 않는다.
## 보안 원칙
##### 4. AIPF에 입력할 OCI OAuth 값
OCI OpenID discovery 문서에서 endpoint를 확인한다. URL을 임의로 조합하지 말고 아래 discovery endpoint의 실제 응답을 기준으로 한다.
```text
https://<identity-domain>/.well-known/openid-configuration
```
현재 `identityAPAC`에서 확인한 형식은 다음과 같다.
| AIPF 입력 항목 | 값 형식 |
|---|---|
| Server URL | `https://hmm-backoffice.cloud-handson.com/mcp/dds/sse` |
| Authentication mode | `OAuth` |
| OAuth client ID/secret | `AIPF_DDS_AD_TEST` 생성 결과의 client ID/secret |
| Authorization URL | `https://idcs-<domain>.identity.oraclecloud.com:443/oauth2/v1/authorize` |
| Token / Refresh URL | `https://idcs-<domain>.identity.oraclecloud.com:443/oauth2/v1/token` |
| Scopes | `openid profile email offline_access` (실제 app policy에 맞춰 조정) |
##### 5. MCP 및 DB 매핑 전환
Bridge와 AIPF OAuth login을 검증한 뒤, OCI access token을 decode하여 `iss`, `aud`, immutable `sub`를 기록한다. 그 다음에만 다음 값을 교체한다.
| 대상 | 변경 |
|---|---|
| HMM MCP `DDS_MCP_OIDC_ISSUER_URI` | OCI discovery가 반환한 issuer (현재 확인값 `https://identity.oraclecloud.com/`) |
| HMM MCP `DDS_MCP_OIDC_AUDIENCE` | OCI user access token의 실제 `aud` 값 |
| `CB_EXTERNAL_IDENTITY_BINDING` | OCI `iss + sub` → Alice/Bob application user 등록 |
| AIPF MCP source | OCI OAuth client ID/secret/endpoint로 신규 source 생성 |
OCI token을 실제로 받은 뒤 audience를 정하는 이유는 client ID, resource scope, Domain 정책에 따라 access token claim이 달라질 수 있기 때문이다. 추측값으로 MCP를 먼저 재시작하면 기존 Keycloak 검증을 불필요하게 중단시킬 수 있다.
#### 적용 중 확인된 트러블슈팅 기록
| 증상/오류 | 원인 | 해결/판정 |
|---|---|---|
| `401 Authorization Required`, endpoint에 `/admin/v1/admin/v1/Apps` | `oci identity-domains ... --endpoint``/admin/v1`까지 넣어 CLI가 경로를 중복 | CLI endpoint에는 Domain base URL만 넣는다. raw request일 때만 target URI에 `/admin/v1/...`를 넣는다. |
| `No such option: --header` | `oci raw-request``--header`가 아니라 `--request-headers` 사용 | `--request-headers '{"Content-Type":"application/json"}'`로 호출 |
| `Missing required attribute(s): basedOnTemplate.` | OCI App API는 OAuth client 생성 시 template 필수 | `"basedOnTemplate":{"value":"CustomWebAppTemplateId"}` 추가 |
| authorization endpoint가 `302`가 아니라 `HTTP 200 OK` | OCI Domain이 redirect 대신 로그인 HTML과 secure session cookie를 반환 | 정상. browser에서 OCI 로그인 화면이 보이면 OAuth preflight 통과로 판정 |
| OCI console browser automation 불가 | 현재 작업 환경에 browser binding 없음 | OCI Console 화면 조작이 필요한 Bridge 생성은 RDP/관리자 Console session으로 수행; API로 가능한 OAuth app/MCP 설정은 자동화 계속 |
| Windows VM WinRM 5986 `Connection refused` | WinRM HTTPS listener/Windows Firewall/NSG 미구성 | RDP 설치를 사용하거나 WinRM HTTPS를 별도 구성. Bridge 기능의 오류로 해석하지 않음 |
| OCI Compute Instance Run Command plugin은 `RUNNING`인데 명령이 계속 `ACCEPTED` / `VISIBLE` | plugin 상태 보고는 가능하지만 Windows agent가 command execution을 수신·실행하지 못함. PowerShell preflight와 단순 `echo` 모두 동일 | Bridge installer 실행 수단으로 사용하지 않는다. Windows의 Oracle Cloud Agent/Run Command service, outbound OCI connectivity, plugin 로그를 RDP에서 점검한 뒤 재시도한다. |
| AD Bridge test가 LDAPS certificate 오류 | AD DS 인증서 체인이 Bridge host trust store에 없음 | 사내 CA/AD 인증서를 Bridge host에 신뢰시키고 LDAPS를 유지 |
| OCI OAuth login은 성공하지만 MCP `401` | MCP issuer/audience가 Keycloak 값 그대로이거나 OCI `sub` binding 없음 | OCI JWT claim 확인 후 MCP env와 `CB_EXTERNAL_IDENTITY_BINDING`을 함께 교체 |
| AIPF source에서 이전 사용자로 로그인 | OCI Domain browser SSO session 유지 | OCI Domain logout 후 source 재연결; AIPF source 저장 token과 browser SSO session을 구분 |
#### 전환 검증 기준
| 단계 | 증거 | 성공 기준 |
|---|---|---|
| AD Bridge 연결 | Bridge 상태 `Connected`, AD 사용자/그룹 동기화 결과 | `dds-alice`, `dds-bob`이 OCI Domain에 존재 |
| delegated authentication | OCI Domain의 test delegated authentication | 두 사용자의 AD 비밀번호로 성공 |
| OAuth code flow | AIPF가 OCI login → callback → MCP source 연결 완료 | OCI issuer의 access token이 저장됨 |
| MCP 검증 | OCI JWT의 signature/issuer/audience 통과 | `iss + sub` mapping 조회 성공 |
| DDS 권한 | 같은 `휴가` tool 호출 | Alice 1건, Bob default deny |
| 회귀/보안 | Keycloak token, 미매핑 OCI token, 만료 token | DB query 전 거부 |
### 이 설계가 답하는 질문
이 PoC는 다음 네 가지를 의도적으로 분리한다.
| 질문 | 답 | 담당 구성요소 |
|---|---|---|
| 직원 계정과 비밀번호는 어디에 있는가? | Windows AD DS가 원천이다. | AD DS |
| 사용자가 MCP에 로그인했음을 어떻게 증명하는가? | Keycloak이 AD LDAP 인증을 거쳐 OIDC access token(JWT)을 발급한다. | Keycloak |
| MCP 서버가 Oracle DB에 접속해 DDS context를 열 자격은 어떻게 얻는가? | OCI Identity Domain의 confidential service client가 database-access token을 발급받는다. | OCI IAM integrated application / credential app |
| Alice와 Bob이 서로 다른 데이터만 보게 하는 최종 판단은 누가 하는가? | Oracle Deep Sec이 요청별 local DDS END USER와 DATA GRANT를 적용한다. | Oracle Database |
즉, **AD 계정으로 로그인한 사용자 토큰**과 **MCP 서버가 DB에 접속하는 서비스 토큰**은 목적과 발급자가 다른 별개의 token이다. 전자는 “누가 요청했는가”를, 후자는 “어떤 애플리케이션이 DB context를 열 수 있는가”를 증명한다. 하나를 다른 하나의 대체물로 쓰지 않는다.
## 2. 격리 인프라
| 항목 | 구성 |
|---|---|
| 네트워크 | `handson-vcn`의 전용 `10.0.2.0/28` public subnet |
| Windows VM | `hmm-ad-dss-test-isolated`, Windows Server 2022, 2 OCPU / 16 GB |
| 관리 접근 | RDP TCP/3389은 작업자 공인 IP 한 곳에만 NSG로 허용 |
| OIDC HTTPS | `https://ad.cloud-handson.com` (Caddy TLS reverse proxy → Keycloak) |
| 서브넷 보안 목록 | 인바운드 전부 차단, Windows Update 및 테스트용 아웃바운드만 허용 |
| 도메인 | `dds.test` (AD DS forest/domain) |
이 VM은 테스트 전용이다. 기존 공유 public subnet의 NAT 경로를 변경하지 않으며, 비용 발생 리소스이므로 검증 종료 뒤 중지 또는 삭제를 결정한다.
## 3. 식별자와 권한 흐름
```text
AD 사용자 (UPN: alice@dds.test, objectGUID)
│ LDAP / OIDC bridge
검증된 Bearer claims
iss, aud, exp, sub = 불변 외부 subject
│ MCP bearer authenticator
CB_EXTERNAL_IDENTITY_BINDING
issuer + subject -> CB_APP_USER.user_id
CB_DDS_END_USER_MAP
user_id -> DDS_U_<user_id> local END USER
ORA_END_USER_CONTEXT + DDS DATA GRANT / 권한 함수
```
`sub`는 Keycloak이 발급하는 안정적인 federated-user 식별자다. AD objectGUID는 원천 디렉터리의 감사 식별자로 유지하되, 실제 권한 키는 **검증된 JWT의 `iss + sub`**로 고정한다. UPN은 로그인 화면·감사 표시용으로 보관할 수 있지만 권한 키로 신뢰하지 않는다. `iss + sub` 조합이 유일하지 않거나, 매핑이 없거나, 사용자가 비활성이면 요청은 fail-closed로 거부한다.
### 전체 요청 흐름
```text
(1) 사용자가 AIPF에서 dds MCP를 연결
│ OAuth authorization code flow
(2) Keycloak 로그인 화면
│ AD LDAP bind: dds-alice / dds-bob의 비밀번호 확인
(3) Keycloak access token 발급
│ iss, aud=dds-mcp, exp, sub 포함 / Keycloak signing key로 서명
(4) AIPF가 MCP 요청에 Bearer access token을 첨부
(5) MCP
│ Keycloak JWKS로 서명·issuer·audience·만료 검증
│ CB_EXTERNAL_IDENTITY_BINDING에서 iss+sub 조회
(6) MCP가 OCI IAM service token을 별도로 획득
│ client credentials: DDS_MCP_SERVICE_TEST
(7) Oracle DB
│ service token으로 trusted application identity 확인
│ 조회한 DDS_U_n end-user context attach
(8) Deep Sec
│ DDS_U_n의 DATA ROLE/DATA GRANT만 적용하여 SQL 실행
(9) MCP가 허용된 결과만 AIPF로 반환
```
MCP는 이 흐름의 **정책 집행점**이다. 사용자의 JWT를 검증하지 못하면 5단계에서 멈추고, DB service token이 유효하지 않으면 7단계에서 멈추며, 둘 다 통과해도 해당 DDS END USER의 grant가 없으면 8단계에서 멈춘다. 어느 단계에서도 실패를 우회하여 공유 DB 계정의 광범위한 권한으로 조회해서는 안 된다.
### 사용자 인증과 DB 접속 신뢰의 분리
이 PoC에는 서로 다른 두 신뢰 체인이 있다. 둘을 같은 OAuth token으로 혼동하지 않는다.
```text
[업무 사용자 인증]
AD user -> Keycloak (AD LDAP 인증) -> Keycloak JWT
-> MCP validates iss + sub -> DDS_U_1 / DDS_U_2 선택
[DB 접속 신뢰]
MCP service -> OCI IAM DDS_MCP_SERVICE_TEST (client credentials)
-> database-access token -> Oracle DB
-> 선택된 local DDS END USER context attach
```
| 역할 | 현재 담당 | 하는 일 |
|---|---|---|
| AD 사용자 인증 | Keycloak | AD LDAP의 계정·비밀번호를 확인하고 Keycloak JWT를 발급 |
| 업무 사용자 식별 | MCP | 검증된 Keycloak `iss + sub`를 HMM application user와 local DDS END USER에 매핑 |
| DB 접속 애플리케이션 신뢰 | OCI IAM credential app | MCP service가 database-access token을 받아 Oracle DB에 신뢰된 application임을 증명 |
| 최종 데이터 권한 | Oracle Deep Sec | attach된 local DDS END USER의 DATA ROLE과 DATA GRANT로 행·열을 제한 |
따라서 OCI IAM credential app은 현재 AD 사용자를 직접 로그인시키지 않는다. OCI IAM은 MCP service의 database-access token 발급자이며, Oracle DB가 해당 service client를 application identity로 신뢰하게 한다. AD 사용자가 어떤 DDS END USER가 되는지는 MCP가 결정한다.
향후 OCI IAM을 사용자 인증의 중심으로 바꾸려면 Keycloak을 OCI IAM의 외부 IdP(OIDC 또는 SAML)로 federation하고, AIPF/MCP가 OCI IAM user token을 받도록 변경한다. 그 경우 Oracle DB도 OCI IAM user token의 issuer·group claim을 직접 검증할 수 있다. 현재 PoC의 local DDS END USER 매핑과는 별도의 확장 경로다.
### OCI Identity Domain integrated application을 쓰는 이유와 역할
여기서 말하는 OCI Domain integrated application은 `DDS_ORACLE_DB_TEST` database resource application이다. 이를 사용자용 웹 로그인 앱으로 오해하면 안 된다. 이 앱은 **Oracle Autonomous Database가 신뢰할 OAuth audience와 scope를 OCI Identity Domain에 선언하는 등록물**이다.
일반 OAuth access token은 “어느 resource server를 위한 token인지”가 불명확하면 DB가 받아들여서는 안 된다. integrated application은 DB용 resource identity를 만들고, `DB_ACCESS_SCOPE` 같은 전용 scope를 발급 정책에 묶는다. 그러면 DB는 다음 세 가지가 일치할 때만 MCP의 service token을 받아들인다.
| DB가 확인하는 값 | integrated application에서 정하는 값 | 이 PoC의 의미 |
|---|---|---|
| resource application | `DDS_ORACLE_DB_TEST` | 이 token의 수신자가 DDS 대상 Oracle DB임을 식별 |
| scope | `DB_ACCESS_SCOPE` | DB context 접근이라는 최소 권한을 명시 |
| issuer | OCI Identity Domain URL | token을 발급하고 서명키를 제공하는 신뢰 경계 |
| OAuth client ID | `DDS_MCP_SERVICE_TEST` | token을 요청한 MCP service를 식별 |
`DDS_MCP_SERVICE_TEST`는 integrated application 자체가 아니라, 그 resource/scope를 요청할 수 있도록 허가된 **confidential service client**다. client secret을 가진 백엔드 MCP만 client credentials flow로 token을 발급받는다. 브라우저·AIPF·AD 사용자는 이 secret이나 service token을 보거나 보관하지 않는다.
```text
OCI Identity Domain
DDS_ORACLE_DB_TEST DDS_MCP_SERVICE_TEST
(resource / integrated app) (confidential OAuth client)
┌───────────────────────┐ ┌─────────────────────────┐
│ audience: Oracle DB │<--scope--│ client credentials only │
│ scope: DB_ACCESS_SCOPE│ │ secret: MCP host only │
└──────────┬────────────┘ └───────────┬─────────────┘
│ database-access token │ token request
└────────────────────────────────────┘
Autonomous DB application identity
```
이 구조를 쓰는 이유는 DB password를 MCP에 장기 보관하거나, 모든 사용자에게 DB 로그인 권한을 주지 않기 위해서다. service client는 DB context를 여는 최소 권한만 보유하고, 사람별 데이터 권한은 local DDS END USER에 남긴다. 서비스 client secret이 노출되더라도 해당 client에 부여하지 않은 데이터 권한이 자동으로 생기는 구조가 아니다. 다만 token 발급과 DB context 생성 자체를 악용할 수 있으므로 secret은 즉시 회전하고, 이 client에는 불필요한 OCI 권한을 부여하지 않는다.
### OCI IAM과 Keycloak을 함께 쓰는 이유
현재 AD DS는 LDAP/Kerberos 디렉터리이지 인터넷 서비스가 직접 검증할 OAuth JWT issuer는 아니다. Keycloak은 AD LDAP과 OIDC 사이의 번역 계층이다. 반면 OCI IAM integrated application은 Oracle DB가 지원하는 database-access token contract를 제공한다.
| 경계 | 입력 | 출력 | 선택 이유 |
|---|---|---|---|
| AD DS → Keycloak | AD 사용자명/비밀번호, LDAP 사용자·그룹 | Keycloak OIDC JWT | AD를 계정 원천으로 유지하면서 표준 OAuth/OIDC를 제공 |
| Keycloak → MCP | Keycloak JWT/JWKS | 검증된 `iss + sub` | MCP가 사용자 identity를 안전하게 해석 |
| MCP → OCI IAM | service client ID/secret | DB 전용 access token | DB가 신뢰할 service identity 증명 |
| OCI IAM → Oracle DB | database-access token | application identity | DB password 없이 DB context 접근을 제한 |
| Oracle DB → DDS | local END USER | 필터된 query 결과 | 데이터 권한을 DB 내부에서 강제 |
따라서 “OCI Domain credential app이 AD를 신뢰한다”는 표현은 현재 PoC에는 정확하지 않다. 현재는 **Keycloak이 AD를 신뢰해 사용자 인증을 수행**하고, **Oracle DB가 OCI IAM을 신뢰해 MCP service를 인증**한다. 두 체인은 MCP 안에서 만나며, MCP가 Keycloak token의 subject를 local DDS END USER로 결정한다.
## 4. 구현 단계
1. Windows Server에 AD DS를 설치하고 `dds.test` forest를 생성한다.
2. 테스트 사용자 `dds-alice`, `dds-bob`와 권한 그룹을 만들고, LDAP 조회와 인증을 확인한다.
3. AD LDAP과 연동한 OIDC bridge(예: Keycloak)를 별도 서비스로 구성한다. bridge가 내는 JWT의 `iss`, `aud`, `exp`, JWKS 서명을 MCP가 검증한다.
4. `issuer + sub`에서 `CB_APP_USER``CB_DDS_END_USER_MAP`을 해석하도록 백오피스/DB 매핑을 추가한다.
5. alice/bob의 서로 다른 `DDS_U_*` context에서 동일 MCP tool 호출 결과가 권한에 따라 달라지는지 검증한다.
6. 매핑 누락, 만료 토큰, 다른 issuer/audience, 비활성 계정은 모두 거부되는 regression을 추가한다.
## 4.1 현재 테스트 bridge
`database/adb/47_dds_ad_identity_test_setup.sql`은 HMM DB의 기존 업무 사용자 원천을 변경하지 않는다. 스크립트가 만든 `CB_EXTERNAL_IDENTITY_BINDING`에는 최종 OIDC issuer와 Keycloak이 실제로 발급한 subject만 보관한다.
| AD 사용자 | OIDC issuer | JWT `sub` | HMM application user | DDS END USER |
|---|---|---|---:|---|
| `dds-alice` | `https://ad.cloud-handson.com/realms/dds-test` | `11cf09c9-4713-4145-85cd-e3397d4a2405` | 1 | `DDS_U_1` |
| `dds-bob` | `https://ad.cloud-handson.com/realms/dds-test` | `d9610e18-a327-4dda-9496-69e5a2067460` | 2 | `DDS_U_2` |
`dds-mcp` client는 access token audience에 `dds-mcp`를 포함한다. MCP는 Keycloak JWKS에서 JWT 서명과 `iss`, `aud`, 만료 시간을 검증한 뒤에만 위 매핑을 조회한다. 실제 `/dds/mcp/messages` 초기화 요청에 Alice JWT를 넣어 성공 응답을 확인했다.
초기 게시 단계에서는 END USER와 DATA ROLE만 만들고 default deny로 시작한다. 현재 테스트에서는 Alice에만 vector view SELECT data grant를 추가했고 Bob은 grant 없이 유지한다.
### Keycloak 구성에서 중요한 값
Keycloak realm `dds-test`은 AD LDAP user federation을 사용한다. 로그인 화면에서 입력한 `dds-alice` 또는 `dds-bob`의 비밀번호 검증은 AD DS가 담당하며, Keycloak은 성공한 LDAP 사용자의 immutable identity를 자신의 `sub`로 유지해 JWT에 넣는다. AD 비밀번호나 LDAP bind credential은 MCP와 AIPF에 전달되지 않는다.
| Keycloak 항목 | 값/정책 | 필요한 이유 |
|---|---|---|
| Realm issuer | `https://ad.cloud-handson.com/realms/dds-test` | MCP의 허용 issuer와 identity binding의 namespace |
| MCP audience | `dds-mcp` | 다른 Keycloak client용 token의 재사용 방지 |
| AIPF confidential client | `aipf-dds-mcp` | AIPF OAuth callback과 token exchange 수행 |
| MCP resource client | `dds-mcp` | access token audience 검증 대상 |
| Redirect URI | AIPF callback 두 경로와 wildcard 등록 | authorization code를 허용된 AIPF에만 반환 |
| PKCE | AIPF client에서 미사용 | 현재 AIPF가 `code_challenge_method`를 보내지 않기 때문; 향후 AIPF 지원 시 S256으로 전환 |
| Direct grant | 테스트 client에서만 제한적으로 사용 | CLI 검증용; 운영 browser client에는 사용하지 않음 |
JWT 검증은 서명만 확인하는 것으로 충분하지 않다. MCP는 최소한 다음을 모두 확인해야 한다.
1. `alg`가 허용된 비대칭 서명 알고리즘인지 확인하고, Keycloak JWKS의 해당 `kid`로 signature를 검증한다.
2. `iss`가 realm issuer와 정확히 같은지 확인한다. URL의 realm·host가 다른 token은 거부한다.
3. `aud``dds-mcp`가 포함됐는지 확인한다.
4. `exp`, `nbf`, `iat` 및 허용 clock skew를 검사한다.
5. 검증된 `iss + sub`로만 `CB_EXTERNAL_IDENTITY_BINDING`을 조회한다. browser가 표시한 email, username, group 문자열만으로 매핑하지 않는다.
Keycloak signing key rotation 시 MCP의 JWKS cache가 새 `kid`를 다시 조회할 수 있어야 한다. issuer나 realm을 바꾸면 binding table의 issuer 값과 MCP allow-list도 함께 변경해야 한다.
### 테스트 Keycloak 사용자 비밀번호를 얻는 방법
**실제 로그인 인증 원천은 Windows AD DS다.** `dds-alice``dds-bob`은 Keycloak에 로컬로 만든 계정이 아니라 AD DS의 테스트 사용자다. AIPF가 Keycloak 로그인 화면으로 이동하면 사용자는 AD 사용자명과 AD 비밀번호를 입력하고, Keycloak은 LDAP을 통해 AD에 비밀번호를 확인한다. AD 인증이 성공할 때에만 Keycloak이 OIDC JWT를 발급한다.
```text
사용자: dds-alice + AD 비밀번호 입력
Keycloak ── LDAP 인증 요청 ──► Windows AD DS
│ │
└── 인증 성공 ◄─────┘
Keycloak OIDC access token 발급
```
`.runtime/dds-ad-test-users.env`는 인증 서버나 Keycloak 설정이 아니다. 테스트 중 AD 비밀번호를 안전하게 다시 입력할 수 있도록 **현재 AD에 설정된 테스트 비밀번호를 로컬에서 참고하는 git-ignore 파일**일 뿐이다. AD에서 비밀번호를 변경하면 실제 로그인에는 새 AD 비밀번호가 즉시 적용되며, 이 파일은 참고값으로만 함께 갱신한다. 이 비밀번호는 OAuth client secret이나 OCI IAM service client secret과 전혀 다른 값이다.
실제 값은 저장소·문서·채팅에 기록하지 않는다. 현재 작업 환경에서는 git-ignore된 권한 제한 파일 `.runtime/dds-ad-test-users.env`에 보관한다. 다음 명령은 비밀번호를 터미널에 표시하지 않고 macOS 클립보드에 복사한다.
```bash
# dds-alice 비밀번호 복사
sed -n 's/^DDS_ALICE_PASSWORD=//p' .runtime/dds-ad-test-users.env | pbcopy
# dds-bob 비밀번호 복사
sed -n 's/^DDS_BOB_PASSWORD=//p' .runtime/dds-ad-test-users.env | pbcopy
```
명령 출력이 없는 것이 정상이다. 원하는 사용자 항목을 실행한 다음 Keycloak 로그인 화면의 password 칸에 `⌘V`로 붙여넣는다. 로그인 ID는 각각 `dds-alice`, `dds-bob`이며, 도메인 표기가 필요한 Windows/LDAP 화면에서는 `DDS\\dds-alice`, `DDS\\dds-bob`을 사용한다. `pbcopy`가 올바른 명령이며 `pbcop`는 오타다.
파일이 없거나 비밀번호가 맞지 않으면 값 추측이나 문서 검색을 하지 않는다. AD 관리자에게 test-user password reset을 요청하고, reset 후 이 권한 제한 파일만 갱신한다. user password reset은 AIPF OAuth client secret이나 OCI IAM service client secret rotation을 의미하지 않는다.
## 4.2 AIPF MCP 등록 값
AIPF의 **Edit MCP server** 화면에서 다음 값으로 등록한다. AIPF는 confidential OAuth client를 요구하므로, 일반 테스트용 public client `dds-mcp`가 아니라 AIPF 전용 client `aipf-dds-mcp`를 사용한다.
| AIPF 항목 | 입력값 |
|---|---|
| Server name | `alice-dds` (Bob 검증 때는 `bob-dds`처럼 별도 등록) |
| Server URL | `https://hmm-backoffice.cloud-handson.com/mcp/dds/sse` |
| Authentication mode | `OAuth` |
| Redirect callback URL | AIPF 표시값 `https://aipf.cloud-handson.com/agentFactory/v1/tools/mcp/callback` — 입력하지 않는 읽기 전용 값 |
| OAuth client ID | `aipf-dds-mcp` |
| OAuth client secret | 운영자 보관 secret. 저장소·문서·대화에 기록하지 않는다. 현재 작업 환경에서는 `.runtime/aipf-dds-mcp-client.env``OAUTH_CLIENT_SECRET` 값을 사용한다. |
| Authorization URL | `https://ad.cloud-handson.com/realms/dds-test/protocol/openid-connect/auth` |
| Token URL | `https://ad.cloud-handson.com/realms/dds-test/protocol/openid-connect/token` |
| Refresh URL | `https://ad.cloud-handson.com/realms/dds-test/protocol/openid-connect/token` |
| Scopes | `openid profile email` |
### OAuth client secret을 얻는 방법
여기서 필요한 secret은 AD 사용자 비밀번호가 아니라 Keycloak의 `aipf-dds-mcp` OAuth client secret이다. secret은 저장소에 커밋하거나 문서·대화에 적지 않는다. 현재 작업 환경에서는 권한 제한 파일에 보관하며, 다음 명령으로 **값을 화면에 표시하지 않고** macOS 클립보드에 복사한다.
```bash
sed -n 's/^OAUTH_CLIENT_SECRET=//p' .runtime/aipf-dds-mcp-client.env | pbcopy
```
명령 출력이 없는 것이 정상이다. 이후 AIPF의 **OAuth client secret** 입력칸에 `⌘V`로 붙여넣고 저장한다. 복사 명령은 `pbcopy`이며 `pbcop`가 아니다. 파일의 secret 값은 Keycloak에서 OAuth client를 재생성하거나 rotation한 경우에만 다시 동기화한다.
### AIPF callback 호환 보정
현재 AIPF는 화면에 표시하는 callback과 달리 OAuth authorization request에는 `/agentFactory`가 빠진 `https://aipf.cloud-handson.com/v1/tools/mcp/callback`을 보낸다. AIPF Nginx는 이 경로를 같은 origin의 실제 callback으로 302 전환하도록 구성했다.
```text
/v1/tools/mcp/callback
-> /agentFactory/v1/tools/mcp/callback
```
이 전환은 OAuth `code``state`를 보존하고 브라우저가 `/agentFactory` 범위의 AIPF 세션 쿠키를 다시 전송하게 한다. AIPF의 OAuth callback 생성 로직이 수정되면 이 Nginx 보정은 제거할 수 있다.
### AIPF 로그인 상태와 MCP 등록을 구분할 것
AIPF의 MCP 등록 정보와 Keycloak의 브라우저 SSO 세션은 서로 다르다.
| 상태 | 보관 위치 | 영향 |
|---|---|---|
| MCP OAuth client ID/secret, 연결 설정 | AIPF의 MCP source 설정 | 최초 OAuth 연결 및 refresh token 교환에 사용 |
| Keycloak SSO 로그인 세션 | 브라우저의 `ad.cloud-handson.com` cookie | 다음 OAuth 연결 시 이전 사용자로 자동 로그인될 수 있음 |
| AIPF가 저장한 MCP token/refresh token | AIPF backend source별 저장소 | 등록한 MCP source가 이후 tool 호출할 때 사용 |
그러므로 Alice에서 Bob을 시험할 때는 source를 `bob-dds`로 새로 만들거나 기존 연결을 끊고, Keycloak logout을 먼저 수행한다. 같은 browser session에서 바로 다시 연결하면 AIPF 설정을 Bob으로 바꿔도 Keycloak SSO가 Alice를 다시 인증할 수 있다. Keycloak logout endpoint는 다음과 같다.
```text
https://ad.cloud-handson.com/realms/dds-test/protocol/openid-connect/logout
```
새 AIPF source에는 OAuth client ID와 secret을 다시 입력한다. source 간에 인증정보가 자동 상속된다고 가정하지 않는다.
## 4.3 테스트 시나리오
### A. Alice 인증과 MCP 연결
1. AIPF에서 위 값으로 `alice-dds`를 저장하고 연결한다.
2. Keycloak 로그인 화면에서 AD 사용자 `dds-alice`로 로그인한다. 도메인 표기가 필요하면 `DDS\\dds-alice`를 사용한다.
3. AIPF가 callback으로 돌아오면 `alice-dds` MCP 연결이 완료된다.
4. AIPF Agent 대화에서 다음 요청을 실행한다.
```text
dds_vector_search 도구를 사용해서 "휴가"를 검색해줘.
embeddingMode는 DEMO, limit은 10으로 해줘.
```
5. MCP는 Keycloak access token의 검증된 `iss + sub`를 Alice의 application user와 `DDS_U_1`에 매핑한 뒤 `dds_vector_search`를 호출한다.
### B. Bob 권한 비교
1. AIPF OAuth 연결을 끊거나 새 MCP 등록 `bob-dds`를 만든다.
2. 같은 OAuth 설정으로 연결한 뒤 AD 사용자 `dds-bob`으로 로그인한다.
3. Alice와 동일한 검색 요청을 실행한다.
4. DDS data grant 구성 후 기대 결과는 다음과 같다.
| 사용자 | JWT subject 매핑 | 기대 결과 |
|---|---|---|
| `dds-alice` | application user `1` → `DDS_U_1` | 허용된 지식 행만 반환 |
| `dds-bob` | application user `2` → `DDS_U_2` | 빈 결과 또는 DDS 권한 거부 |
### C. 판정 기준과 현 상태
| 확인 항목 | 판정 방법 | 현재 상태 |
|---|---|---|
| OAuth 로그인 | AIPF가 Keycloak 로그인 후 MCP 등록 화면으로 복귀 | 확인 완료 |
| JWT 검증/매핑 | `tools/list` 또는 `dds_vector_search`가 Bearer 토큰 검증을 통과 | 확인 완료 |
| Alice/Bob 데이터 차이 | 같은 `dds_vector_search` 호출에서 반환 행이 다름 | 확인 완료 — Alice 1건 반환, Bob default deny |
| 미매핑 토큰 거부 | 매핑 없는 `iss + sub`의 MCP 호출이 `AUTHORIZATION_DENIED` | 구현 완료, 회귀 검증 대기 |
`dds_vector_search`는 현재 유일한 DDS MCP 도구이며 입력값은 `query`(필수), `limit`(1~100), `embeddingMode`(`DEMO` 또는 `AI`)다. service identity와 Alice 허용/Bob 거부 DDS grant를 반영해 권한 차이까지 검증했다.
## 4.4 OCI IAM database-access token 구성
Keycloak access token은 MCP의 업무 사용자 식별과 `iss + sub` 매핑에만 사용한다. Oracle Deep Sec context를 열 때는 별도의 OCI IAM database-access token이 필요하다. 이 토큰은 `DDS_MCP_SERVICE_TEST` confidential client가 client credentials flow로 받고, DB는 해당 client ID를 application identity로 신뢰한다.
```text
AD user -> Keycloak token -> MCP user mapping -> local DDS END USER
\
OCI IAM DDS_MCP_SERVICE_TEST -- database-access token --> Oracle Deep Sec context
```
구성 순서는 OCI IAM database resource(`DDS_ORACLE_DB_TEST`)와 scope(`DB_ACCESS_SCOPE`) 생성, OCI IAM service client 생성, Autonomous DB의 OCI IAM identity provider/credential 등록, 그리고 `CREATE APPLICATION IDENTITY ... MAPPED TO 'IAM_OAUTH_CLIENT_ID=...'` 순서다. 이 단계는 Autonomous DB의 외부 인증 구성을 변경할 수 있으므로 기존 설정을 먼저 조회하고 Redmine에 기록한다.
### 실제 구성 절차와 신뢰 등록 방향
다음 순서는 “누가 누구를 신뢰하도록 등록하는가”를 기준으로 작성했다. secret, client ID, application ID는 환경별 값이므로 이 문서에 넣지 않고 권한 제한 환경 파일 또는 OCI console에서 관리한다.
1. OCI Identity Domain에서 database resource/integrated application `DDS_ORACLE_DB_TEST`를 만들고 `DB_ACCESS_SCOPE`를 정의한다. 이것이 DB가 받아들일 audience와 scope의 계약이다.
2. 같은 Domain에서 confidential client `DDS_MCP_SERVICE_TEST`를 만든다. grant type은 `client_credentials`만 허용하고, 1단계의 DB scope만 허용한다. 발급된 client secret은 MCP host의 mode 600 환경 파일에만 저장한다.
3. Autonomous DB에 OCI IAM external authentication을 활성화하고, Domain URL 및 database resource application ID를 등록한다. DB의 issuer URL은 token `iss`와 정규형까지 일치해야 한다. 이 환경에서는 HTTPS `:443`을 포함한다.
4. DB에 OCI IAM signing-key credential을 만들고, `DDS_MCP_SERVICE_TEST`의 OAuth client ID를 DB application identity에 매핑한다. 이 단계로 DB는 “이 client credentials로 발급된 DB token을 가진 서비스”를 신뢰한다.
5. HMM MCP runtime에 service client ID, secret, token endpoint, scope, resource 대상 view를 주입하고 서비스 계정을 재시작한다. 사용자 access token이나 AD 비밀번호를 이 파일에 넣지 않는다.
6. 별도로 Keycloak `iss + sub` → `CB_APP_USER` → `CB_DDS_END_USER_MAP`을 등록한다. `DDS_U_1_ROLE` 같은 local role에 대상 객체 DATA GRANT를 필요한 사용자에게만 부여한다.
신뢰의 방향은 아래와 같다.
```text
AD DS ──(LDAP credential 확인)──► Keycloak
Keycloak ──(signed user JWT)────► MCP
MCP ──(client credentials)──────► OCI Identity Domain
OCI Identity Domain ──(signed DB token)──► Oracle DB
Oracle DB ──(DDS grant 평가)────► 데이터
```
Oracle DB는 AD DS나 Keycloak을 직접 신뢰하도록 등록되어 있지 않다. 반대로 OCI IAM integrated application도 AD 사용자 credential을 검증하지 않는다. MCP가 두 신뢰 체인의 검증 결과를 결합하는 위치다.
### MCP 런타임에서 token을 사용하는 방식
MCP 요청을 처리할 때 서비스는 두 token을 다음 순서로 취급한다.
| 순서 | token | MCP가 하는 일 | 실패하면 |
|---:|---|---|---|
| 1 | Keycloak user access token | HTTP `Authorization: Bearer`에서 추출, JWKS 검증, `iss + sub` binding 해석 | `401` 또는 `AUTHORIZATION_DENIED`; DB에 접속하지 않음 |
| 2 | OCI IAM database-access token | server-side client credentials로 얻어 DB connection/context attach에 사용 | DDS context 불가; 사용자 token으로 대체하지 않음 |
| 3 | 없음(내부 context) | `DDS_U_n`의 data role/data grant로 target view를 query | Oracle default deny; 결과 반환 안 함 |
MCP endpoint는 `https://hmm-backoffice.cloud-handson.com/mcp/dds/sse`이며, SSE transport의 message endpoint는 `https://hmm-backoffice.cloud-handson.com/mcp/dds/messages`다. browser 주소창으로 SSE URL을 여는 것은 OAuth 로그인 페이지가 아니라 event stream 연결 시도이므로 유효한 동작 검증 방법이 아니다. AIPF OAuth 등록 또는 Bearer token을 포함한 MCP client로 접속해야 한다.
### 적용 결과와 검증
| 구성 요소 | 적용값 | 상태 |
|---|---|---|
| OCI IAM database resource | `DDS_ORACLE_DB_TEST` / `DB_ACCESS_SCOPE` | 적용 완료 |
| OCI IAM service client | `DDS_MCP_SERVICE_TEST`, client credentials만 허용 | 적용 완료 |
| Autonomous DB identity provider | `OCI_IAM`, database resource app ID와 OCI IAM domain URL 등록 | 적용 완료 |
| DB signing-key credential | `OCI_IAM_DOMAIN_DB_CRED$` | 적용 완료 |
| DB application identity | `DDS_MCP_SERVICE_TEST` → OCI IAM service client ID | 적용 완료 |
| HMM DDS MCP runtime | client ID·secret·scope를 권한 제한 환경 파일로 로드 | 적용 완료 |
| Alice data grant | `DDS_U_1_ROLE` → `CB_VECTOR_SEARCH_DOCUMENTS` SELECT | 적용 완료 |
| Bob data grant | 없음 | default deny |
database-access token은 `resource_app_id`, `tenant_iss`, scope가 DB identity provider 등록과 모두 일치해야 한다. OCI IAM domain URL은 token의 issuer와 같은 정규형(`:443` 포함)을 사용했다. 포트가 빠진 URL로 등록하면 Oracle이 `ORA-52602`(invalid database access token)으로 context 사용을 거부한다.
2026-08-04 검증 결과는 다음과 같다. 동일한 `dds_vector_search` 호출에서 Alice는 DDS context attach 후 query가 성공했고, Bob은 data grant가 없어 보호 객체 조회가 거부됐다. 초기에는 HMM knowledge chunk 데이터가 없어 Alice의 성공 응답 행 수가 `0`이었으나, 이후 비교 가능한 테스트 문서를 추가하여 Alice의 1건 반환까지 재검증했다.
이후 권한 차이를 눈으로 확인할 수 있도록 `DDS_AD_ALICE_LEAVE_001` 테스트 문서와 `DDS_AD_TEST` 태그를 HMM knowledge 원천에 추가했다. 같은 `휴가` 검색에서 Alice는 이 문서 1건을 반환하고 Bob은 `ORA-00942` 기반 default deny로 거부되는 것을 확인했다.
### AIPF 화면에서의 권한 차이 해석
동일한 AIPF Agent 요청을 Alice와 Bob으로 실행한다.
```text
dds_vector_search 도구를 사용해서 "휴가"를 검색해줘.
embeddingMode는 DEMO, limit은 10으로 해줘.
```
| 로그인 사용자 | AIPF에서 보이는 결과 | DB 측 실제 의미 | 판정 |
|---|---|---|---|
| `dds-alice` | `휴가 정책 - Alice 전용 테스트` 1건 반환 | Keycloak `sub` → `DDS_U_1`, `DDS_U_1_ROLE`의 SELECT DATA GRANT가 `CB_VECTOR_SEARCH_DOCUMENTS`에 적용 | 허용 |
| `dds-bob` | “검색에 실패했습니다”, “DDS 보호 객체를 조회할 수 없습니다” | Keycloak `sub` → `DDS_U_2`, 해당 role에 DATA GRANT가 없어 Oracle이 `ORA-00942`로 보호 객체를 숨김 | default deny / 정상 차단 |
따라서 Bob의 AIPF 화면 문구는 시스템 장애가 아니라 의도한 보안 결과다. 현재 MCP 응답은 SQL 오류를 외부에 노출하지 않기 위해 일반 문구로 변환한다. 운영 UI에서는 이를 “접근 권한이 없습니다”로 표시하도록 개선할 수 있지만, PoC의 권한 검증 자체는 Alice 허용·Bob 거부로 완료됐다.
## 5. 운영 보안 기준과 장애 구분
### secret과 token의 보관 원칙
| 값 | 소유자 | 허용 위치 | 금지 위치 |
|---|---|---|---|
| AD 사용자 비밀번호 | 사용자/AD | AD에서 hash로 관리, 사용자가 Keycloak login form에만 입력 | MCP 설정, AIPF secret, Git, 문서 |
| Keycloak AIPF client secret | AIPF OAuth client | 권한 제한 secret store 또는 `.runtime/aipf-dds-mcp-client.env` | Git, Redmine 본문, 채팅 |
| OCI IAM service client secret | MCP backend | HMM host의 mode 600 환경 파일/secret manager | AIPF browser, Java source, Git |
| Keycloak user access token | AIPF/MCP 요청 경로 | AIPF의 보호된 token 저장소, HTTPS request header | URL query, application log |
| OCI IAM DB service token | MCP process memory | token endpoint 응답 및 DB context attach | browser, client log, source code |
secret rotation 시에는 Keycloak 또는 OCI IAM에서 새 secret을 만들고, 해당 runtime secret store만 갱신한 뒤 서비스를 재시작한다. 이전 secret의 폐기는 새 token 발급과 MCP query를 확인한 후 수행한다. secret의 실제 값은 로그·shell history·스크린샷에도 남기지 않는다.
### default deny가 정상인 경우와 장애인 경우
| 관측 결과 | 가능한 원인 | 기대 처리/조치 |
|---|---|---|
| 이전 Alice로 자동 로그인 | Keycloak SSO cookie가 남아 있음 | logout 후 Bob으로 다시 인증 |
| `401` 또는 tools discovery 실패 | Bearer token 없음/만료, issuer·audience 검증 실패, AIPF source OAuth 설정 누락 | AIPF OAuth 설정과 Keycloak token claim 확인 |
| `invalid_request: Missing parameter: code_challenge_method` | Keycloak client에 PKCE 강제인데 AIPF가 PKCE를 보내지 않음 | 현재 AIPF client의 PKCE 정책을 호환 설정으로 조정; AIPF 지원 후 S256 전환 |
| `unauthorized_client` / invalid client credentials | AIPF에 잘못된 Keycloak client secret 저장 | 안전한 runtime 파일에서 secret을 다시 복사하고 source 재연결 |
| `DDS_CONTEXT_UNAVAILABLE` | OCI IAM client/scope/token endpoint 또는 DB external auth 구성 누락 | service client scope, DB application identity, domain issuer URL 점검 |
| Oracle `ORA-52602` | database-access token issuer/resource/scope와 DB 등록값 불일치 | Domain URL 정규형(이 환경은 `:443` 포함), resource app ID와 scope 점검 |
| Alice는 결과, Bob은 “DDS 보호 객체” 오류 | Bob에 DATA GRANT 없음 | 의도한 default deny. 정책상 필요한 경우에만 최소 grant 추가 |
| Alice도 Bob도 모두 결과 없음 | source data 부재, vector/embedding 조건, 공통 service token 문제 | 데이터 존재 여부와 DDS context attach를 분리해 점검 |
권한 거부와 인프라 장애를 구분하기 위해 MCP 내부 log에는 SQLState/Oracle error code를 남길 수 있으나, 외부 MCP 응답에는 table명·SQL·DB credential 정보를 노출하지 않는다. 감사 로그에는 요청 시각, correlation ID, token의 `iss + sub` hash 또는 내부 user ID, 선택된 DDS END USER, tool명, allow/deny 결과를 남긴다.
### 최소 권한 운영 원칙
1. `DDS_MCP_SERVICE_TEST`에는 database-access scope 외의 OCI 권한을 부여하지 않는다.
2. DB application identity는 MCP runtime 전용이며, 개인 사용자나 AIPF browser가 직접 사용하지 않는다.
3. local DDS END USER에는 필요한 DATA ROLE만 붙이고, 권한은 object/row/column 단위로 좁힌다. 새 사용자는 grant 없이 생성하는 default deny를 기본으로 한다.
4. `CB_EXTERNAL_IDENTITY_BINDING` 변경은 관리자만 수행하고 issuer와 immutable subject를 audit한다. UPN/email 변경만으로 기존 권한이 다른 계정에 이전되지 않아야 한다.
5. HTTPS는 Keycloak, AIPF, MCP 모두에서 강제하고, JWKS·token endpoint 호출의 TLS 검증을 끄지 않는다.
6. AD DS VM의 RDP와 LDAP 접근은 관리망/허용 IP로 제한한다. public LDAP/LDAPS를 인터넷에 열지 않는다.
7. 테스트 종료 후 Windows VM, public DNS, test clients와 secret의 보존·폐기 여부를 Redmine에 기록한다.
## 6. Entra ID로 전환할 때
Entra tenant가 확보되면 AD DS/bridge 테스트에서 확인한 `issuer + immutable subject -> CB_APP_USER -> DDS END USER` 계약은 유지한다. 바뀌는 부분은 token issuer와 JWKS 검증 설정뿐이다. Entra의 claim 이름(`oid`, `sub`, `preferred_username` 등)은 실제 발급 토큰을 확인한 뒤 결정하며, `sub` 단독이 아니라 tenant/issuer 경계를 반드시 포함한다.
## 7. 완료 기준
- [x] `dds.test` AD forest와 두 테스트 사용자가 생성되었다.
- [x] Keycloak LDAP bridge가 HTTPS OIDC JWT를 발급하고 각 사용자가 서로 다른 안정 subject를 가진다.
- [x] MCP가 JWT signature/issuer/audience를 검증하고 `issuer + sub` 매핑을 해석한다.
- [x] subject 매핑을 통해 각 요청에 대응하는 DDS END USER context만 attach된다.
- [x] 서로 다른 권한의 동일 MCP 호출에서 데이터 행/열 결과가 달라진다.
- [ ] 미매핑·만료·issuer/audience 불일치 요청은 데이터 접근 전에 거부된다.
- [ ] 종료 시 테스트 VM과 전용 네트워크 리소스의 정리 여부 및 비용 상태를 Redmine에 기록한다.
AD 사용자 비밀번호, OAuth client secret, OCI service client secret, access/refresh token은 문서·Git·대화에 적지 않는다. 위치, rotation 절차, 안전한 입력 방법만 [Cookbook](cookbook.md)에 기록한다.

View File

@@ -0,0 +1,722 @@
# 설계서: Windows AD 기반 DDS END USER 매핑 테스트 (#744)
> **상태**: Keycloak 기반 검증 완료 · OCI IAM AD Bridge + delegated authentication 전환 진행 중
> **최종수정**: 2026-08-05
> **추적성**: Redmine #744 · 선행 설계: [DDS MCP END USER Context](../617-dds-mcp-end-user-context/README.md)
[개요로 돌아가기](README.md) · [적용 Cookbook](cookbook.md) · [트러블슈팅](troubleshooting.md)
## 1. 목적과 범위
Microsoft Entra ID 테넌트가 아직 준비되지 않은 상황에서, OCI 격리 네트워크의 Windows Server AD DS를 이용해 업무 사용자 디렉터리와 DDS END USER 매핑을 먼저 검증한다.
이 환경은 **Microsoft Entra access token을 Oracle DB가 직접 검증하는 환경이 아니다.** AD DS는 사용자·그룹·UPN을 제공하고, 이후 Keycloak 또는 별도 검증 서비스가 AD LDAP을 기반으로 발급·검증한 토큰의 안정적인 subject를 MCP의 업무 사용자로 해석한다. Entra tenant와 app registration이 준비되면 OIDC issuer/audience/JWKS 검증 경로로 교체 또는 병행한다.
### 2026-08-05: OCI IAM AD Bridge 전환 목표
초기 PoC는 AD DS를 OIDC로 연결하기 위해 Keycloak을 사용했다. 이제 OCI Identity Domain의 **Microsoft Active Directory (AD) Bridge**와 **delegated authentication**을 사용해 Keycloak을 대체한다. AD DS는 계속 사용자·비밀번호의 원천이며, OCI Identity Domain이 AD Bridge를 통해 AD 비밀번호를 확인한 후 AIPF/MCP용 OIDC token을 발급한다.
```text
기존: AD DS → Keycloak → Keycloak user JWT → MCP → DDS
전환: AD DS → OCI AD Bridge + delegated authentication
→ OCI Identity Domain user JWT → MCP → DDS
```
이미 운영 중인 OCI database resource application `DDS_ORACLE_DB_TEST`와 DB service client `DDS_MCP_SERVICE_TEST`는 제거하거나 사용자 로그인 client로 재사용하지 않는다. 이들은 MCP가 Oracle DB DDS context를 여는 **machine-to-machine** 신뢰에 계속 사용한다. 사용자 로그인에는 별도의 OCI confidential OAuth application을 만든다.
#### 전환 원칙
1. Keycloak, 기존 mapping, 기존 AIPF source는 OCI 경로 검증 전까지 유지한다.
2. OCI AD Bridge가 AD 사용자·그룹을 동기화하고 delegated authentication으로 `dds-alice`/`dds-bob`의 **AD 비밀번호**를 확인하는 것을 먼저 검증한다.
3. AIPF 전용 OCI OAuth client를 별도로 만든다. callback URI와 refresh token 사용은 기존 AIPF 호환 설정을 유지한다.
4. MCP는 OCI Identity Domain의 discovery/JWKS, issuer, audience로 검증 대상을 바꾼다. Keycloak token과 OCI token을 동시에 허용하지 않는다.
5. OCI token의 검증된 `iss + sub`를 별도 binding으로 Alice=`DDS_U_1`, Bob=`DDS_U_2`에 등록한다.
6. OCI 경로에서 Alice 1건 반환과 Bob default deny를 확인한 뒤에만 Keycloak source와 DNS/VM 정리 여부를 결정한다.
#### 현재 전환 인벤토리
| 항목 | 현재 값/상태 | 전환 시 처리 |
|---|---|---|
| Windows AD DS | `dds.test`, `hmm-ad-dss-test-isolated`, 실행 중 | AD 사용자 원천으로 유지 |
| OCI Identity Domain | `identityAPAC`, 활성 | AD Bridge와 사용자 OAuth client를 이 Domain에 추가 |
| DB resource app | `DDS_ORACLE_DB_TEST` | 유지 |
| DB service client | `DDS_MCP_SERVICE_TEST` | 유지 |
| 사용자 OIDC issuer | Keycloak `https://ad.cloud-handson.com/realms/dds-test` | OCI Domain issuer로 교체 |
| AIPF OAuth client | Keycloak `aipf-dds-mcp` | OCI Domain의 새 confidential client로 교체 |
| MCP public endpoint | `https://hmm-backoffice.cloud-handson.com/mcp/dds/sse` | 유지 |
| DDS local users/data grants | `DDS_U_1` 허용, `DDS_U_2` grant 없음 | 유지 |
#### OCI AD Bridge 구축 절차
1. `identityAPAC`의 **Directory integrations**에서 Microsoft AD Bridge를 생성한다.
2. OCI가 표시하는 Domain URL, Bridge client ID/secret을 기록하고 Bridge client installer를 내려받는다. 이 client secret은 AIPF OAuth client secret과 다르며, Windows Bridge client 설정에만 사용한다.
3. AD domain에 join된 Windows VM에 Bridge client를 설치한다. 운영은 Domain Controller와 별도 member server를 권장하지만, 이 격리 PoC는 기존 AD VM에서 설치 가능 여부를 우선 검증한다.
4. Bridge service account에는 동기화 대상 OU/그룹에 대한 read 권한과 `cn=Deleted Objects` 읽기 권한을 준다. delegated authentication도 사용할 경우 Oracle이 요구하는 password/lockout attribute 권한을 최소 범위로 부여한다.
5. LDAPS(636)를 사용하도록 구성하고, Bridge VM에서 OCI Domain HTTPS 443 및 AD LDAP/LDAPS 연결을 확인한다.
6. `dds-alice`, `dds-bob`이 포함된 OU와 필요한 AD 그룹만 동기화 대상으로 선택한다.
7. OCI console의 **Delegated authentication**에서 Bridge를 시험하고 활성화한다. 성공하면 OCI 로그인 화면의 password 검증은 AD에서 수행한다.
8. AIPF용 OCI confidential OAuth application을 생성하고, AIPF callback URI, authorization-code/refresh-token grant, `openid` scope를 설정한다.
9. OCI discovery document의 issuer/JWKS와 token의 `aud`, `sub`를 확인한 뒤 MCP runtime과 `CB_EXTERNAL_IDENTITY_BINDING`을 교체한다.
#### 전체 구성도: AD 계정 로그인부터 DDS 권한 적용까지
아래 구성에서 AD에 새로 붙는 구성요소는 **OCI IAM AD Bridge Client**다. 이는 AD Domain Controller 내부의 AD DS를 대체하거나 AD 비밀번호를 복제하는 서버가 아니다. Domain-joined Windows에 설치되는 Windows service로서, AD에는 LDAP/LDAPS로 연결하고 OCI Identity Domain에는 HTTPS 443으로 연결한다.
```mermaid
flowchart LR
subgraph USER[사용자·클라이언트]
U["업무 사용자<br/>dds-alice / dds-bob"]
AIPF["AIPF<br/>MCP OAuth client"]
end
subgraph ADNET[OCI 격리 네트워크 · dds.test]
AD["Windows Server 2022<br/>AD DS Domain Controller<br/>계정·비밀번호·그룹 원천"]
BRIDGE["OCI IAM AD Bridge Client<br/>Windows service<br/>사용자/그룹 동기화 + 인증 위임"]
AD -->|"LDAP 또는 LDAPS<br/>사용자·OU·그룹 읽기"| BRIDGE
BRIDGE -->|"delegated authentication<br/>AD 비밀번호 확인 요청"| AD
end
subgraph OCIID[OCI Identity Domain · identityAPAC]
DOMAIN["OCI Identity Domain<br/>사용자 디렉터리·SSO·OIDC issuer"]
OAUTH["AIPF_DDS_AD_TEST<br/>confidential OAuth client<br/>authorization_code + refresh_token"]
DBAPP["DDS_ORACLE_DB_TEST<br/>database resource / integrated app<br/>DB_ACCESS_SCOPE"]
SVC["DDS_MCP_SERVICE_TEST<br/>confidential service client<br/>client_credentials only"]
DOMAIN --- OAUTH
DOMAIN --- DBAPP
DOMAIN --- SVC
end
BRIDGE -->|"HTTPS 443<br/>AD 사용자·그룹 동기화"| DOMAIN
U -->|"1. AIPF에서 MCP 연결"| AIPF
AIPF -->|"2. OAuth authorization code"| OAUTH
OAUTH -->|"3. AD 비밀번호 검증 위임"| BRIDGE
OAUTH -->|"4. OCI user access token<br/>iss + aud + sub"| AIPF
subgraph MCPHOST[HMM Backoffice VM]
MCP["DDS MCP Server<br/>hmm-backoffice.cloud-handson.com<br/>/mcp/dds/sse"]
JWT["JWT 검증<br/>OCI discovery / JWKS<br/>issuer + audience + exp"]
MAP["Identity binding<br/>OCI iss + sub<br/>→ CB_APP_USER → DDS_U_n"]
DBTOKEN["DB service token 획득<br/>client credentials"]
MCP --> JWT --> MAP
MCP --> DBTOKEN
end
AIPF -->|"5. Authorization: Bearer<br/>OCI user access token"| MCP
MCP -->|"6. discovery/JWKS HTTPS"| DOMAIN
DBTOKEN -->|"7. client ID + secret<br/>DB_ACCESS_SCOPE"| SVC
SVC -->|"8. database-access token"| DBTOKEN
subgraph ADB[Oracle Autonomous Database · HMMAIPOC]
APPID["DB application identity<br/>DDS_MCP_SERVICE_TEST 신뢰"]
CTX["ORA_END_USER_CONTEXT<br/>DDS_U_1 또는 DDS_U_2 attach"]
DDS["Oracle Deep Sec<br/>DATA ROLE / DATA GRANT<br/>default deny"]
DATA["CB_VECTOR_SEARCH_DOCUMENTS<br/>보호 데이터"]
APPID --> CTX --> DDS --> DATA
end
DBTOKEN -->|"9. database-access token"| APPID
MAP -->|"10. 선택된 local END USER"| CTX
DATA -->|"11. 허용된 행만"| MCP
MCP -->|"12. tool result"| AIPF
```
| 번호 | 구간 | 전달되는 것 | 보안 의미 |
|---:|---|---|---|
| 1~4 | 사용자 → AIPF → OCI Domain → AD Bridge → AD | AD 사용자명·비밀번호, OAuth code, OCI user token | AD는 비밀번호 원천으로 남고 OCI Domain이 OIDC issuer 역할 수행 |
| 5~6 | AIPF → MCP → OCI Domain | Bearer user JWT, JWKS/discovery | MCP가 서명·issuer·audience·만료를 검증해 사용자 위조 차단 |
| 7~9 | MCP → OCI Domain → DB | service client credentials, DB 전용 access token | MCP process만 DB context를 열 수 있음; user JWT를 DB service token으로 쓰지 않음 |
| 10~12 | MCP → DB DDS → AIPF | local DDS END USER, 필터된 query 결과 | Alice/Bob 권한은 DB DATA GRANT에서 최종 강제 |
#### AD Bridge가 하는 일과 하지 않는 일
| AD Bridge가 하는 일 | AD Bridge가 하지 않는 일 |
|---|---|
| AD OU의 사용자·그룹 변경을 OCI Domain으로 동기화 | AD Domain Controller 또는 LDAP 서버를 대체하지 않음 |
| OCI Domain의 delegated authentication 요청을 AD에 전달해 AD 비밀번호를 검증 | AD 사용자 비밀번호를 MCP나 AIPF에 전달하지 않음 |
| AD 계정 비활성화·그룹 변경을 OCI user 상태/그룹과 동기화 | AIPF OAuth client secret 또는 DB service client secret을 보관하지 않음 |
| OCI Domain과 AD 사이의 제한된 연결을 담당 | Oracle DB의 DDS data grant를 평가하거나 우회하지 않음 |
Bridge 설치 위치는 운영에서는 AD Domain Controller와 분리한 domain-joined Windows member server가 원칙이다. 이 PoC는 격리된 단일 Windows VM이므로 동일 VM 설치 가능성을 확인하되, 결과 문서에는 이 제약을 남긴다. Bridge VM에는 OCI Domain으로의 outbound HTTPS 443과 AD DS로의 LDAP/LDAPS 389/636만 허용한다.
#### 적용 가이드: OCI AD Bridge와 AIPF OAuth client
이 절은 다음 환경에서 재적용할 때 사용하는 runbook이다. 명령의 secret 값은 출력하거나 Git에 넣지 않는다.
##### 1. 사전 점검
| 점검 | 기대값 | 실패 시 조치 |
|---|---|---|
| OCI Identity Domain | `identityAPAC` 등 대상 Domain이 `ACTIVE` | Domain 관리자 권한 및 Domain type/AD Bridge 허용량 확인 |
| AD DS | `dds.test` Domain Controller와 DNS·LDAP 정상 | Bridge를 Domain-joined Windows에 설치할 수 있는지 확인 |
| 네트워크 | Bridge VM → OCI Domain TCP/443, Bridge VM → AD TCP/636(LDAPS 권장) | NSG, Windows Firewall, proxy, AD 인증서 점검 |
| AD service account | 동기화 OU 읽기 및 delegated authentication에 필요한 최소 attribute 권한 | Domain Admin을 상시 사용하지 말고 전용 service account 생성 |
| AIPF callback | `https://aipf.cloud-handson.com/agentFactory/v1/tools/mcp/callback``/v1/tools/mcp/callback` | 두 URL 모두 OCI OAuth client redirect URI에 등록 |
현재 PoC의 Windows VM은 `hmm-ad-dss-test-isolated`이며 실행 중이다. OCI에서 확인한 public WinRM HTTPS(5986)는 `Connection refused` 상태다. 따라서 Bridge client 설치는 현재 RDP로 Windows에 접속해 수행하거나, 운영 자동화를 원하면 WinRM HTTPS와 Windows Firewall을 별도 구성해야 한다. 이 WinRM 상태는 AD Bridge 자체의 요구사항이 아니라 원격 설치 방식의 제약이다.
##### 2. OCI AD Bridge 생성과 Windows 설치
1. OCI Console에서 **Identity & Security → Domains → 대상 Domain → Directory integrations → Add → Microsoft Active Directory Bridge**로 이동한다.
2. 화면에서 표시되는 **Identity Domain URL, Bridge client ID, Bridge client secret**을 안전한 password manager/secret store에 보관한다. 이것은 AIPF OAuth client secret 및 DB service client secret과 다른 값이다.
3. Bridge client installer를 내려받아 domain-joined Windows에 설치한다.
4. 설치 UI에서 위 OCI Domain URL/client ID/client secret 및 AD Bridge service account를 입력하고 연결을 시험한다.
5. AD 연결은 **LDAPS 사용**을 선택한다. AD DS 인증서가 Bridge Windows trust store에서 신뢰되지 않으면 먼저 CA/서버 인증서를 배포한다. 설치 후 SSL 선택은 변경이 어렵다.
6. Directory integrations 화면에서 동기화할 사용자 OU, 그룹 OU를 최소 범위로 선택한다. `dds-alice`, `dds-bob`과 필요한 역할 그룹이 포함돼야 한다.
7. Bridge initial sync 후 OCI Domain Users/Groups에 두 사용자가 나타나는지 확인한다.
8. **Security → Delegated authentication**에서 `Test Delegated Authentication`으로 AD 사용자명과 AD 비밀번호를 시험하고 성공한 뒤에만 활성화한다.
`dds-alice`/`dds-bob` 로그인은 계속 AD 계정으로 한다. OCI Domain에 동기화된 user 레코드는 SSO/OIDC subject를 위한 cloud-side representation이고, delegated authentication 활성화 뒤의 비밀번호 검증 원천은 AD DS다.
##### 3. AIPF용 OCI OAuth client 생성
OCI Domain에는 DB service client와 별도로 AIPF용 confidential client가 필요하다. 이 PoC에서 만든 application 이름은 `AIPF_DDS_AD_TEST`이며 다음 정책을 사용한다.
| 항목 | 적용값 |
|---|---|
| Application type | Confidential OAuth client (`CustomWebAppTemplateId`) |
| Grants | `authorization_code`, `refresh_token` |
| Refresh token | 허용 (`allowOffline=true`) |
| Redirect URI | AIPF callback 2개 경로 |
| Access token expiry | 3600초 |
| Refresh token expiry | 1209600초(14일) |
| Consent | PoC에서 bypass; 운영 전 사용자 동의/정책 검토 |
OCI CLI로 생성할 경우 Identity Domain App API의 필수 `basedOnTemplate`을 반드시 포함한다.
```json
{
"schemas": ["urn:ietf:params:scim:schemas:oracle:idcs:App"],
"displayName": "AIPF_DDS_AD_TEST",
"basedOnTemplate": {"value": "CustomWebAppTemplateId"},
"active": true,
"isOAuthClient": true,
"clientType": "confidential",
"allowedGrants": ["authorization_code", "refresh_token"],
"redirectUris": [
"https://aipf.cloud-handson.com/agentFactory/v1/tools/mcp/callback",
"https://aipf.cloud-handson.com/v1/tools/mcp/callback"
],
"allowOffline": true
}
```
생성된 app ID/client ID/client secret은 local 검증 환경에서는 git-ignore된 `.runtime/aipf-dds-oci-ad-client.env`에만 저장한다. 파일에는 `OCI_AIPF_DDS_APP_ID`, `OCI_AIPF_DDS_CLIENT_ID`, `OCI_AIPF_DDS_CLIENT_SECRET`만 두며 값은 문서·Redmine·채팅·shell 출력에 기록하지 않는다.
##### 4. AIPF에 입력할 OCI OAuth 값
OCI OpenID discovery 문서에서 endpoint를 확인한다. URL을 임의로 조합하지 말고 아래 discovery endpoint의 실제 응답을 기준으로 한다.
```text
https://<identity-domain>/.well-known/openid-configuration
```
현재 `identityAPAC`에서 확인한 형식은 다음과 같다.
| AIPF 입력 항목 | 값 형식 |
|---|---|
| Server URL | `https://hmm-backoffice.cloud-handson.com/mcp/dds/sse` |
| Authentication mode | `OAuth` |
| OAuth client ID/secret | `AIPF_DDS_AD_TEST` 생성 결과의 client ID/secret |
| Authorization URL | `https://idcs-<domain>.identity.oraclecloud.com:443/oauth2/v1/authorize` |
| Token / Refresh URL | `https://idcs-<domain>.identity.oraclecloud.com:443/oauth2/v1/token` |
| Scopes | `openid profile email offline_access` (실제 app policy에 맞춰 조정) |
##### 5. MCP 및 DB 매핑 전환
Bridge와 AIPF OAuth login을 검증한 뒤, OCI access token을 decode하여 `iss`, `aud`, immutable `sub`를 기록한다. 그 다음에만 다음 값을 교체한다.
| 대상 | 변경 |
|---|---|
| HMM MCP `DDS_MCP_OIDC_ISSUER_URI` | OCI discovery가 반환한 issuer (현재 확인값 `https://identity.oraclecloud.com/`) |
| HMM MCP `DDS_MCP_OIDC_AUDIENCE` | OCI user access token의 실제 `aud` 값 |
| `CB_EXTERNAL_IDENTITY_BINDING` | OCI `iss + sub` → Alice/Bob application user 등록 |
| AIPF MCP source | OCI OAuth client ID/secret/endpoint로 신규 source 생성 |
OCI token을 실제로 받은 뒤 audience를 정하는 이유는 client ID, resource scope, Domain 정책에 따라 access token claim이 달라질 수 있기 때문이다. 추측값으로 MCP를 먼저 재시작하면 기존 Keycloak 검증을 불필요하게 중단시킬 수 있다.
#### 적용 중 확인된 트러블슈팅 기록
| 증상/오류 | 원인 | 해결/판정 |
|---|---|---|
| `401 Authorization Required`, endpoint에 `/admin/v1/admin/v1/Apps` | `oci identity-domains ... --endpoint``/admin/v1`까지 넣어 CLI가 경로를 중복 | CLI endpoint에는 Domain base URL만 넣는다. raw request일 때만 target URI에 `/admin/v1/...`를 넣는다. |
| `No such option: --header` | `oci raw-request``--header`가 아니라 `--request-headers` 사용 | `--request-headers '{"Content-Type":"application/json"}'`로 호출 |
| `Missing required attribute(s): basedOnTemplate.` | OCI App API는 OAuth client 생성 시 template 필수 | `"basedOnTemplate":{"value":"CustomWebAppTemplateId"}` 추가 |
| authorization endpoint가 `302`가 아니라 `HTTP 200 OK` | OCI Domain이 redirect 대신 로그인 HTML과 secure session cookie를 반환 | 정상. browser에서 OCI 로그인 화면이 보이면 OAuth preflight 통과로 판정 |
| OCI console browser automation 불가 | 현재 작업 환경에 browser binding 없음 | OCI Console 화면 조작이 필요한 Bridge 생성은 RDP/관리자 Console session으로 수행; API로 가능한 OAuth app/MCP 설정은 자동화 계속 |
| Windows VM WinRM 5986 `Connection refused` | WinRM HTTPS listener/Windows Firewall/NSG 미구성 | RDP 설치를 사용하거나 WinRM HTTPS를 별도 구성. Bridge 기능의 오류로 해석하지 않음 |
| OCI Compute Instance Run Command plugin은 `RUNNING`인데 명령이 계속 `ACCEPTED` / `VISIBLE` | plugin 상태 보고는 가능하지만 Windows agent가 command execution을 수신·실행하지 못함. PowerShell preflight와 단순 `echo` 모두 동일 | Bridge installer 실행 수단으로 사용하지 않는다. Windows의 Oracle Cloud Agent/Run Command service, outbound OCI connectivity, plugin 로그를 RDP에서 점검한 뒤 재시도한다. |
| AD Bridge test가 LDAPS certificate 오류 | AD DS 인증서 체인이 Bridge host trust store에 없음 | 사내 CA/AD 인증서를 Bridge host에 신뢰시키고 LDAPS를 유지 |
| OCI OAuth login은 성공하지만 MCP `401` | MCP issuer/audience가 Keycloak 값 그대로이거나 OCI `sub` binding 없음 | OCI JWT claim 확인 후 MCP env와 `CB_EXTERNAL_IDENTITY_BINDING`을 함께 교체 |
| AIPF source에서 이전 사용자로 로그인 | OCI Domain browser SSO session 유지 | OCI Domain logout 후 source 재연결; AIPF source 저장 token과 browser SSO session을 구분 |
#### 전환 검증 기준
| 단계 | 증거 | 성공 기준 |
|---|---|---|
| AD Bridge 연결 | Bridge 상태 `Connected`, AD 사용자/그룹 동기화 결과 | `dds-alice`, `dds-bob`이 OCI Domain에 존재 |
| delegated authentication | OCI Domain의 test delegated authentication | 두 사용자의 AD 비밀번호로 성공 |
| OAuth code flow | AIPF가 OCI login → callback → MCP source 연결 완료 | OCI issuer의 access token이 저장됨 |
| MCP 검증 | OCI JWT의 signature/issuer/audience 통과 | `iss + sub` mapping 조회 성공 |
| DDS 권한 | 같은 `휴가` tool 호출 | Alice 1건, Bob default deny |
| 회귀/보안 | Keycloak token, 미매핑 OCI token, 만료 token | DB query 전 거부 |
### 이 설계가 답하는 질문
이 PoC는 다음 네 가지를 의도적으로 분리한다.
| 질문 | 답 | 담당 구성요소 |
|---|---|---|
| 직원 계정과 비밀번호는 어디에 있는가? | Windows AD DS가 원천이다. | AD DS |
| 사용자가 MCP에 로그인했음을 어떻게 증명하는가? | Keycloak이 AD LDAP 인증을 거쳐 OIDC access token(JWT)을 발급한다. | Keycloak |
| MCP 서버가 Oracle DB에 접속해 DDS context를 열 자격은 어떻게 얻는가? | OCI Identity Domain의 confidential service client가 database-access token을 발급받는다. | OCI IAM integrated application / credential app |
| Alice와 Bob이 서로 다른 데이터만 보게 하는 최종 판단은 누가 하는가? | Oracle Deep Sec이 요청별 local DDS END USER와 DATA GRANT를 적용한다. | Oracle Database |
즉, **AD 계정으로 로그인한 사용자 토큰**과 **MCP 서버가 DB에 접속하는 서비스 토큰**은 목적과 발급자가 다른 별개의 token이다. 전자는 “누가 요청했는가”를, 후자는 “어떤 애플리케이션이 DB context를 열 수 있는가”를 증명한다. 하나를 다른 하나의 대체물로 쓰지 않는다.
## 2. 격리 인프라
| 항목 | 구성 |
|---|---|
| 네트워크 | `handson-vcn`의 전용 `10.0.2.0/28` public subnet |
| Windows VM | `hmm-ad-dss-test-isolated`, Windows Server 2022, 2 OCPU / 16 GB |
| 관리 접근 | RDP TCP/3389은 작업자 공인 IP 한 곳에만 NSG로 허용 |
| OIDC HTTPS | `https://ad.cloud-handson.com` (Caddy TLS reverse proxy → Keycloak) |
| 서브넷 보안 목록 | 인바운드 전부 차단, Windows Update 및 테스트용 아웃바운드만 허용 |
| 도메인 | `dds.test` (AD DS forest/domain) |
이 VM은 테스트 전용이다. 기존 공유 public subnet의 NAT 경로를 변경하지 않으며, 비용 발생 리소스이므로 검증 종료 뒤 중지 또는 삭제를 결정한다.
## 3. 식별자와 권한 흐름
```text
AD 사용자 (UPN: alice@dds.test, objectGUID)
│ LDAP / OIDC bridge
검증된 Bearer claims
iss, aud, exp, sub = 불변 외부 subject
│ MCP bearer authenticator
CB_EXTERNAL_IDENTITY_BINDING
issuer + subject -> CB_APP_USER.user_id
CB_DDS_END_USER_MAP
user_id -> DDS_U_<user_id> local END USER
ORA_END_USER_CONTEXT + DDS DATA GRANT / 권한 함수
```
`sub`는 Keycloak이 발급하는 안정적인 federated-user 식별자다. AD objectGUID는 원천 디렉터리의 감사 식별자로 유지하되, 실제 권한 키는 **검증된 JWT의 `iss + sub`**로 고정한다. UPN은 로그인 화면·감사 표시용으로 보관할 수 있지만 권한 키로 신뢰하지 않는다. `iss + sub` 조합이 유일하지 않거나, 매핑이 없거나, 사용자가 비활성이면 요청은 fail-closed로 거부한다.
### 전체 요청 흐름
```text
(1) 사용자가 AIPF에서 dds MCP를 연결
│ OAuth authorization code flow
(2) Keycloak 로그인 화면
│ AD LDAP bind: dds-alice / dds-bob의 비밀번호 확인
(3) Keycloak access token 발급
│ iss, aud=dds-mcp, exp, sub 포함 / Keycloak signing key로 서명
(4) AIPF가 MCP 요청에 Bearer access token을 첨부
(5) MCP
│ Keycloak JWKS로 서명·issuer·audience·만료 검증
│ CB_EXTERNAL_IDENTITY_BINDING에서 iss+sub 조회
(6) MCP가 OCI IAM service token을 별도로 획득
│ client credentials: DDS_MCP_SERVICE_TEST
(7) Oracle DB
│ service token으로 trusted application identity 확인
│ 조회한 DDS_U_n end-user context attach
(8) Deep Sec
│ DDS_U_n의 DATA ROLE/DATA GRANT만 적용하여 SQL 실행
(9) MCP가 허용된 결과만 AIPF로 반환
```
MCP는 이 흐름의 **정책 집행점**이다. 사용자의 JWT를 검증하지 못하면 5단계에서 멈추고, DB service token이 유효하지 않으면 7단계에서 멈추며, 둘 다 통과해도 해당 DDS END USER의 grant가 없으면 8단계에서 멈춘다. 어느 단계에서도 실패를 우회하여 공유 DB 계정의 광범위한 권한으로 조회해서는 안 된다.
### 사용자 인증과 DB 접속 신뢰의 분리
이 PoC에는 서로 다른 두 신뢰 체인이 있다. 둘을 같은 OAuth token으로 혼동하지 않는다.
```text
[업무 사용자 인증]
AD user -> Keycloak (AD LDAP 인증) -> Keycloak JWT
-> MCP validates iss + sub -> DDS_U_1 / DDS_U_2 선택
[DB 접속 신뢰]
MCP service -> OCI IAM DDS_MCP_SERVICE_TEST (client credentials)
-> database-access token -> Oracle DB
-> 선택된 local DDS END USER context attach
```
| 역할 | 현재 담당 | 하는 일 |
|---|---|---|
| AD 사용자 인증 | Keycloak | AD LDAP의 계정·비밀번호를 확인하고 Keycloak JWT를 발급 |
| 업무 사용자 식별 | MCP | 검증된 Keycloak `iss + sub`를 HMM application user와 local DDS END USER에 매핑 |
| DB 접속 애플리케이션 신뢰 | OCI IAM credential app | MCP service가 database-access token을 받아 Oracle DB에 신뢰된 application임을 증명 |
| 최종 데이터 권한 | Oracle Deep Sec | attach된 local DDS END USER의 DATA ROLE과 DATA GRANT로 행·열을 제한 |
따라서 OCI IAM credential app은 현재 AD 사용자를 직접 로그인시키지 않는다. OCI IAM은 MCP service의 database-access token 발급자이며, Oracle DB가 해당 service client를 application identity로 신뢰하게 한다. AD 사용자가 어떤 DDS END USER가 되는지는 MCP가 결정한다.
향후 OCI IAM을 사용자 인증의 중심으로 바꾸려면 Keycloak을 OCI IAM의 외부 IdP(OIDC 또는 SAML)로 federation하고, AIPF/MCP가 OCI IAM user token을 받도록 변경한다. 그 경우 Oracle DB도 OCI IAM user token의 issuer·group claim을 직접 검증할 수 있다. 현재 PoC의 local DDS END USER 매핑과는 별도의 확장 경로다.
### OCI Identity Domain integrated application을 쓰는 이유와 역할
여기서 말하는 OCI Domain integrated application은 `DDS_ORACLE_DB_TEST` database resource application이다. 이를 사용자용 웹 로그인 앱으로 오해하면 안 된다. 이 앱은 **Oracle Autonomous Database가 신뢰할 OAuth audience와 scope를 OCI Identity Domain에 선언하는 등록물**이다.
일반 OAuth access token은 “어느 resource server를 위한 token인지”가 불명확하면 DB가 받아들여서는 안 된다. integrated application은 DB용 resource identity를 만들고, `DB_ACCESS_SCOPE` 같은 전용 scope를 발급 정책에 묶는다. 그러면 DB는 다음 세 가지가 일치할 때만 MCP의 service token을 받아들인다.
| DB가 확인하는 값 | integrated application에서 정하는 값 | 이 PoC의 의미 |
|---|---|---|
| resource application | `DDS_ORACLE_DB_TEST` | 이 token의 수신자가 DDS 대상 Oracle DB임을 식별 |
| scope | `DB_ACCESS_SCOPE` | DB context 접근이라는 최소 권한을 명시 |
| issuer | OCI Identity Domain URL | token을 발급하고 서명키를 제공하는 신뢰 경계 |
| OAuth client ID | `DDS_MCP_SERVICE_TEST` | token을 요청한 MCP service를 식별 |
`DDS_MCP_SERVICE_TEST`는 integrated application 자체가 아니라, 그 resource/scope를 요청할 수 있도록 허가된 **confidential service client**다. client secret을 가진 백엔드 MCP만 client credentials flow로 token을 발급받는다. 브라우저·AIPF·AD 사용자는 이 secret이나 service token을 보거나 보관하지 않는다.
```text
OCI Identity Domain
DDS_ORACLE_DB_TEST DDS_MCP_SERVICE_TEST
(resource / integrated app) (confidential OAuth client)
┌───────────────────────┐ ┌─────────────────────────┐
│ audience: Oracle DB │<--scope--│ client credentials only │
│ scope: DB_ACCESS_SCOPE│ │ secret: MCP host only │
└──────────┬────────────┘ └───────────┬─────────────┘
│ database-access token │ token request
└────────────────────────────────────┘
Autonomous DB application identity
```
이 구조를 쓰는 이유는 DB password를 MCP에 장기 보관하거나, 모든 사용자에게 DB 로그인 권한을 주지 않기 위해서다. service client는 DB context를 여는 최소 권한만 보유하고, 사람별 데이터 권한은 local DDS END USER에 남긴다. 서비스 client secret이 노출되더라도 해당 client에 부여하지 않은 데이터 권한이 자동으로 생기는 구조가 아니다. 다만 token 발급과 DB context 생성 자체를 악용할 수 있으므로 secret은 즉시 회전하고, 이 client에는 불필요한 OCI 권한을 부여하지 않는다.
### OCI IAM과 Keycloak을 함께 쓰는 이유
현재 AD DS는 LDAP/Kerberos 디렉터리이지 인터넷 서비스가 직접 검증할 OAuth JWT issuer는 아니다. Keycloak은 AD LDAP과 OIDC 사이의 번역 계층이다. 반면 OCI IAM integrated application은 Oracle DB가 지원하는 database-access token contract를 제공한다.
| 경계 | 입력 | 출력 | 선택 이유 |
|---|---|---|---|
| AD DS → Keycloak | AD 사용자명/비밀번호, LDAP 사용자·그룹 | Keycloak OIDC JWT | AD를 계정 원천으로 유지하면서 표준 OAuth/OIDC를 제공 |
| Keycloak → MCP | Keycloak JWT/JWKS | 검증된 `iss + sub` | MCP가 사용자 identity를 안전하게 해석 |
| MCP → OCI IAM | service client ID/secret | DB 전용 access token | DB가 신뢰할 service identity 증명 |
| OCI IAM → Oracle DB | database-access token | application identity | DB password 없이 DB context 접근을 제한 |
| Oracle DB → DDS | local END USER | 필터된 query 결과 | 데이터 권한을 DB 내부에서 강제 |
따라서 “OCI Domain credential app이 AD를 신뢰한다”는 표현은 현재 PoC에는 정확하지 않다. 현재는 **Keycloak이 AD를 신뢰해 사용자 인증을 수행**하고, **Oracle DB가 OCI IAM을 신뢰해 MCP service를 인증**한다. 두 체인은 MCP 안에서 만나며, MCP가 Keycloak token의 subject를 local DDS END USER로 결정한다.
## 4. 구현 단계
1. Windows Server에 AD DS를 설치하고 `dds.test` forest를 생성한다.
2. 테스트 사용자 `dds-alice`, `dds-bob`와 권한 그룹을 만들고, LDAP 조회와 인증을 확인한다.
3. AD LDAP과 연동한 OIDC bridge(예: Keycloak)를 별도 서비스로 구성한다. bridge가 내는 JWT의 `iss`, `aud`, `exp`, JWKS 서명을 MCP가 검증한다.
4. `issuer + sub`에서 `CB_APP_USER``CB_DDS_END_USER_MAP`을 해석하도록 백오피스/DB 매핑을 추가한다.
5. alice/bob의 서로 다른 `DDS_U_*` context에서 동일 MCP tool 호출 결과가 권한에 따라 달라지는지 검증한다.
6. 매핑 누락, 만료 토큰, 다른 issuer/audience, 비활성 계정은 모두 거부되는 regression을 추가한다.
## 4.1 현재 테스트 bridge
`database/adb/47_dds_ad_identity_test_setup.sql`은 HMM DB의 기존 업무 사용자 원천을 변경하지 않는다. 스크립트가 만든 `CB_EXTERNAL_IDENTITY_BINDING`에는 최종 OIDC issuer와 Keycloak이 실제로 발급한 subject만 보관한다.
| AD 사용자 | OIDC issuer | JWT `sub` | HMM application user | DDS END USER |
|---|---|---|---:|---|
| `dds-alice` | `https://ad.cloud-handson.com/realms/dds-test` | `11cf09c9-4713-4145-85cd-e3397d4a2405` | 1 | `DDS_U_1` |
| `dds-bob` | `https://ad.cloud-handson.com/realms/dds-test` | `d9610e18-a327-4dda-9496-69e5a2067460` | 2 | `DDS_U_2` |
`dds-mcp` client는 access token audience에 `dds-mcp`를 포함한다. MCP는 Keycloak JWKS에서 JWT 서명과 `iss`, `aud`, 만료 시간을 검증한 뒤에만 위 매핑을 조회한다. 실제 `/dds/mcp/messages` 초기화 요청에 Alice JWT를 넣어 성공 응답을 확인했다.
초기 게시 단계에서는 END USER와 DATA ROLE만 만들고 default deny로 시작한다. 현재 테스트에서는 Alice에만 vector view SELECT data grant를 추가했고 Bob은 grant 없이 유지한다.
### Keycloak 구성에서 중요한 값
Keycloak realm `dds-test`은 AD LDAP user federation을 사용한다. 로그인 화면에서 입력한 `dds-alice` 또는 `dds-bob`의 비밀번호 검증은 AD DS가 담당하며, Keycloak은 성공한 LDAP 사용자의 immutable identity를 자신의 `sub`로 유지해 JWT에 넣는다. AD 비밀번호나 LDAP bind credential은 MCP와 AIPF에 전달되지 않는다.
| Keycloak 항목 | 값/정책 | 필요한 이유 |
|---|---|---|
| Realm issuer | `https://ad.cloud-handson.com/realms/dds-test` | MCP의 허용 issuer와 identity binding의 namespace |
| MCP audience | `dds-mcp` | 다른 Keycloak client용 token의 재사용 방지 |
| AIPF confidential client | `aipf-dds-mcp` | AIPF OAuth callback과 token exchange 수행 |
| MCP resource client | `dds-mcp` | access token audience 검증 대상 |
| Redirect URI | AIPF callback 두 경로와 wildcard 등록 | authorization code를 허용된 AIPF에만 반환 |
| PKCE | AIPF client에서 미사용 | 현재 AIPF가 `code_challenge_method`를 보내지 않기 때문; 향후 AIPF 지원 시 S256으로 전환 |
| Direct grant | 테스트 client에서만 제한적으로 사용 | CLI 검증용; 운영 browser client에는 사용하지 않음 |
JWT 검증은 서명만 확인하는 것으로 충분하지 않다. MCP는 최소한 다음을 모두 확인해야 한다.
1. `alg`가 허용된 비대칭 서명 알고리즘인지 확인하고, Keycloak JWKS의 해당 `kid`로 signature를 검증한다.
2. `iss`가 realm issuer와 정확히 같은지 확인한다. URL의 realm·host가 다른 token은 거부한다.
3. `aud``dds-mcp`가 포함됐는지 확인한다.
4. `exp`, `nbf`, `iat` 및 허용 clock skew를 검사한다.
5. 검증된 `iss + sub`로만 `CB_EXTERNAL_IDENTITY_BINDING`을 조회한다. browser가 표시한 email, username, group 문자열만으로 매핑하지 않는다.
Keycloak signing key rotation 시 MCP의 JWKS cache가 새 `kid`를 다시 조회할 수 있어야 한다. issuer나 realm을 바꾸면 binding table의 issuer 값과 MCP allow-list도 함께 변경해야 한다.
### 테스트 Keycloak 사용자 비밀번호를 얻는 방법
**실제 로그인 인증 원천은 Windows AD DS다.** `dds-alice``dds-bob`은 Keycloak에 로컬로 만든 계정이 아니라 AD DS의 테스트 사용자다. AIPF가 Keycloak 로그인 화면으로 이동하면 사용자는 AD 사용자명과 AD 비밀번호를 입력하고, Keycloak은 LDAP을 통해 AD에 비밀번호를 확인한다. AD 인증이 성공할 때에만 Keycloak이 OIDC JWT를 발급한다.
```text
사용자: dds-alice + AD 비밀번호 입력
Keycloak ── LDAP 인증 요청 ──► Windows AD DS
│ │
└── 인증 성공 ◄─────┘
Keycloak OIDC access token 발급
```
`.runtime/dds-ad-test-users.env`는 인증 서버나 Keycloak 설정이 아니다. 테스트 중 AD 비밀번호를 안전하게 다시 입력할 수 있도록 **현재 AD에 설정된 테스트 비밀번호를 로컬에서 참고하는 git-ignore 파일**일 뿐이다. AD에서 비밀번호를 변경하면 실제 로그인에는 새 AD 비밀번호가 즉시 적용되며, 이 파일은 참고값으로만 함께 갱신한다. 이 비밀번호는 OAuth client secret이나 OCI IAM service client secret과 전혀 다른 값이다.
실제 값은 저장소·문서·채팅에 기록하지 않는다. 현재 작업 환경에서는 git-ignore된 권한 제한 파일 `.runtime/dds-ad-test-users.env`에 보관한다. 다음 명령은 비밀번호를 터미널에 표시하지 않고 macOS 클립보드에 복사한다.
```bash
# dds-alice 비밀번호 복사
sed -n 's/^DDS_ALICE_PASSWORD=//p' .runtime/dds-ad-test-users.env | pbcopy
# dds-bob 비밀번호 복사
sed -n 's/^DDS_BOB_PASSWORD=//p' .runtime/dds-ad-test-users.env | pbcopy
```
명령 출력이 없는 것이 정상이다. 원하는 사용자 항목을 실행한 다음 Keycloak 로그인 화면의 password 칸에 `⌘V`로 붙여넣는다. 로그인 ID는 각각 `dds-alice`, `dds-bob`이며, 도메인 표기가 필요한 Windows/LDAP 화면에서는 `DDS\\dds-alice`, `DDS\\dds-bob`을 사용한다. `pbcopy`가 올바른 명령이며 `pbcop`는 오타다.
파일이 없거나 비밀번호가 맞지 않으면 값 추측이나 문서 검색을 하지 않는다. AD 관리자에게 test-user password reset을 요청하고, reset 후 이 권한 제한 파일만 갱신한다. user password reset은 AIPF OAuth client secret이나 OCI IAM service client secret rotation을 의미하지 않는다.
## 4.2 AIPF MCP 등록 값
AIPF의 **Edit MCP server** 화면에서 다음 값으로 등록한다. AIPF는 confidential OAuth client를 요구하므로, 일반 테스트용 public client `dds-mcp`가 아니라 AIPF 전용 client `aipf-dds-mcp`를 사용한다.
| AIPF 항목 | 입력값 |
|---|---|
| Server name | `alice-dds` (Bob 검증 때는 `bob-dds`처럼 별도 등록) |
| Server URL | `https://hmm-backoffice.cloud-handson.com/mcp/dds/sse` |
| Authentication mode | `OAuth` |
| Redirect callback URL | AIPF 표시값 `https://aipf.cloud-handson.com/agentFactory/v1/tools/mcp/callback` — 입력하지 않는 읽기 전용 값 |
| OAuth client ID | `aipf-dds-mcp` |
| OAuth client secret | 운영자 보관 secret. 저장소·문서·대화에 기록하지 않는다. 현재 작업 환경에서는 `.runtime/aipf-dds-mcp-client.env``OAUTH_CLIENT_SECRET` 값을 사용한다. |
| Authorization URL | `https://ad.cloud-handson.com/realms/dds-test/protocol/openid-connect/auth` |
| Token URL | `https://ad.cloud-handson.com/realms/dds-test/protocol/openid-connect/token` |
| Refresh URL | `https://ad.cloud-handson.com/realms/dds-test/protocol/openid-connect/token` |
| Scopes | `openid profile email` |
### OAuth client secret을 얻는 방법
여기서 필요한 secret은 AD 사용자 비밀번호가 아니라 Keycloak의 `aipf-dds-mcp` OAuth client secret이다. secret은 저장소에 커밋하거나 문서·대화에 적지 않는다. 현재 작업 환경에서는 권한 제한 파일에 보관하며, 다음 명령으로 **값을 화면에 표시하지 않고** macOS 클립보드에 복사한다.
```bash
sed -n 's/^OAUTH_CLIENT_SECRET=//p' .runtime/aipf-dds-mcp-client.env | pbcopy
```
명령 출력이 없는 것이 정상이다. 이후 AIPF의 **OAuth client secret** 입력칸에 `⌘V`로 붙여넣고 저장한다. 복사 명령은 `pbcopy`이며 `pbcop`가 아니다. 파일의 secret 값은 Keycloak에서 OAuth client를 재생성하거나 rotation한 경우에만 다시 동기화한다.
### AIPF callback 호환 보정
현재 AIPF는 화면에 표시하는 callback과 달리 OAuth authorization request에는 `/agentFactory`가 빠진 `https://aipf.cloud-handson.com/v1/tools/mcp/callback`을 보낸다. AIPF Nginx는 이 경로를 같은 origin의 실제 callback으로 302 전환하도록 구성했다.
```text
/v1/tools/mcp/callback
-> /agentFactory/v1/tools/mcp/callback
```
이 전환은 OAuth `code``state`를 보존하고 브라우저가 `/agentFactory` 범위의 AIPF 세션 쿠키를 다시 전송하게 한다. AIPF의 OAuth callback 생성 로직이 수정되면 이 Nginx 보정은 제거할 수 있다.
### AIPF 로그인 상태와 MCP 등록을 구분할 것
AIPF의 MCP 등록 정보와 Keycloak의 브라우저 SSO 세션은 서로 다르다.
| 상태 | 보관 위치 | 영향 |
|---|---|---|
| MCP OAuth client ID/secret, 연결 설정 | AIPF의 MCP source 설정 | 최초 OAuth 연결 및 refresh token 교환에 사용 |
| Keycloak SSO 로그인 세션 | 브라우저의 `ad.cloud-handson.com` cookie | 다음 OAuth 연결 시 이전 사용자로 자동 로그인될 수 있음 |
| AIPF가 저장한 MCP token/refresh token | AIPF backend source별 저장소 | 등록한 MCP source가 이후 tool 호출할 때 사용 |
그러므로 Alice에서 Bob을 시험할 때는 source를 `bob-dds`로 새로 만들거나 기존 연결을 끊고, Keycloak logout을 먼저 수행한다. 같은 browser session에서 바로 다시 연결하면 AIPF 설정을 Bob으로 바꿔도 Keycloak SSO가 Alice를 다시 인증할 수 있다. Keycloak logout endpoint는 다음과 같다.
```text
https://ad.cloud-handson.com/realms/dds-test/protocol/openid-connect/logout
```
새 AIPF source에는 OAuth client ID와 secret을 다시 입력한다. source 간에 인증정보가 자동 상속된다고 가정하지 않는다.
## 4.3 테스트 시나리오
### A. Alice 인증과 MCP 연결
1. AIPF에서 위 값으로 `alice-dds`를 저장하고 연결한다.
2. Keycloak 로그인 화면에서 AD 사용자 `dds-alice`로 로그인한다. 도메인 표기가 필요하면 `DDS\\dds-alice`를 사용한다.
3. AIPF가 callback으로 돌아오면 `alice-dds` MCP 연결이 완료된다.
4. AIPF Agent 대화에서 다음 요청을 실행한다.
```text
dds_vector_search 도구를 사용해서 "휴가"를 검색해줘.
embeddingMode는 DEMO, limit은 10으로 해줘.
```
5. MCP는 Keycloak access token의 검증된 `iss + sub`를 Alice의 application user와 `DDS_U_1`에 매핑한 뒤 `dds_vector_search`를 호출한다.
### B. Bob 권한 비교
1. AIPF OAuth 연결을 끊거나 새 MCP 등록 `bob-dds`를 만든다.
2. 같은 OAuth 설정으로 연결한 뒤 AD 사용자 `dds-bob`으로 로그인한다.
3. Alice와 동일한 검색 요청을 실행한다.
4. DDS data grant 구성 후 기대 결과는 다음과 같다.
| 사용자 | JWT subject 매핑 | 기대 결과 |
|---|---|---|
| `dds-alice` | application user `1` → `DDS_U_1` | 허용된 지식 행만 반환 |
| `dds-bob` | application user `2` → `DDS_U_2` | 빈 결과 또는 DDS 권한 거부 |
### C. 판정 기준과 현 상태
| 확인 항목 | 판정 방법 | 현재 상태 |
|---|---|---|
| OAuth 로그인 | AIPF가 Keycloak 로그인 후 MCP 등록 화면으로 복귀 | 확인 완료 |
| JWT 검증/매핑 | `tools/list` 또는 `dds_vector_search`가 Bearer 토큰 검증을 통과 | 확인 완료 |
| Alice/Bob 데이터 차이 | 같은 `dds_vector_search` 호출에서 반환 행이 다름 | 확인 완료 — Alice 1건 반환, Bob default deny |
| 미매핑 토큰 거부 | 매핑 없는 `iss + sub`의 MCP 호출이 `AUTHORIZATION_DENIED` | 구현 완료, 회귀 검증 대기 |
`dds_vector_search`는 현재 유일한 DDS MCP 도구이며 입력값은 `query`(필수), `limit`(1~100), `embeddingMode`(`DEMO` 또는 `AI`)다. service identity와 Alice 허용/Bob 거부 DDS grant를 반영해 권한 차이까지 검증했다.
## 4.4 OCI IAM database-access token 구성
Keycloak access token은 MCP의 업무 사용자 식별과 `iss + sub` 매핑에만 사용한다. Oracle Deep Sec context를 열 때는 별도의 OCI IAM database-access token이 필요하다. 이 토큰은 `DDS_MCP_SERVICE_TEST` confidential client가 client credentials flow로 받고, DB는 해당 client ID를 application identity로 신뢰한다.
```text
AD user -> Keycloak token -> MCP user mapping -> local DDS END USER
\
OCI IAM DDS_MCP_SERVICE_TEST -- database-access token --> Oracle Deep Sec context
```
구성 순서는 OCI IAM database resource(`DDS_ORACLE_DB_TEST`)와 scope(`DB_ACCESS_SCOPE`) 생성, OCI IAM service client 생성, Autonomous DB의 OCI IAM identity provider/credential 등록, 그리고 `CREATE APPLICATION IDENTITY ... MAPPED TO 'IAM_OAUTH_CLIENT_ID=...'` 순서다. 이 단계는 Autonomous DB의 외부 인증 구성을 변경할 수 있으므로 기존 설정을 먼저 조회하고 Redmine에 기록한다.
### 실제 구성 절차와 신뢰 등록 방향
다음 순서는 “누가 누구를 신뢰하도록 등록하는가”를 기준으로 작성했다. secret, client ID, application ID는 환경별 값이므로 이 문서에 넣지 않고 권한 제한 환경 파일 또는 OCI console에서 관리한다.
1. OCI Identity Domain에서 database resource/integrated application `DDS_ORACLE_DB_TEST`를 만들고 `DB_ACCESS_SCOPE`를 정의한다. 이것이 DB가 받아들일 audience와 scope의 계약이다.
2. 같은 Domain에서 confidential client `DDS_MCP_SERVICE_TEST`를 만든다. grant type은 `client_credentials`만 허용하고, 1단계의 DB scope만 허용한다. 발급된 client secret은 MCP host의 mode 600 환경 파일에만 저장한다.
3. Autonomous DB에 OCI IAM external authentication을 활성화하고, Domain URL 및 database resource application ID를 등록한다. DB의 issuer URL은 token `iss`와 정규형까지 일치해야 한다. 이 환경에서는 HTTPS `:443`을 포함한다.
4. DB에 OCI IAM signing-key credential을 만들고, `DDS_MCP_SERVICE_TEST`의 OAuth client ID를 DB application identity에 매핑한다. 이 단계로 DB는 “이 client credentials로 발급된 DB token을 가진 서비스”를 신뢰한다.
5. HMM MCP runtime에 service client ID, secret, token endpoint, scope, resource 대상 view를 주입하고 서비스 계정을 재시작한다. 사용자 access token이나 AD 비밀번호를 이 파일에 넣지 않는다.
6. 별도로 Keycloak `iss + sub` → `CB_APP_USER` → `CB_DDS_END_USER_MAP`을 등록한다. `DDS_U_1_ROLE` 같은 local role에 대상 객체 DATA GRANT를 필요한 사용자에게만 부여한다.
신뢰의 방향은 아래와 같다.
```text
AD DS ──(LDAP credential 확인)──► Keycloak
Keycloak ──(signed user JWT)────► MCP
MCP ──(client credentials)──────► OCI Identity Domain
OCI Identity Domain ──(signed DB token)──► Oracle DB
Oracle DB ──(DDS grant 평가)────► 데이터
```
Oracle DB는 AD DS나 Keycloak을 직접 신뢰하도록 등록되어 있지 않다. 반대로 OCI IAM integrated application도 AD 사용자 credential을 검증하지 않는다. MCP가 두 신뢰 체인의 검증 결과를 결합하는 위치다.
### MCP 런타임에서 token을 사용하는 방식
MCP 요청을 처리할 때 서비스는 두 token을 다음 순서로 취급한다.
| 순서 | token | MCP가 하는 일 | 실패하면 |
|---:|---|---|---|
| 1 | Keycloak user access token | HTTP `Authorization: Bearer`에서 추출, JWKS 검증, `iss + sub` binding 해석 | `401` 또는 `AUTHORIZATION_DENIED`; DB에 접속하지 않음 |
| 2 | OCI IAM database-access token | server-side client credentials로 얻어 DB connection/context attach에 사용 | DDS context 불가; 사용자 token으로 대체하지 않음 |
| 3 | 없음(내부 context) | `DDS_U_n`의 data role/data grant로 target view를 query | Oracle default deny; 결과 반환 안 함 |
MCP endpoint는 `https://hmm-backoffice.cloud-handson.com/mcp/dds/sse`이며, SSE transport의 message endpoint는 `https://hmm-backoffice.cloud-handson.com/mcp/dds/messages`다. browser 주소창으로 SSE URL을 여는 것은 OAuth 로그인 페이지가 아니라 event stream 연결 시도이므로 유효한 동작 검증 방법이 아니다. AIPF OAuth 등록 또는 Bearer token을 포함한 MCP client로 접속해야 한다.
### 적용 결과와 검증
| 구성 요소 | 적용값 | 상태 |
|---|---|---|
| OCI IAM database resource | `DDS_ORACLE_DB_TEST` / `DB_ACCESS_SCOPE` | 적용 완료 |
| OCI IAM service client | `DDS_MCP_SERVICE_TEST`, client credentials만 허용 | 적용 완료 |
| Autonomous DB identity provider | `OCI_IAM`, database resource app ID와 OCI IAM domain URL 등록 | 적용 완료 |
| DB signing-key credential | `OCI_IAM_DOMAIN_DB_CRED$` | 적용 완료 |
| DB application identity | `DDS_MCP_SERVICE_TEST` → OCI IAM service client ID | 적용 완료 |
| HMM DDS MCP runtime | client ID·secret·scope를 권한 제한 환경 파일로 로드 | 적용 완료 |
| Alice data grant | `DDS_U_1_ROLE` → `CB_VECTOR_SEARCH_DOCUMENTS` SELECT | 적용 완료 |
| Bob data grant | 없음 | default deny |
database-access token은 `resource_app_id`, `tenant_iss`, scope가 DB identity provider 등록과 모두 일치해야 한다. OCI IAM domain URL은 token의 issuer와 같은 정규형(`:443` 포함)을 사용했다. 포트가 빠진 URL로 등록하면 Oracle이 `ORA-52602`(invalid database access token)으로 context 사용을 거부한다.
2026-08-04 검증 결과는 다음과 같다. 동일한 `dds_vector_search` 호출에서 Alice는 DDS context attach 후 query가 성공했고, Bob은 data grant가 없어 보호 객체 조회가 거부됐다. 초기에는 HMM knowledge chunk 데이터가 없어 Alice의 성공 응답 행 수가 `0`이었으나, 이후 비교 가능한 테스트 문서를 추가하여 Alice의 1건 반환까지 재검증했다.
이후 권한 차이를 눈으로 확인할 수 있도록 `DDS_AD_ALICE_LEAVE_001` 테스트 문서와 `DDS_AD_TEST` 태그를 HMM knowledge 원천에 추가했다. 같은 `휴가` 검색에서 Alice는 이 문서 1건을 반환하고 Bob은 `ORA-00942` 기반 default deny로 거부되는 것을 확인했다.
### AIPF 화면에서의 권한 차이 해석
동일한 AIPF Agent 요청을 Alice와 Bob으로 실행한다.
```text
dds_vector_search 도구를 사용해서 "휴가"를 검색해줘.
embeddingMode는 DEMO, limit은 10으로 해줘.
```
| 로그인 사용자 | AIPF에서 보이는 결과 | DB 측 실제 의미 | 판정 |
|---|---|---|---|
| `dds-alice` | `휴가 정책 - Alice 전용 테스트` 1건 반환 | Keycloak `sub` → `DDS_U_1`, `DDS_U_1_ROLE`의 SELECT DATA GRANT가 `CB_VECTOR_SEARCH_DOCUMENTS`에 적용 | 허용 |
| `dds-bob` | “검색에 실패했습니다”, “DDS 보호 객체를 조회할 수 없습니다” | Keycloak `sub` → `DDS_U_2`, 해당 role에 DATA GRANT가 없어 Oracle이 `ORA-00942`로 보호 객체를 숨김 | default deny / 정상 차단 |
따라서 Bob의 AIPF 화면 문구는 시스템 장애가 아니라 의도한 보안 결과다. 현재 MCP 응답은 SQL 오류를 외부에 노출하지 않기 위해 일반 문구로 변환한다. 운영 UI에서는 이를 “접근 권한이 없습니다”로 표시하도록 개선할 수 있지만, PoC의 권한 검증 자체는 Alice 허용·Bob 거부로 완료됐다.
## 5. 운영 보안 기준과 장애 구분
### secret과 token의 보관 원칙
| 값 | 소유자 | 허용 위치 | 금지 위치 |
|---|---|---|---|
| AD 사용자 비밀번호 | 사용자/AD | AD에서 hash로 관리, 사용자가 Keycloak login form에만 입력 | MCP 설정, AIPF secret, Git, 문서 |
| Keycloak AIPF client secret | AIPF OAuth client | 권한 제한 secret store 또는 `.runtime/aipf-dds-mcp-client.env` | Git, Redmine 본문, 채팅 |
| OCI IAM service client secret | MCP backend | HMM host의 mode 600 환경 파일/secret manager | AIPF browser, Java source, Git |
| Keycloak user access token | AIPF/MCP 요청 경로 | AIPF의 보호된 token 저장소, HTTPS request header | URL query, application log |
| OCI IAM DB service token | MCP process memory | token endpoint 응답 및 DB context attach | browser, client log, source code |
secret rotation 시에는 Keycloak 또는 OCI IAM에서 새 secret을 만들고, 해당 runtime secret store만 갱신한 뒤 서비스를 재시작한다. 이전 secret의 폐기는 새 token 발급과 MCP query를 확인한 후 수행한다. secret의 실제 값은 로그·shell history·스크린샷에도 남기지 않는다.
### default deny가 정상인 경우와 장애인 경우
| 관측 결과 | 가능한 원인 | 기대 처리/조치 |
|---|---|---|
| 이전 Alice로 자동 로그인 | Keycloak SSO cookie가 남아 있음 | logout 후 Bob으로 다시 인증 |
| `401` 또는 tools discovery 실패 | Bearer token 없음/만료, issuer·audience 검증 실패, AIPF source OAuth 설정 누락 | AIPF OAuth 설정과 Keycloak token claim 확인 |
| `invalid_request: Missing parameter: code_challenge_method` | Keycloak client에 PKCE 강제인데 AIPF가 PKCE를 보내지 않음 | 현재 AIPF client의 PKCE 정책을 호환 설정으로 조정; AIPF 지원 후 S256 전환 |
| `unauthorized_client` / invalid client credentials | AIPF에 잘못된 Keycloak client secret 저장 | 안전한 runtime 파일에서 secret을 다시 복사하고 source 재연결 |
| `DDS_CONTEXT_UNAVAILABLE` | OCI IAM client/scope/token endpoint 또는 DB external auth 구성 누락 | service client scope, DB application identity, domain issuer URL 점검 |
| Oracle `ORA-52602` | database-access token issuer/resource/scope와 DB 등록값 불일치 | Domain URL 정규형(이 환경은 `:443` 포함), resource app ID와 scope 점검 |
| Alice는 결과, Bob은 “DDS 보호 객체” 오류 | Bob에 DATA GRANT 없음 | 의도한 default deny. 정책상 필요한 경우에만 최소 grant 추가 |
| Alice도 Bob도 모두 결과 없음 | source data 부재, vector/embedding 조건, 공통 service token 문제 | 데이터 존재 여부와 DDS context attach를 분리해 점검 |
권한 거부와 인프라 장애를 구분하기 위해 MCP 내부 log에는 SQLState/Oracle error code를 남길 수 있으나, 외부 MCP 응답에는 table명·SQL·DB credential 정보를 노출하지 않는다. 감사 로그에는 요청 시각, correlation ID, token의 `iss + sub` hash 또는 내부 user ID, 선택된 DDS END USER, tool명, allow/deny 결과를 남긴다.
### 최소 권한 운영 원칙
1. `DDS_MCP_SERVICE_TEST`에는 database-access scope 외의 OCI 권한을 부여하지 않는다.
2. DB application identity는 MCP runtime 전용이며, 개인 사용자나 AIPF browser가 직접 사용하지 않는다.
3. local DDS END USER에는 필요한 DATA ROLE만 붙이고, 권한은 object/row/column 단위로 좁힌다. 새 사용자는 grant 없이 생성하는 default deny를 기본으로 한다.
4. `CB_EXTERNAL_IDENTITY_BINDING` 변경은 관리자만 수행하고 issuer와 immutable subject를 audit한다. UPN/email 변경만으로 기존 권한이 다른 계정에 이전되지 않아야 한다.
5. HTTPS는 Keycloak, AIPF, MCP 모두에서 강제하고, JWKS·token endpoint 호출의 TLS 검증을 끄지 않는다.
6. AD DS VM의 RDP와 LDAP 접근은 관리망/허용 IP로 제한한다. public LDAP/LDAPS를 인터넷에 열지 않는다.
7. 테스트 종료 후 Windows VM, public DNS, test clients와 secret의 보존·폐기 여부를 Redmine에 기록한다.
## 6. Entra ID로 전환할 때
Entra tenant가 확보되면 AD DS/bridge 테스트에서 확인한 `issuer + immutable subject -> CB_APP_USER -> DDS END USER` 계약은 유지한다. 바뀌는 부분은 token issuer와 JWKS 검증 설정뿐이다. Entra의 claim 이름(`oid`, `sub`, `preferred_username` 등)은 실제 발급 토큰을 확인한 뒤 결정하며, `sub` 단독이 아니라 tenant/issuer 경계를 반드시 포함한다.
## 7. 완료 기준
- [x] `dds.test` AD forest와 두 테스트 사용자가 생성되었다.
- [x] Keycloak LDAP bridge가 HTTPS OIDC JWT를 발급하고 각 사용자가 서로 다른 안정 subject를 가진다.
- [x] MCP가 JWT signature/issuer/audience를 검증하고 `issuer + sub` 매핑을 해석한다.
- [x] subject 매핑을 통해 각 요청에 대응하는 DDS END USER context만 attach된다.
- [x] 서로 다른 권한의 동일 MCP 호출에서 데이터 행/열 결과가 달라진다.
- [ ] 미매핑·만료·issuer/audience 불일치 요청은 데이터 접근 전에 거부된다.
- [ ] 종료 시 테스트 VM과 전용 네트워크 리소스의 정리 여부 및 비용 상태를 Redmine에 기록한다.

View File

@@ -0,0 +1,79 @@
# Cookbook: OCI AD Bridge로 DDS MCP 사용자 인증 전환
[개요로 돌아가기](README.md) · [아키텍처](architecture.md) · [트러블슈팅](troubleshooting.md)
## 적용 순서
Keycloak을 삭제하지 않는다. 아래 단계를 순서대로 끝내고 Alice/Bob DDS 결과를 확인한 뒤에만 AIPF의 기본 MCP source를 OCI 경로로 바꾼다.
| 단계 | 작업 | 성공 판정 | 실패 시 |
|---:|---|---|---|
| 0 | AD·네트워크·관리 경로 점검 | AD/LDAPS/OCI HTTPS 연결 가능 | [Windows/LDAPS](troubleshooting.md#windows-ad-bridge) |
| 1 | OCI AD Bridge 생성·Windows client 설치 | Bridge `Connected` | [Windows/AD Bridge](troubleshooting.md#windows-ad-bridge) |
| 2 | AD OU/그룹 동기화 | OCI Domain에 Alice/Bob 표시 | [동기화](troubleshooting.md#동기화와-delegated-authentication) |
| 3 | delegated authentication 시험·활성화 | AD 비밀번호 로그인 성공 | [동기화](troubleshooting.md#동기화와-delegated-authentication) |
| 4 | AIPF OAuth source 등록 | OCI 로그인 → callback 완료 | [OAuth/AIPF](troubleshooting.md#oci-oauth와-aipf) |
| 5 | MCP issuer/audience·DB binding 교체 | OCI JWT 검증·DDS mapping 성공 | [MCP/DDS](troubleshooting.md#mcp와-dds) |
| 6 | Alice/Bob 비교 및 Keycloak 정리 판단 | Alice 1건, Bob 거부 | [권한](troubleshooting.md#mcp와-dds) |
## 0. 사전 점검
- AD Domain: `dds.test`
- OCI Identity Domain: `identityAPAC`
- Bridge 설치 위치: 운영은 domain-joined Windows member server 권장. 이 PoC는 격리된 AD VM에서 설치 가능 여부를 검증한다.
- 네트워크: Bridge host → OCI Domain HTTPS 443, Bridge host → AD LDAPS 636
- AD Bridge service account: 동기화 대상 OU 읽기, `cn=Deleted Objects` 읽기, delegated authentication에 필요한 password/lockout attribute 최소 권한
Windows VM에 RDP 외의 관리 경로가 없으면 installer를 RDP에서 실행한다. WinRM/OCI Run Command 상태가 불완전하면 원격 설치 수단으로 가정하지 않는다.
## 1. OCI AD Bridge 생성과 설치
1. OCI Console에서 **Identity & Security → Domains → identityAPAC → Directory integrations → Add → Microsoft Active Directory Bridge**를 연다.
2. 표시되는 **Bridge client ID/secret**과 Domain URL을 secret store에 보관한다.
3. Bridge installer를 domain-joined Windows에 설치한다.
4. installer에서 OCI Domain URL/client credential 및 AD Bridge service account를 입력한다.
5. **LDAPS**를 선택하고 연결 시험을 통과한다.
6. Directory integrations에서 사용자·그룹 OU를 최소 범위로 선택하고 initial sync를 실행한다.
Bridge client secret은 AIPF OAuth client secret 및 DB service client secret과 다르다. 각 secret을 서로 대체해 입력하지 않는다.
## 2. delegated authentication 활성화
1. OCI Domain에서 동기화된 `dds-alice`, `dds-bob`을 확인한다.
2. **Security → Delegated authentication → Test Delegated Authentication**에서 각 AD 계정/AD 비밀번호를 시험한다.
3. 두 계정이 성공한 뒤에만 `Activate Delegated Authentication`을 켠다.
이후 OCI 로그인 화면에 입력한 비밀번호는 Bridge를 거쳐 AD DS에서 검증된다. OCI Domain은 비밀번호 원천이 아니라 OIDC issuer다.
## 3. AIPF OCI OAuth client
`AIPF_DDS_AD_TEST`는 AIPF 전용 confidential client다.
| 설정 | 값 |
|---|---|
| Grant | `authorization_code`, `refresh_token` |
| Redirect URI | `https://aipf.cloud-handson.com/agentFactory/v1/tools/mcp/callback``https://aipf.cloud-handson.com/v1/tools/mcp/callback` |
| Scope | `openid profile email offline_access` |
| Authorization URL | discovery의 `authorization_endpoint` |
| Token / Refresh URL | discovery의 `token_endpoint` |
discovery URL은 `https://<identity-domain>/.well-known/openid-configuration`이다. endpoint를 추측해 입력하지 않는다. local test 환경의 client credential은 git-ignore된 `.runtime/aipf-dds-oci-ad-client.env`에만 보관한다.
## 4. AIPF source와 MCP 전환
1. AIPF에서 기존 Keycloak source를 삭제하지 않고 새 source를 만든다.
2. Server URL은 `https://hmm-backoffice.cloud-handson.com/mcp/dds/sse`를 유지한다.
3. Authentication mode를 `OAuth`로 선택하고 OCI client ID/secret과 discovery endpoint를 입력한다.
4. OCI login에서 Alice로 로그인한 뒤 받은 access token의 `iss`, `aud`, `sub`를 안전하게 확인한다.
5. MCP의 issuer/audience와 `CB_EXTERNAL_IDENTITY_BINDING`을 OCI claim 기준으로 함께 바꾼다.
6. Alice/Bob 권한 비교가 끝난 후에만 기존 source를 해제한다.
## 5. 검증과 rollback
| 검증 | 성공 |
|---|---|
| Alice `휴가` 검색 | `휴가 정책 - Alice 전용 테스트` 1건 |
| Bob 같은 검색 | 권한 없는 보호 객체가 default deny |
| 잘못된/만료/미매핑 OCI JWT | DB query 이전에 거부 |
실패 시 MCP issuer/audience와 AIPF source를 Keycloak 값으로 되돌린다. DB service client, `DDS_U_1`/`DDS_U_2`, DATA GRANT는 OCI 사용자 OAuth 전환과 별개이므로 rollback 대상이 아니다.

View File

@@ -0,0 +1,43 @@
# 트러블슈팅: OCI AD Bridge · OAuth · DDS MCP
[개요로 돌아가기](README.md) · [적용 Cookbook](cookbook.md) · [아키텍처](architecture.md)
## Windows AD Bridge
| 증상 | 원인 확인 | 해결 |
|---|---|---|
| WinRM TCP/5986 `Connection refused` | listener, Windows Firewall, NSG 확인 | RDP로 installer를 실행하거나 WinRM HTTPS를 별도로 구성한다. Bridge 자체 오류가 아니다. |
| OCI Run Command plugin은 `RUNNING`인데 command가 `ACCEPTED`/`VISIBLE` | 단순 `echo`도 실행되지 않으면 script 문제가 아님 | Windows Oracle Cloud Agent/Run Command service, outbound OCI 연결, plugin log를 RDP에서 점검한다. |
| LDAPS certificate 오류 | Bridge host가 AD DS server/CA certificate를 신뢰하는지 확인 | CA chain을 Windows trust store에 배포하고 LDAPS를 유지한다. |
| Bridge `Connected`가 되지 않음 | OCI 443, AD 636, Bridge service account credential/권한 확인 | NSG/proxy/firewall 및 최소 AD 권한을 순서대로 점검한다. |
## 동기화와 delegated authentication
| 증상 | 원인 확인 | 해결 |
|---|---|---|
| Alice/Bob이 OCI Domain에 보이지 않음 | 선택한 사용자 OU/하위 OU와 initial sync 상태 | OU 범위와 filter를 수정하고 sync를 재실행한다. |
| OCI 로그인 비밀번호가 실패 | delegated authentication test에서 동일 AD 계정으로 재현 | AD 비밀번호, Bridge AD 연결, service account delegated-auth 권한을 확인한다. |
| AD 비밀번호를 OCI local password로 입력하려 함 | delegated authentication 활성화 여부 | 성공 시험 후 활성화한다. 활성화 뒤 실제 비밀번호 원천은 AD다. |
## OCI OAuth와 AIPF
| 증상/오류 | 원인 | 해결 |
|---|---|---|
| `/admin/v1/admin/v1/Apps` 및 401 | `oci identity-domains --endpoint``/admin/v1`까지 넣음 | CLI에는 Domain base URL만 사용한다. raw request target에만 `/admin/v1`을 넣는다. |
| `No such option: --header` | `oci raw-request` 옵션명 오류 | `--request-headers '{"Content-Type":"application/json"}'`를 사용한다. |
| `Missing required attribute(s): basedOnTemplate` | confidential client app template 누락 | `basedOnTemplate.value=CustomWebAppTemplateId`를 지정한다. |
| authorize URL이 302 대신 200 | OCI Domain이 sign-in HTML과 session cookie를 반환 | 정상이다. browser에서 OCI login UI가 표시되는지 확인한다. |
| AIPF callback 오류 | redirect URI가 두 AIPF 경로 중 실제 요청 경로와 다름 | `/agentFactory/.../callback``/v1/.../callback` 둘 다 등록한다. |
| 이전 사용자로 자동 로그인 | OCI Domain browser SSO session 유지 | OCI logout 후 새 MCP source를 연결한다. |
## MCP와 DDS
| 증상 | 원인 | 해결 |
|---|---|---|
| MCP `401` / tools discovery 실패 | OCI issuer/audience가 runtime에 반영되지 않았거나 token 만료 | discovery/JWT claim을 기준으로 issuer·audience를 설정한다. |
| token 검증은 되지만 `AUTHORIZATION_DENIED` | OCI `iss + sub` binding 미등록/비활성 | `CB_EXTERNAL_IDENTITY_BINDING`과 DDS END USER mapping을 함께 확인한다. |
| `DDS_CONTEXT_UNAVAILABLE` | DB service client/scope/domain URL 설정 문제 | `DDS_MCP_SERVICE_TEST`, `DDS_ORACLE_DB_TEST`, `DB_ACCESS_SCOPE`, DB application identity를 확인한다. |
| `ORA-52602` | database-access token issuer/resource/scope 불일치 | DB의 OCI Domain URL 정규형(`:443` 포함), resource app ID/scope를 맞춘다. |
| Alice 성공, Bob “DDS 보호 객체” | Bob DATA GRANT 없음 | 의도된 default deny다. 정책상 필요할 때만 최소 grant를 추가한다. |
외부 오류 메시지에는 SQL, table명, secret을 노출하지 않는다. 내부 감사에는 correlation ID, 내부 user ID, 선택 DDS END USER, allow/deny 결과를 남긴다.