Metadata-Version: 2.5
Name: chamelion-vault
Version: 0.3.0
Summary: Client for a self-hosted chamelion vault gateway
Requires-Python: >=3.10
Description-Content-Type: text/markdown

<!-- ⚠️ 이 파일은 PyPI 설명란에 그대로 실린다 — 전 세계에 공개된다.
     볼트 주소·DDNS·포트·조직 이름·실제 시크릿 경로를 적지 않는다.
     예시는 언제나 명백한 가짜(`myproject/dev/EXAMPLE_KEY`)로 쓴다.
     저장소의 `tests/test_client_package.py` 가 이 규칙을 검사한다. -->

# chamelion-vault

자체 호스팅 chamelion vault 게이트웨이에서 **시크릿 값을 받아 오는 클라이언트**입니다.

- **의존성이 없습니다.** 표준 라이브러리만 씁니다.
- **값을 디스크에 쓰지 않고 캐시하지 않습니다.** 받은 값은 프로세스 메모리에만 있습니다.
- **로깅을 설정하지 않습니다.** 이 패키지는 여러분의 애플리케이션 안에서 돕니다.

> 이 패키지는 **볼트를 운영하는 조직의 구성원**을 위한 것입니다. 볼트 주소·API 키·
> 터널 접속 정보는 여기 없고 사내 문서에 있습니다. 그것을 모르면 설치까지는 되지만
> 값을 받는 단계에서 막히는 것이 정상입니다.


## 쓰는 법

범위(팀 · 프로젝트 · 환경 · 앱)를 정한 `Vault` 를 하나 만들고, 그 위에서 받습니다.

```python
from chamelion_vault import Vault

vault = Vault(team="myteam", project="myproject", env="dev", app="api")

api_key = vault.secret("EXAMPLE_KEY").get_secret_value()   # 값 하나
everything = vault.secrets()                                # 받을 수 있는 것 전부
```

| 기능 | 하는 일 | 돌려주는 것 |
|---|---|---|
| `Vault(team=, project=, env=, app=)` | 범위를 정합니다. 비운 칸은 환경변수에서 읽습니다 | `Vault` |
| `vault.secret(name)` | 시크릿 하나의 **값** | 값 상자(`SecretStr`) |
| `vault.secret(name, field=)` | 번들의 필드 하나 | 값 상자 |
| `vault.secrets(shared=False)` | 이 범위에서 받을 수 있는 것 전부 | 이름 → `Secret` |
| `chamelion-vault run -- 명령` | 전부 받아 명령의 환경변수로 | 명령의 종료 코드 |

같은 코드가 노트북에서는 `dev`, 배포 서버에서는 `prod` 를 받도록 범위를 환경변수로 둘 수 있습니다.

```bash
export CHAMELION_VAULT_TEAM=myteam CHAMELION_VAULT_PROJECT=myproject CHAMELION_VAULT_ENV=dev
```

```python
vault = Vault()   # 팀 · 프로젝트 · 환경 · 앱을 환경변수에서
```

범위가 비어 있으면 **요청을 보내기 전에** `MissingScopeError` 가 비어 있는 칸을 전부 알려 줍니다.

### 앱

한 프로젝트 안의 서비스마다 다른 값(각자의 `DATABASE_URL` 등)은 **앱 전용**으로 등록됩니다.
`app` 을 주면 **프로젝트 공통 + 그 앱 전용**을 받고, 다른 앱의 값은 오지 않습니다 —
`secret()` 도 그 앱에 없는 이름은 프로젝트 공통에서 찾습니다. `CHAMELION_VAULT_APP` 이 설정된 환경에서
공통만 받으려면 `Vault(app="")` · `--app ""` 를 씁니다. 공통과 앱
전용에 같은 이름은 등록할 수 없으므로 어느 값이 이기는지 고민할 일이 없습니다.

### 번들 — 필드가 여럿인 시크릿

아이디 · 비밀번호처럼 한 몸인 크리덴셜은 한 시크릿 안의 여러 필드입니다. `secret()` 은
어느 필드인지 추측하지 않습니다.

```python
vault.secret("EXAMPLE_ACCOUNT", field="password").get_secret_value()
vault.secret("EXAMPLE_ACCOUNT")   # FieldSelectionError — 필드 이름(password, username)만 알려 줍니다
```

전부 필요하면 `secrets()` 의 `Secret` 을 씁니다.

```python
account = vault.secrets()["EXAMPLE_ACCOUNT"]
sorted(account)                     # ['password', 'username']
account["username"].get_secret_value()
account.project, account.env, account.app, account.name   # 위치 — 비밀이 아닙니다
```

`Secret.path` 는 0.2 호환용 표기입니다. 공용 · `common` 에서 온 값의 실제 위치는 `project` · `env` 로 확인하세요.

### 공용 값까지 한 번에

여러 프로젝트가 함께 쓰는 값은 볼트의 **공용 프로젝트**에 있습니다. 볼트 관리자가 내
프로젝트에 그 공용 프로젝트를 **구독**으로 열어 두면 한 번에 받습니다.

