Skip to main content
Glama
gwanghun-choi

vault-mcp

README.md
# vault-mcp

`vault-mcp` 는 단순한 HashiCorp Vault 관리 도구가 아니라, 사내 **서버·웹사이트·API 접속
정보(Credential)를 중앙에서 인증·인가·감사하며 관리하는 Ops Broker** 다.

LLM(Claude) 또는 CLI 가 사용자의 요청을 중앙 API 에 전달하면, 중앙 API 가 **사용자 인증·인가 →
리소스 해석 → Vault Credential 조회 → 결과 마스킹 → 감사 기록**을 통제한다. 실제 Secret 은
Vault 에만 있고, LLM·CLI·DB·API 응답 어디에도 새지 않는다.

> **현재 단계: STEP 8.7 — Resource 유형 확장(SERVER/WEBSITE/API) 완료.**
> 다음은 STEP 9(인증 개선). SSH 실행(Runner)은 STEP 10 으로 아직 미구현이다.
> 단계별 상세는 [docs/development-roadmap.md](docs/development-roadmap.md).

---

## 목차

- [핵심 원칙](#핵심-원칙)
- [아키텍처](#아키텍처)
- [Resource 유형과 저장 책임](#resource-유형과-저장-책임)
- [API 레퍼런스](#api-레퍼런스)
- [인증 방식](#인증-방식)
- [Vault 경로와 삭제 정책](#vault-경로와-삭제-정책)
- [데이터 모델](#데이터-모델)
- [빠른 시작](#빠른-시작)
- [환경 변수](#환경-변수)
- [테스트·품질 게이트](#테스트품질-게이트)
- [디렉터리 구조](#디렉터리-구조)
- [개발 단계](#개발-단계)
- [배포](#배포)
- [기술 스택](#기술-스택)
- [문서 인덱스](#문서-인덱스)

---

## 핵심 원칙

| 원칙 | 내용 |
|---|---|
| **Secret 격리** | 실제 Secret 은 **Vault 에만**. DB·API 응답·로그·감사에는 `credential_ref`(논리 경로)만. |
| **1 Resource = 1 Credential** | 하나의 등록 항목 = 하나의 접속 대상 = 하나의 Credential. `credential_id` 개념 없음(1:N 아님). |
| **소유자 전용** | 리소스는 소유자만 관리. 타인 리소스는 **403 이 아니라 404**(존재 은닉). |
| **클라이언트 입력 불신** | 요청 body 의 `user_id`/`credential_ref`/`vault_path`/`host` 를 신뢰하지 않는다. 서버가 결정. |
| **LLM 은 별칭만** | Claude 는 `alias` + `operation` 만 추출. 서버 해석·Credential 조회는 중앙 API 가 사용자 권한 안에서 수행. |
| **감사 강제** | 모든 Credential 접근(등록/교체/삭제/reveal)은 감사 로그에 기록(Secret 원문 제외). |

---

## 아키텍처

현재 구현된 데이터 흐름(등록 / 조회 / 삭제). SSH 실행(Runner)은 STEP 10 으로 아직 없다.

```mermaid
flowchart TD
    subgraph client [클라이언트]
      W[Web UI · Jinja2]
      S[Claude Skill · REST]
    end
    W -->|Cookie 세션 + CSRF| API
    S -->|API Key| API
    API[vault-mcp FastAPI]

    API -->|인증 4종 → users.id| AUTH{인증·인가}
    AUTH -->|비민감 메타데이터| PG[(PostgreSQL<br/>vault_mcp schema)]
    AUTH -->|credential_ref 로 Secret| VAULT[(HashiCorp Vault<br/>KV v2)]
    API --> AUDIT[(audit_logs)]

    PG -.->|credential_ref = 논리 경로| VAULT
```

| 흐름 | 경로 |
|---|---|
| **등록** | 리소스 등록(PG) → Credential 등록 → Vault write → `credential_ref` 를 PG 에 저장 |
| **조회(reveal)** | 소유권 확인(PG) → `credential_ref` 로 Vault read → 평문 Secret 반환(no-store) |
| **삭제** | Vault soft delete → PG 상태/`credential_ref` 갱신 → commit. **commit 실패 시 Vault undelete 로 보상** |

컴포넌트 책임:

| 컴포넌트 | 한다 | 하지 않는다 |
|---|---|---|
| **Claude Skill** | `alias`/`operation` 추출, 결과 분석, **비민감 요약만** 출력 | Secret 원문 출력(사용자 요청해도 금지), Vault/경로 접근 |
| **Web UI** | 로그인, 리소스 CRUD, Credential 등록/교체/삭제, API Key 발급 | Secret 재표시(Web 에 reveal 없음) |
| **FastAPI** | 인증·인가·리소스 해석·Vault 연동·마스킹·감사 | Secret 을 응답/로그에 노출 |
| **Vault** | 실제 Secret 저장 | — |
| **PostgreSQL** | 비민감 메타데이터 + `credential_ref` | 실제 Secret 저장 |

---

## Resource 유형과 저장 책임

STEP 8.7 부터 등록 대상은 **SERVER / WEBSITE / API** 세 유형이다(`OTHER` 는 범위 밖). 유형은
등록 후 변경 불가(409). 정본: [docs/resource-types.md](docs/resource-types.md).

`✓` 사용 · `—` 항상 NULL(보내면 400) · **굵게** = 필수

| 필드 | 저장소 | SERVER | WEBSITE | API |
|---|---|:--:|:--:|:--:|
| `alias` / `description` / `service_name` | PG | **✓** / ✓ / **✓** | **✓** / ✓ / ✓ | **✓** / ✓ / ✓ |
| `status` / `environment` | PG | ✓ / ✓ | ✓ / — | ✓ / — |
| `host` / `ssh_port` | PG | **✓** / **✓** | — | — |
| `site_url` | PG | — | **✓** | — |
| `base_url` / `auth_type`(ApiAuthType) | PG | — | — | **✓** / **✓** |
| `header_name` / `token_prefix` / `scopes` / `expires_at` | PG | — | — | ✓ |
| `login_id` | PG | **—**(¹) | **✓** | ✓(= `client_id`) |
| `memo` | PG | — | ✓ | ✓ |
| `username` / `port` | **Vault** | **✓** / ✓ | — | — |
| `password` | **Vault** | ✓(²) | **✓** | — |
| `private_key` / `passphrase` | **Vault** | ✓(²) | — | — |
| `api_key` / `token` / `refresh_token` / `client_secret` / `webhook_secret` | **Vault** | — | — | ✓(³) |

- (¹) **SERVER 의 SSH `username` 은 Vault 에만** 있다(하위 호환). 그래서 목록 응답의 `login_id`
  는 SERVER 에서 항상 `null`.
- (²) SERVER 는 `auth_type`(SshAuthType) 에 따라 `password` **또는** `private_key`(+선택
  `passphrase`).
- (³) API 는 `auth_type`(ApiAuthType) 이 **어떤 Secret 이 필수인지 결정**한다.
  `client_id` 는 secret 이 아니므로 PG, **`client_secret` 은 Vault** — reveal 이 한 쌍으로 반환.

**인증 유형 enum 은 분리 유지**한다: SERVER = `SshAuthType`(PASSWORD/PRIVATE_KEY, Vault),
API = `ApiAuthType`(API_KEY/ACCESS_TOKEN/BEARER_TOKEN/PERSONAL_ACCESS_TOKEN/CLIENT_CREDENTIALS/
OAUTH_TOKEN/WEBHOOK_SECRET/CUSTOM, PostgreSQL). WEBSITE 는 인증 유형 개념이 없다.

---

## API 레퍼런스

Base prefix `/api/v1`. 스키마·Authorize·curl 예시: [docs/swagger-guide.md](docs/swagger-guide.md)
(`/docs`, `/redoc`, `/openapi.json` 은 local/dev 에서만 노출).

**Secret 반환** 은 reveal 하나뿐이다. 나머지 Credential API 는 상태 메타데이터만 준다.

| Method | Path | 용도 | 인증 / scope | Secret |
|---|---|---|---|:--:|
| GET | `/health` · `/ready` | Liveness / Readiness(DB+Vault) | 없음 | — |
| POST | `/auth/register` · `/auth/login` | LOCAL 가입 / 로그인(JWT 발급) | 없음 | — |
| GET | `/me` | 현재 사용자 프로필 | 인증 | — |
| POST/GET/DELETE | `/api-keys` · `/api-keys/{id}` | API Key 발급(1회 노출)/목록/폐기 | Web 사용자만(API Key 로 불가) | 발급 시 1회 |
| GET | `/servers` · `/servers/{alias}` | operation 권한 있는 서버 목록/상세 | `servers.read` | — |
| POST/PUT/GET/DELETE | `/resources/{id}/credentials` | **전 유형** Credential 등록/교체/상태/삭제 | 인증 | — |
| POST/PUT/GET/DELETE | `/servers/{id}/credentials/ssh` | SSH Credential (**SERVER 전용 하위 호환**) | 인증 | — |
| GET | `/my/resources` · `/my/resources/search` | 내 소유 리소스 목록/검색(전 유형) | `resources:read` | — |
| POST | `/my/resources/{id}/credentials/reveal` | ⚠️ Credential 원문 조회 | `credentials:reveal` | **평문** |

- `/resources/{id}/credentials`(전 유형, STEP 8.7 신규)와 `/servers/{id}/credentials/ssh`(SERVER
  전용 하위 호환)는 SERVER 에 대해 **동작·저장 결과가 동일**하다. 후자는 WEBSITE/API 대상이면 409.
- reveal 응답은 `resource_type` discriminator 로 갈리는 oneOf(SERVER/WEBSITE/API). SERVER 응답은
  `resource_type` 키만 additive 로 추가돼 기존 Skill 이 그대로 동작한다.
- scope 표기 불일치는 코드 그대로다: `servers.read`(점) vs `resources:read`/`credentials:reveal`(콜론).

**Web UI**(`/web/*`, HTML+Cookie, OpenAPI 미노출): 로그인, 대시보드, 리소스 CRUD(유형 선택 →
유형별 폼), Credential 등록/삭제(reveal 없음), API Key 발급/폐기. 상세: [docs/web-ui.md](docs/web-ui.md).

---

## 인증 방식

한 `Authorization` 헤더에서 4종을 라우팅하며 **모두 내부 `users.id` 로 귀결**된다. 클라이언트가
보낸 `user_id`/`tenant_id`/`oid` 는 신뢰하지 않는다.

| 방식 | 헤더 | 알고리즘 | 사용자 식별 | 용도 · 활성 조건 |
|---|---|---|---|---|
| **LOCAL JWT** | `Bearer <JWT>` | HS256 | `sub` = 내부 user_id | 개발 주 로그인 / Web UI. `local_auth_enabled` + issuer 일치 |
| **Microsoft Entra** | `Bearer <JWT>` | RS256(only, JWKS) | `(tid, oid)` 매핑 | 사내 SSO. Bearer JWT 기본 경로 |
| **API Key** | `ApiKey <dv_...>` (또는 `Bearer dv_...`) | HMAC-SHA256 해시 | `key_hash` → user_id | Claude/CLI. 접두 `dv_dev_`/`dv_live_` |
| **DEV** | 헤더 없음 | — | `dev_auth_user_id` | 로컬 편의. `dev_auth_enabled` + `is_local`(prod fail-fast) |

- **저장 안 되는 것:** 평문 password(Argon2id 해시만), Access/Refresh Token, **API Key 원문**(발급
  시 1회만 노출). 상세: [docs/token-storage-policy.md](docs/token-storage-policy.md).
- LOCAL(HS256)과 Entra(RS256)는 알고리즘·issuer·audience 로 분리, 교차검증 금지.
- API Key scope: `resources:read`, `credentials:reveal`, `servers.read` 등. Web 사용자는 scope
  검사를 통과(스코프 개념은 API Key 전용).
- 상세: [docs/local-authentication.md](docs/local-authentication.md),
  [docs/entra-authentication.md](docs/entra-authentication.md),
  [docs/api-key-authentication.md](docs/api-key-authentication.md).

---

## Vault 경로와 삭제 정책

논리 경로(DB `credential_ref` 에 저장). **DB 값이 경로의 정본** — 읽기/교체/삭제 시 재생성하지
않고 저장값을 그대로 쓴다(그래서 기존 행이 호환 로직 없이 동작). 경로는 UUID 로만 구성(사용자
입력·alias 미사용 → path traversal 차단).

| 구분 | 형식 |
|---|---|
| **신규**(전 유형) | `users/{user_id}/resources/{resource_id}/credential` |
| **legacy**(STEP 6~8 SERVER) | `users/{user_id}/servers/{server_resource_id}/ssh` |

KV v2 물리 경로 = `{mount}/data/{credential_ref}`. VaultClient 는 **App Token Policy 가 실제로
허용하는 endpoint 만** 쓴다:

| 동작 | HTTP endpoint | 필요 capability |
|---|---|---|
| write / read | `POST/GET /v1/{mount}/data/{path}` | `data/*` create·read·update |
| metadata read | `GET /v1/{mount}/metadata/{path}` | `metadata/*` read |
| **soft delete** | `POST /v1/{mount}/delete/{path}` | `delete/*` update |
| undelete(보상) | `POST /v1/{mount}/undelete/{path}` | `undelete/*` update |

> **삭제는 soft delete 다**(destroy 아님, 복구 가능). 정책이 `data/*` 에 delete 를 주지 않으므로
> `DELETE /data/` 가 아니라 **`POST /delete/`**(버전 지정, metadata 로 current_version 선조회)를
> 쓴다 — 전자를 쓰면 403 으로 삭제가 전부 실패한다. **Resource 삭제도 Vault Secret 을 함께 soft
> delete** 하고, DB commit 실패 시 **undelete 로 보상**한다.
> 정본: [docs/vault-client.md](docs/vault-client.md), [docs/credential-storage-flow.md](docs/credential-storage-flow.md).

---

## 데이터 모델

전용 PostgreSQL schema `vault_mcp` 에 격리(공유 DB `vault_mcp` 의 public·타 schema 미사용).
enum 은 비네이티브 VARCHAR(32). 정본: [docs/data-model.md](docs/data-model.md).

| 테이블 | 역할 |
|---|---|
| `users` | 신원. LOCAL(login_id+password_hash) / MICROSOFT(tenant_id+entra_oid) 두 출처 |
| `api_keys` | API Key 메타데이터. 원문 미저장(key_hash/key_prefix 만) |
| `server_resources` | 등록 대상 Resource(SERVER/WEBSITE/API 전부). 이름은 하위 호환으로 유지 |
| `server_permissions` | 인가 단위: 사용자 × 리소스 × operation. Default Deny, DENY 우선 |
| `vault_connections` | Vault 접속 메타데이터. 중앙 Vault(시스템 소유)면 owner NULL |
| `audit_logs` | 감사 이력. Secret 컬럼 없음 |

주요 제약(`server_resources`):

- `uq_server_resources_owner_user_id_alias` — UNIQUE(owner_user_id, alias) **WHERE status ≠
  'DELETED'**(partial). soft delete 는 행을 남기므로, 삭제한 별칭을 재사용할 수 있게 DELETED 를
  제외한다(0006). → 같은 alias 의 DELETED 행이 여러 개일 수 있어 alias 조회는 DELETED 를 거른다.
- `resource_type_allowed` — `resource_type IN ('SERVER','WEBSITE','API')`
- `auth_type_api_only` — `auth_type IS NULL OR resource_type = 'API'`(SshAuthType/ApiAuthType 혼용 차단)

**Migration 이력**(revision id 는 `alembic_version.version_num` VARCHAR(32) 제약):

| revision | 내용 |
|---|---|
| `0001_initial` | 초기 스키마 5테이블(vault_mcp schema, enum=VARCHAR(32)) |
| `0002_local_auth_and_api_keys` | LOCAL 로그인 필드 + api_keys 테이블, Entra 필드 nullable 전환 |
| `0003_credential_ref_nullable` | `server_resources.credential_ref` nullable(미등록=NULL) |
| `0004_server_environment` | `environment` 컬럼(DEV/STAGING/PROD 표시용) |
| `0005_resource_types` | `resource_type` + 유형별 필드, host/ssh_port/service_name NOT NULL 해제, 기존 행 SERVER backfill |
| `0006_alias_unique_active_only` | alias unique 를 DELETED 제외 partial index 로 교체 |

> SQLite(테스트)는 ALTER COLUMN / ADD CONSTRAINT 미지원 → NOT NULL 완화·CHECK 추가는
> **PostgreSQL 에서만** 수행하고, SQLite 는 모델 `create_all` 이 동일한 최종 스키마를 만든다.

---

## 빠른 시작

**요구:** Python 3.12+, [uv](https://docs.astral.sh/uv/). DB/Vault 없이도 `/api/v1/health` 는 뜬다.

```bash
# [로컬 PC / WSL·zsh]
uv sync                                   # 가상환경 + 의존성
cp .env.example .env                      # 값 채우기(아래 환경 변수 참고)
uv run uvicorn app.main:app --reload      # 또는: bash scripts/run-local.sh
```

- API 문서: `http://127.0.0.1:8000/docs` (local/dev 에서만)
- Web UI: `http://127.0.0.1:8000/web/login`
- PoC 로그인 계정 생성: `docs/poc-user-seed.md` 의 seed 스크립트

> LOCAL 로그인은 `.env` 에서 `LOCAL_AUTH_ENABLED=true` 여야 활성화된다(기본 false).
> Credential 등록/조회는 Vault 연결이 필요하다 — 개발 Vault 터널·App Token 준비는
> [docs/vault-local-development.md](docs/vault-local-development.md).

---

## 환경 변수

`.env` 로 관리(Git 커밋 금지, `.env.example` 만 저장). Secret 값은 **Vault 또는 K8S Secret** 으로,
`.env` 에는 서버별 Credential 을 넣지 않는다.

| 접두 | 예시 키 | 비고 |
|---|---|---|
| `APP_*` | APP_ENV, APP_HOST, APP_PORT | `APP_ENV` = local/dev/test/production |
| `DB_*` | DB_HOST/PORT/NAME/USERNAME/**PASSWORD**/SCHEMA | 공유 DB `vault_mcp`, schema `vault_mcp` |
| `LOCAL_*` / `PASSWORD_*` | LOCAL_AUTH_ENABLED, **LOCAL_JWT_SECRET**, LOCAL_JWT_ISSUER | HS256 자체 JWT |
| `API_KEY_*` | **API_KEY_HASH_SECRET**, API_KEY_MAX_ACTIVE_PER_USER | 미설정 시 SHA-256 fallback |
| `ENTRA_*` | ENTRA_TENANT_ID, **ENTRA_CLIENT_SECRET**, ENTRA_AUDIENCE, ENTRA_ISSUER | Resource Server 검증 |
| `VAULT_*` | VAULT_ADDR, **VAULT_TOKEN** / VAULT_TOKEN_FILE, VAULT_KV_MOUNT | Token 원문 또는 0600 파일 |
| `DEV_AUTH_*` | DEV_AUTH_ENABLED, DEV_AUTH_USER_ID | local/dev 한정(prod fail-fast) |
| 기타 | LOG_LEVEL, DOCS_ENABLED, CORS_ALLOWED_ORIGINS | |

**굵게** = `SecretStr`(로그/응답 미노출): `DB_PASSWORD`, `LOCAL_JWT_SECRET`,
`API_KEY_HASH_SECRET`, `ENTRA_CLIENT_SECRET`, `VAULT_TOKEN`.

---

## 테스트·품질 게이트

```bash
uv run pytest              # 493 passed, 7 skipped (skip = 실 Vault 통합 테스트)
uv run ruff check .        # All checks passed
uv run ruff format --check .
uv run mypy app            # strict, no issues (87 files)
```

- 테스트 32개 파일. 보안 경계(타 사용자 접근, 권한 없는 operation, Secret 노출, alias 위조,
  Vault 실패 일관성 등)를 필수로 테스트.
- 실 Vault 통합 테스트는 기본 skip. 실행:
  `VAULT_MCP_INTEGRATION=1 VAULT_ADDR=... VAULT_TOKEN_FILE=... uv run pytest tests/vault/test_real_vault_integration.py`.

---

## 디렉터리 구조

```
app/
  main.py                       FastAPI 앱 조립 + OpenAPI 설정
  core/                         config, exceptions, openapi, api_docs, logging, middleware, security
  api/
    dependencies.py             인증/서비스 DI
    v1/router.py, v1/endpoints/ health, auth, me, api_keys, servers,
                                credentials(SSH 하위호환), resource_credentials(전 유형),
                                my_resources(목록/검색/reveal)
    internal/                   dev 전용 라우터(local/dev 에서만 등록)
  auth/                         verifier(Entra), local_token/local_service, dependencies, context, jwks, password
  api_keys/                     generator, hasher, service, repository, schemas
  credentials/                  service, compensation, validation, schemas, errors
  server_resources/             crud, service(alias 해석/인가), repository, validation, schemas
  vault/                        client(KV v2), factory, token_file, credential_ref, errors
  db/                           base(schema 격리), session, models/{user,api_key,server_resource,...}
  domain/                       enums, errors, resolved
  audit/                        service(감사 기록)
  web/                          router(Jinja2), csrf, dependencies
  templates/  static/{css,js}   Web UI
  scripts/seed_poc_user.py      PoC 사용자 seed
  ssh/  operations/             SSH Runner·운영 API 자리(STEP 10~11, 미구현)
migrations/versions/            Alembic 0001~0006
tests/                          api, api_keys, auth, services, vault, web, skills, db, models
skills/vault-mcp/             전사 공통 Claude Skill(REST 호출, Secret 미출력)
deploy/                         gitops-repo(Kustomize+ArgoCD), k8s/dev(수동 kubectl 대안)
docs/                           설계·운영 문서(아래 인덱스)
Dockerfile  Jenkinsfile         멀티스테이지 이미지 / CI 파이프라인
```

---

## 개발 단계

| STEP | 내용 | 상태 |
|---|---|:--:|
| 1 | FastAPI 골격 | ✅ |
| 2 | DB 모델·권한·서버 해석(Alembic, schema 격리) | ✅ |
| 3 | Microsoft Entra 인증 | ✅ |
| 4 | LOCAL 로그인 + API Key + 사용자별 Vault 경로 모델 | ✅ |
| 5 | 개발 Vault 설치·단독 검증(NCP) | ✅ |
| 6 | FastAPI ↔ Vault 연동 + Credential 등록 | ✅ |
| 7 | Skill 기반 REST API PoC | ✅ |
| 8 | 사용자 Web UI(Jinja2) | ✅ |
| 8.5 / 8.6 | 개발 K8S 배포 / Jenkins+Harbor+ArgoCD GitOps | ✅ |
| **8.7** | **Resource 유형 확장(SERVER/WEBSITE/API)** | ✅ |
| 9 | 사용자 인증 개선(Device Flow/짧은 Access Token) | 예정 |
| 10 | SSH Runner 등 Credential 활용 | 미구현 |
| 11 / 12 / 13 | 읽기 전용 운영 API / CLI+Keyring / K8S 배포 정리 | 미구현 |

**아직 미구현:** SSH 실행(Runner), 운영 API(logs/status/disk), CLI/OS Keyring. Entra 실제 App
Registration 통합 검증은 조직 환경에서 수행 필요.

---

## 배포

Jenkins(CI) → Harbor(registry) → ArgoCD(GitOps). 정본: [docs/ci-gitops.md](docs/ci-gitops.md),
[deploy/gitops-repo/README.md](deploy/gitops-repo/README.md).

```mermaid
flowchart LR
    GIT[소스 push] -->|pollSCM| J[Jenkins]
    J -->|build| IMG[이미지 sha-태그]
    IMG -->|push| H[Harbor]
    J -->|newTag 갱신 commit| D[vault-mcp-deploy repo]
    D -->|watch| A[ArgoCD]
    A -->|PreSync: migration Job| K[K8S vault-mcp ns]
    A -->|sync: Deployment| K
```

- **이미지:** `Dockerfile` 멀티스테이지, non-root(uid 10001), uvicorn :8000. Secret 미포함(K8S Secret 주입).
- **Jenkinsfile:** Init(short SHA) → Build → Push Harbor → deploy repo 의 `newTag` 를 `sha-<SHA>` 로
  갱신. Deployment 와 migration Job 이 **같은 SHA** 로 반영(불일치 방지).
- **GitOps:** `deploy/gitops-repo/`(Kustomize + ArgoCD Application). migration 은 PreSync Hook, seed 는 수동 1회.
- `deploy/k8s/dev/` 는 GitOps 없이 **수동 kubectl apply 로 검증하는 대안**이다.

---

## 기술 스택

| 영역 | 스택 |
|---|---|
| 언어 | Python 3.12+ |
| 웹 | FastAPI ≥0.115, Uvicorn ≥0.34 |
| 검증 | Pydantic v2 ≥2.9, pydantic-settings ≥2.6 |
| DB | SQLAlchemy 2.x async, asyncpg ≥0.30, Alembic ≥1.14, PostgreSQL |
| 인증 | PyJWT[crypto] ≥2.9, pwdlib[argon2] ≥0.2 |
| Vault/HTTP | httpx ≥0.28 (KV v2 직접 호출) |
| Web | Jinja2 ≥3.1, python-multipart |
| 개발 | pytest + pytest-asyncio, aiosqlite, ruff(line 100, py312), mypy(strict) |
| 패키지 | uv(비패키지 애플리케이션 모드) |

---

## 문서 인덱스

| 주제 | 문서 |
|---|---|
| **개요** | [architecture.md](docs/architecture.md) · [security-principles.md](docs/security-principles.md) · [development-roadmap.md](docs/development-roadmap.md) |
| **Resource / 데이터** | [resource-types.md](docs/resource-types.md) · [data-model.md](docs/data-model.md) · [authorization-model.md](docs/authorization-model.md) · [server-resolution-flow.md](docs/server-resolution-flow.md) |
| **인증** | [local-authentication.md](docs/local-authentication.md) · [entra-authentication.md](docs/entra-authentication.md) · [entra-setup-guide.md](docs/entra-setup-guide.md) · [api-key-authentication.md](docs/api-key-authentication.md) · [token-storage-policy.md](docs/token-storage-policy.md) |
| **Vault / Credential** | [vault-client.md](docs/vault-client.md) · [vault-architecture-and-key-management.md](docs/vault-architecture-and-key-management.md) · [user-owned-vault-layout.md](docs/user-owned-vault-layout.md) · [credential-storage-flow.md](docs/credential-storage-flow.md) · [credential-reveal-api.md](docs/credential-reveal-api.md) · [vault-error-handling.md](docs/vault-error-handling.md) · [vault-local-development.md](docs/vault-local-development.md) · [vault-production-readiness.md](docs/vault-production-readiness.md) |
| **API / Swagger** | [swagger-guide.md](docs/swagger-guide.md) · [skill-rest-api-poc.md](docs/skill-rest-api-poc.md) · [api-key-auth-flow.md](docs/api-key-auth-flow.md) |
| **Web UI** | [web-ui.md](docs/web-ui.md) · [web-authentication.md](docs/web-authentication.md) · [api-key-web-flow.md](docs/api-key-web-flow.md) · [poc-user-seed.md](docs/poc-user-seed.md) |
| **Skill 배포** | [cowork-skill-deployment.md](docs/cowork-skill-deployment.md) · [security-model.md](docs/security-model.md) |
| **배포** | [ci-gitops.md](docs/ci-gitops.md) · [deploy/gitops-repo/README.md](deploy/gitops-repo/README.md) · [deploy/k8s/dev/README.md](deploy/k8s/dev/README.md) |
| **작업 기록** | [docs/work-log/](docs/work-log/) |

---

Git 정책: 기본 브랜치 `main`, 명시적 요청 없이 commit/push 하지 않는다. `.env`·Token·인증서·
Private Key 는 커밋 금지. 프로젝트 원칙과 작업 하네스는 [CLAUDE.md](CLAUDE.md).