```python
everything = vault.secrets(shared=True)
```

`shared=True` 는 **프로젝트 공통 + 앱 전용 + 환경과 무관한 값(`common`) + 구독한 공용
프로젝트**(같은 환경과 `common`)를 한 응답으로 받습니다. 두 곳에 같은 이름이 있으면 서버가
어느 쪽도 고르지 않고 **전부 거부**합니다.

### `.env` 파일 없이 실행하기

```bash
chamelion-vault run -- python app.py
chamelion-vault run --app api --shared -- python app.py
```

받을 수 있는 값을 전부 받아 **실행할 명령의 환경변수로만** 넣습니다. 파일을 만들지 않고,
값을 화면에 찍지 않습니다. 이름은 시크릿 이름 그대로이고, 번들은 `이름_필드` 입니다
(`EXAMPLE_DB_HOST`). 이미 있는 환경변수와 이름이 겹치면 볼트 값이 덮어쓰고, 덮인 **이름**을
경고로 알립니다. 실행한 명령에는 API 키(`CHAMELION_VAULT_KEY`)를 넘기지 않습니다.

### 값이 상자에 담겨 옵니다

`get_secret_value()` 로 열기 전까지 값은 어디에도 그대로 나타나지 않습니다.

```python
value = vault.secret("EXAMPLE_KEY")
print(value)   # <REDACTED>
print(vault)   # Vault(myproject/dev/api) — 키는 객체에 없습니다
```

무심코 남긴 로그 한 줄에 진짜 키가 찍히는 것을 막습니다. **연 뒤에는 막아 주지 않으니**
꺼낸 값은 바로 쓰고 변수에 오래 들고 있지 마세요. 이 패키지는 값을 캐시하지 않습니다 —
같은 값을 두 번 부르면 두 번 요청합니다.

## 키는 두 종류입니다

| 키 | 받는 곳 | 열리는 것 |
|---|---|---|
| **개인 액세스 키** | 웹 콘솔 계정 메뉴 · Slack `/vault key` — 승인 없음 | 내가 멤버인 프로젝트의 `local` · `dev` · `staging` · `common` |
| **서비스 키** | 볼트 관리자가 발급 | 그 프로젝트의 **모든 환경**(`prod` 포함) · 구독한 공용 프로젝트 |

- **개인 액세스 키로는 `prod` 를 받지 못합니다**(본인 비공개 시크릿만 예외). 노트북의 키가
  새도 운영 서비스가 무사하도록 정한 규칙입니다. `prod` 는 배포 서버 · CI 에 둔 서비스 키로 받습니다.
- 개인 액세스 키는 **30일 동안 쓰지 않으면 만료**됩니다. 다시 받으면 이전 키는 그 자리에서
  폐기됩니다 — 잃어버렸을 때도 다시 받으면 됩니다.
- 키는 Slack 창에 **한 번만** 표시됩니다.

## 처음 한 번 — 빈 프로젝트에서 값이 올 때까지

**네 단계입니다.** 어느 하나가 빠져도 값이 오지 않고, **볼트는 어느 단계가 빠졌는지
알려주지 않습니다** — 대신 이 패키지의 오류 문안이 다음에 할 일을 알려줍니다.

**① 설치**

```bash
mkdir myproject && cd myproject
python -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
pip install chamelion-vault
```

`pip list` 에 `chamelion-vault` 하나만 늘어납니다. 딸려 오는 것이 없습니다.

**② 키** — 개인 액세스 키를 받아 환경변수에 넣습니다.

```bash
read -rs CHAMELION_VAULT_KEY && export CHAMELION_VAULT_KEY
```

> ⚠️ **키를 명령줄에 그대로 쓰지 마세요.** 셸 기록에 원문이 남습니다.
> 위 명령은 입력이 화면에 보이지 않는 것이 정상입니다.

**③ 터널** — 창을 하나 따로 열어 띄웁니다. 아래 "터널" 절을 보세요.

```bash
ssh -N vault-tunnel
```

**④ 받기**

```python
# app.py
from chamelion_vault import Vault, VaultError

try:
    value = Vault(team="myteam", project="myproject", env="dev").secret("EXAMPLE_KEY")
except VaultError as error:
    raise SystemExit(f"\n{error}")

print(value)  # <REDACTED>
```

`<REDACTED>` 가 찍히면 값이 온 것입니다. **값 자체는 찍히지 않습니다.**

## 설정 — 환경변수

| 환경변수 | 뜻 | 기본값 |
|---|---|---|
| `CHAMELION_VAULT_KEY` | API 키 (개인 액세스 · 서비스) | 없음 (필수) |
| `CHAMELION_VAULT_URL` | 게이트웨이 주소 | `http://127.0.0.1:8081` |
| `CHAMELION_VAULT_TEAM` | `Vault` 의 팀 | 없음 |
| `CHAMELION_VAULT_PROJECT` | `Vault` 의 프로젝트 | 없음 |
| `CHAMELION_VAULT_ENV` | `Vault` 의 환경 | 없음 |
| `CHAMELION_VAULT_APP` | `Vault` 의 앱 | 없음 — 프로젝트 공통만 |

**설정 파일은 지원하지 않습니다.** 파일을 읽게 만들면 거기에 키를 적게 되고, 그 파일은
언젠가 저장소에 들어갑니다. 키를 인자로 받지도 않습니다 — 같은 이유입니다.

## 터널

게이트웨이는 볼트 호스트의 **루프백에만** 열려 있습니다. 밖에서 값을 받으려면 그
루프백까지 가는 SSH 터널이 필요하고, **이 패키지는 터널을 대신 띄우지 않습니다.**
프로세스 수명·키 경로·OS 차이가 전부 따라 들어와, 실패했을 때 터널 문제인지 조회
문제인지 구분이 안 되기 때문입니다.

`~/.ssh/config` 에 한 번 적어 두면 그 뒤로는 한 줄입니다.

```
Host vault-tunnel
    HostName <볼트 호스트>
    Port <SSH 포트>
    User <계정>
    IdentityFile ~/.ssh/<터널 키>
    IdentitiesOnly yes
    LocalForward 8081 127.0.0.1:8081
```

```bash
ssh -N vault-tunnel
```

> ⚠️ **왼쪽 포트와 오른쪽 포트는 다른 것입니다.** `LocalForward 8081 127.0.0.1:8081` 에서
> 앞의 `8081` 은 내 컴퓨터에서 고른 포트이고, 뒤의 `127.0.0.1:8081` 은 볼트 호스트의
> 게이트웨이입니다. 내 컴퓨터에서 8081 을 이미 쓰고 있다면 **앞의 값만** 바꾸고
> `CHAMELION_VAULT_URL` 을 거기에 맞추세요. 뒤의 값을 바꾸면 터널이 아예 열리지 않습니다.
>
> ```
>     LocalForward 8082 127.0.0.1:8081
> ```
> ```bash
> export CHAMELION_VAULT_URL=http://127.0.0.1:8082
> ```

> ⚠️ **`IdentitiesOnly yes` 를 빠뜨리지 마세요.** 이것이 없으면 기본 키(`~/.ssh/id_*`)가
> 먼저 인증을 시도해, 터널 키에 걸어 둔 제약이 통째로 비껴갑니다. 실패 원인을 찾기
> 가장 어려운 자리입니다.

`<볼트 호스트>`·`<SSH 포트>`·`<계정>`·`<터널 키>` 는 사내 터널 문서에 있습니다.

## 값을 받지 못했을 때

볼트는 **실패 원인을 알려주지 않습니다.** 키가 틀렸는지 · 권한이 없는지 · 그런 시크릿이
없는지를 응답으로 가르면 그것이 곧 남의 볼트를 훑는 도구가 되기 때문입니다. 그래서
이 패키지도 추측하지 않고, 대신 확실히 아는 것만 말합니다.

| 예외 | 뜻 | 할 일 |
|---|---|---|
| `MissingApiKeyError` | 키 환경변수가 비었다 | 개인 액세스 키를 받는다 |
| `MissingScopeError` | 팀 · 프로젝트 · 환경 중 빈 칸이 있다 | 인자나 환경변수를 채운다 |
| `VaultUnreachableError` | 연결 자체가 안 됐다 | 터널이 떠 있는지 본다 |
| `AccessRefusedError` | 볼트가 값을 주지 않았다 | 메시지의 원인 순서대로 확인한다 |
| `FieldSelectionError` | 번들에서 필드를 고르지 않았다 | `field=` 로 고른다 |
| `UnexpectedResponseError` | 계약에 없는 응답이 왔다 | 주소가 정말 게이트웨이인지 확인한다 |

`AccessRefusedError` 의 흔한 원인 순서는 **멤버십 → 환경(개인 액세스 키로 prod) → 팀 이름 →
키 만료 → 이름 오타 → 공용 겹침 → 남의 비공개 시크릿** 입니다. 모든 예외는 `VaultError` 를 상속하므로 한 번에
잡을 수 있습니다.

## 받을 수 없는 것

- **남의 비공개 시크릿.** 소유자 본인만 받습니다. 관리자도 값을 볼 수 없습니다. 여러 사람 ·
  서버가 써야 하는 값이라면 비공개로 두면 안 됩니다.
- **다른 앱 · 다른 프로젝트의 값.** 범위 밖의 값은 응답에 들어가지 않고, 무엇이 빠졌는지도
  알려주지 않습니다. 꼭 있어야 하는 이름이 있으면 받은 뒤 직접 확인하세요.

## 0.1 방식에서 옮기기

경로로 받던 `get_secret` · `get_secrets` 는 0.3.0 에서 **제거 예정 경고**를 내고 그대로 동작합니다.

```python
# 0.1
from chamelion_vault import get_secret
get_secret("myproject", "dev", "EXAMPLE_KEY")["value"]

# 0.3
from chamelion_vault import Vault
Vault(team="myteam", project="myproject", env="dev").secret("EXAMPLE_KEY")
```

0.1 의 머신 키는 `Vault` 로 받지 못합니다. 개인 액세스 키나 서비스 키로 바꿔 주세요.
