<!-- canonical: https://docs.axelabs.ai/architecture/vault-policies -->
<!-- source: content/architecture/vault-policies.mdx -->

---
title: Vault 세션/정책 모델 (3 layer)
description: AXE Vaultwarden 의 session lifecycle + 권한 정책 + per-user unlock 패턴. 운영자가 통제 가능한 layer 와 user 자율 layer 의 구분. 새 직원/customer 의 표준 setup.
---

# Vault 세션/정책 모델 (3 layer)

AXE Vaultwarden (Timshel fork 기반, [D-ops-37](/ops/decisions) + [D-ops-40](/ops/decisions)) 의 session/unlock 정책은 **3 layer 누적**으로 결정. 각 layer 가 누가 통제하는지 + 변경 비용이 다름.

| Layer | 통제 누가 | 영향 범위 | 변경 비용 |
|---|---|---|---|
| **1. Server env** (Vaultwarden config) | 운영자 (compose env_file) | 모든 user, 모든 client | container restart 1회 |
| **2. Org Policies** (Bitwarden 호환 정책) | Org Owner (web UI 또는 API) | 해당 org member 전원, 모든 client | 즉시 (next login 적용) |
| **3. Per-client preferences** (localStorage) | 각 user 본인 (각 device/client 마다) | 본인의 그 client 만 | 본인 30초 |

3 layer 가 **누적 적용** — 각 layer 가 더 paranoia 방향으로 강제하고, 마지막 user 자율 layer 가 ceiling 안에서 편의 조정.

## Layer 1 — Server env

Vaultwarden config 의 env var. `/Users/axe/.axe/vault/docker-compose.yml` 의 `environment:` 블록.

### 핵심: `SSO_AUTH_ONLY_NOT_SESSION`

| 값 | 효과 |
|---|---|
| `false` (default) | SSO 의 refresh token 따라감 — Entra ID 의 token 만료 시점에 자동 logout → re-SSO+MP 강제. user 입력 빈도 ↑ |
| **`true` (AXE 현재 설정, 2026-05-26)** | SSO 가 인증만, session lifecycle 은 Vaultwarden 자체 **30일 idle refresh token** 사용. user 가 30일 안 idle 안 하면 무한 갱신 |

**적용 효과** (AXE 4 user):
- SSO+MP 재입력 = 약 월 1회 (idle 30일 + browser 재시작 시)
- Touch ID/PIN unlock 은 daily

**trade-off**:
- Entra 측 user 비활성/제명이 server session 에 ≤30일 lag
- offboarding 시 운영자가 수동으로 vault 측에서도 user 비활성 (UI: AXE org → Manage → Members → user → Remove + admin panel 에서 user disable) — AXE 4명 규모 충분 처리 가능
- customer 규모 ↑ 시 재평가 (별 결정)

### 다른 관련 env

| env | 효과 |
|---|---|
| `SSO_ONLY=false` | MP 단독 fallback 유지 ([D-ops-26](/ops/decisions) + [D-ops-27](/ops/decisions)) |
| `SIGNUPS_ALLOWED=true` | JIT user provisioning (SSO 신규 user 자동 생성) |
| `SSO_SCOPES="openid profile offline_access User.Read"` | offline_access = refresh token 발급 |
| `ORGANIZATION_INVITE_AUTO_ACCEPT=true` | invite Accept 단계 자동 통과 |

**Vaultwarden 미지원 (의도)**:
- ❌ Maximum Vault Timeout — Bitwarden commercial license, AGPLv3 비호환 ([source comment](https://github.com/Timshel/vaultwarden/blob/main/src/db/models/org_policy.rs): `// MaximumVaultTimeout = 9, // Not supported (Not AGPLv3 Licensed)`)
- ❌ Disable Personal Vault Export — 동일 license 사유
- ❌ Activate Autofill, Automatic App LogIn — Vaultwarden 미구현

→ vault timeout 의 **server-side 강제는 영구 영역 외**. `SSO_AUTH_ONLY_NOT_SESSION=true` 가 가용 영역 안 best 대체.

## Layer 2 — Org Policies (Bitwarden 호환)

Org Owner 가 `/api/organizations/<orgId>/policies/<type>` ([Bitwarden 정책 docs](https://bitwarden.com/help/policies/)) 또는 web UI 로 설정. Vaultwarden 의 [supported set](https://github.com/Timshel/vaultwarden/blob/main/src/db/models/org_policy.rs#L26) 안에서.

### 현재 AXE org 적용 (2026-05-26, [D-ops-40](/ops/decisions) Progress)

| type | 정책 | 값 | 효과 |
|---|---|---|---|
| 1 | MasterPassword | min 12자, complexity 1 | MP **변경 시점** 검증. 기존 MP 영향 0 (paranoia 옵션) |
| 2 | PasswordGenerator | length 20, upper+lower+number+special | item 신규 생성 시 강한 default |
| 3 | SingleOrg | enabled | member 가 다른 org 가입 차단 → 데이터 누설 방지 |
| 5 | PersonalOwnership | enabled | 신규 item 은 org collection 필수 ([D-ops-32](/ops/decisions) 본질 정합) |

**의도적 미설정** (D-ops-38 + D-ops-26+27):

| type | 정책 | 왜 안 켜나 |
|---|---|---|
| 0 | TwoFactorAuthentication | Entra SSO 가 이미 MFA 역할 ([D-ops-38](/ops/decisions)) — vault 자체 2FA 중복 |
| 8 | ResetPassword | 운영자 직권 reset 보존 (default), 강제 policy 불필요 |
| 14 | RemoveUnlockWithPin | PIN unlock = 일상 편의 핵심, 차단하면 user 경험 악화 |

### Vaultwarden 미지원 (Bitwarden Cloud 에서는 가능):

| type | 정책 | 사유 |
|---|---|---|
| 4 | RequireSso | Vaultwarden 미구현 (의도 — MP 단독 fallback 보존 정책과 정합) |
| 9 | MaximumVaultTimeout | AGPL 비호환 |
| 10 | DisablePersonalVaultExport | AGPL 비호환 |

### API 호출 패턴

```bash
# AXE org policy 조회
export BW_SESSION="$(security find-generic-password -s 'axe.vault.session' -w)"
AI_USER=0ef2361b-5a69-4c23-973d-e978a4d55512  # ai@
ORG=0c5d8bbd-ad85-42b4-8b8a-2849031981b1     # AXE
TOKEN=$(jq -r ".\"user_${AI_USER}_token_accessToken\"" \
  "$HOME/Library/Application Support/Bitwarden CLI/data.json")

curl -sf -H "Authorization: Bearer $TOKEN" \
  "https://axe.axelabs.ai/vault/api/organizations/$ORG/policies" \
  | jq '.data | map({type, enabled})'

# 정책 변경 (예: MasterPassword 정책 수정)
curl -sf -X PUT \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  "https://axe.axelabs.ai/vault/api/organizations/$ORG/policies/1" \
  -d '{"type":1,"enabled":true,"data":{"minComplexity":2,"minLength":14,"requireUpper":false,"requireLower":false,"requireNumbers":false,"requireSpecial":false,"enforceOnLogin":false}}'
```

token 만료 시 (~1h) `bw sync` 로 refresh.

## Layer 3 — Per-client preferences

vault timeout / PIN / Touch ID / theme = **각 device 의 client localStorage**. server 가 못 건드림 (Bitwarden 디자인 — privacy by design, prefs 가 user device 안). 정책 부재 시 각 client 의 첫 사용에서 user 가 선택.

### AXE 표준 setup (KDF rotation 완료 후 권장)

**4 user 모두 + 모든 새 직원** 다음 3 client 설정:

#### A. Web vault (보조용, https://axe.axelabs.ai/vault)

| field | 권장 |
|---|---|
| Vault timeout | `On browser refresh` (web vault 의 최장 옵션 — `Never` 노출 안 됨) |
| Vault timeout action | `Lock` (Logout 절대 X — SSO 재진입 비용) |

설정 위치: 우상단 본인 이름 → Account settings → Preferences

#### B. Chrome extension (일상 brower-bound)

| field | 권장 |
|---|---|
| Server URL (self-hosted) | `https://axe.axelabs.ai/vault` |
| Vault timeout | **`Never`** (extension 은 `Never` 노출됨) |
| Vault timeout action | `Lock` |
| Unlock with PIN code | ✅ + PIN 4~12자 (e.g. `axe-2026` word) |
| Lock with master password on browser restart | ✅ (paranoia 균형) |

#### C. Native macOS app (daily driver — 가장 편함)

| field | 권장 |
|---|---|
| Server URL (self-hosted) | `https://axe.axelabs.ai/vault` |
| Vault timeout | `Never` |
| Vault timeout action | `Lock` |
| Unlock with Touch ID | ✅ (macOS Secure Enclave) |
| Lock with master password on browser restart | ✅ |

**실 효과** (3 layer 누적):
- daily unlock = Touch ID (수 초)
- 주 1-2회 = PIN (Chrome ext)
- 월 1회 (idle 30일 또는 paranoia 재시작) = MP

## Vault scope — 무엇을 보호하는가 (trust boundary)

Vault 는 **secret 의 저장 + retrieval + access control** 만 담당하는 저장소. vault item 을 delete/revoke 했다고 해서 그 secret 이 의미하는 **외부 service 의 권한·계정·2FA 가 자동으로 무효화되지 않음**. 두 영역을 혼동하면 offboarding/사고 대응 시 access 잔존 위험.

### 경계 도식

```
┌─ Vault scope (이 페이지 + Vaultwarden 이 통제) ──────────┐
│   • secret 값의 저장 / 암호화 (Argon2id + KDF)            │
│   • org collection 기반 access control (Layer 2)        │
│   • SSO + MP unlock lifecycle (Layer 1, 3)              │
│   • item delete/revoke → 그 저장 위치 에서 차단           │
└──────────────────────────────────────────────────────────┘
            ↓ vault item 은 외부 service 의 "거울" 일 뿐
┌─ 외부 service scope (vault 가 못 건드림) ─────────────────┐
│   • 해당 service 측 계정 자체 (Microsoft / Google / ...) │
│   • TOTP secret 의 generation (해당 service 가 발급)     │
│   • OAuth client_secret / refresh token invalidation    │
│   • GitHub PAT, API key 의 server 측 revoke             │
│   • customer 측 데이터 자체 (KB / 홈택스 export 등)       │
└──────────────────────────────────────────────────────────┘
```

→ **회수 범위 = vault item delete + 외부 service 측 절차**. 둘 다 해야 완전.

### 회수 대상별 분리표

| 회수 대상 | vault 가 처리 | 외부 service 측 절차 필요 |
|---|---|---|
| service password 저장값 | ✅ item delete/revoke | 해당 service 의 password reset (admin 또는 본인) |
| TOTP secret (2FA) | ✅ item delete | 해당 service 의 2FA reset → TOTP secret regenerate |
| OAuth client_secret | ✅ item delete | Azure / Google / GitHub 측에서 client_secret 재발급 + 옛값 폐기 |
| GitHub PAT | ✅ item delete | GitHub Settings → Personal access tokens → Revoke (server 측 token row 무효화) |
| API key (Anthropic / OpenAI / etc) | ✅ item delete | 해당 provider 의 console 에서 key revoke |
| KB / 홈택스 / 회계법인 portal admin 계정 | ❌ vault scope 외 (계정 자체는 vault 가 보관 안 함) | 해당 portal admin 의 계정 비활성/삭제 |
| Microsoft 365 / Entra ID 계정 | ❌ vault scope 외 | customer IT 가 Entra ID 에서 user 비활성 |
| customer 측 데이터 (Excel, PDF 등 export 본) | ❌ vault scope 외 | customer 자체 DLP / device wipe / 법적 요구 |

### 함의

- **offboarding 시**: [/ops/runbook/employee-offboarding](/ops/runbook/employee-offboarding) 의 "외부 service 권한 회수" 표를 vault item 제거와 **반드시 병행**.
- **secret rotation 시**: vault item 값을 새로 채우는 것 만으로는 옛값이 외부 service 측에서 살아있음 — [/ops/runbook/secret-rotation](/ops/runbook/secret-rotation) 의 외부 service rotation step 동반.
- **사고 대응 시**: vault item delete = "이 운영자 머신/AXE org 안에서 더 못 꺼냄". 그 secret 이 leak 된 정황이면 외부 service 측 revoke 가 본질.

## 3 layer 누적 그림

```
┌──────────────────────────────────────────────────────────┐
│ Layer 3: Per-client preferences (user 자율)              │
│   • web `On browser refresh` Lock                       │
│   • extension `Never` + PIN                             │
│   • native `Never` + Touch ID                           │
│   ↓ 위 layer 의 ceiling 안에서 user 편의 조정             │
├──────────────────────────────────────────────────────────┤
│ Layer 2: Org Policies (Owner 통제, member 전원 적용)      │
│   • MasterPassword min 12, complexity 1                 │
│   • PasswordGenerator length 20                         │
│   • SingleOrg                                            │
│   • PersonalOwnership                                    │
│   ↓ 모든 member 자동                                      │
├──────────────────────────────────────────────────────────┤
│ Layer 1: Server env (운영자 통제, 전 instance)            │
│   • SSO_AUTH_ONLY_NOT_SESSION=true → 30일 session       │
│   • SSO_ONLY=false → MP fallback                        │
│   • SIGNUPS_ALLOWED=true → JIT                          │
│   ↓ 인스턴스 기본값                                       │
└──────────────────────────────────────────────────────────┘
```

## 새 직원 onboarding 의무 step

1. **KDF rotation** (1회) — [B-vault-axe.2-sso-mp-incomplete](/ops/backlog) runbook
   - 옛 PBKDF2 → Argon2id 로 vault re-encrypt
   - SSO→MP wrapped variant 정합 ([D-ops-40](/ops/decisions))
2. **Layer 3 setup** (1회 per device) — 위 Web/Extension/Native 표 그대로
3. 끝 — 이후 일상은 Touch ID/PIN 만

운영자가 1번 안내 1회 + 2번 표 공유 1회 = onboarding 끝. layer 1+2 는 server 가 자동.

## Customer (Truvia 등) — sovereignty

| Layer | customer 자율도 |
|---|---|
| Layer 1 (server env) | **customer 자체 통제** — customer 의 macmini compose, customer 자율. AXE 가 권장만 (이 페이지 참조 안내) |
| Layer 2 (org policies) | **customer org Owner 가 설정** — AXE 가 정책 enforce 불가. 권장 (위 4 정책 동일) |
| Layer 3 (per-client) | customer 직원 본인 — 동일 setup 권장 |

→ customer 운영자에게 본 페이지 link 만 전달, 권장 follow. [/services/vault](#) 의 customer onboarding 단계에 vault setup 포함 (TODO).

## 함정

| 함정 | 결과 | 회피 |
|---|---|---|
| Maximum Vault Timeout 정책 시도 (Bitwarden 패턴) | HTTP 400 "Invalid or unsupported policy type" | Vaultwarden 미지원 (AGPL). 대신 `SSO_AUTH_ONLY_NOT_SESSION=true` 사용 |
| `SSO_ONLY=true` + Entra ID 장애 | 운영자 vault 진입 불가 (lockout) | `SSO_ONLY=false` 유지 ([D-ops-26](/ops/decisions) + [D-ops-27](/ops/decisions)) — MP fallback 보존 |
| RemoveUnlockWithPin 정책 enable | PIN unlock 불가 → 일상 MP 입력 강제 | 본 정책 enable 금지 (paranoia 와 편의 균형) |
| 옛 KDF (PBKDF2) user 가 SSO→MP unlock 실패 | client 가 wrapped MP 변환 불가 | user 별 KDF rotation 의무 (onboarding step 1) |
| Org policy 변경이 즉시 client 에 반영 안 되는 듯 | client 가 sync 시점에 fetch | bw sync 또는 client 강제 sync (일반적 ≤1분 안 자동) |
| 운영자가 Layer 3 강제 시도 (Chrome managed policy 등) | macmini 별 enrollment 필요 — overhead 큼 | AXE 4명 규모 = docs 안내 + 신뢰. 10+ 직원 시점에 재평가 ([B-vault-chrome-mdm-policy-distribution](#)) |
| Entra user 비활성 후 ≤30일 server session 잔존 | offboard 직후 vault 접근 가능 | offboard 시 운영자가 vault admin panel 에서 user disable 같이 실행 (manual checklist) |

## 관련

- [D-ops-37](/ops/decisions) — AXE-local Vaultwarden fork build
- [D-ops-40](/ops/decisions) — axe.3 release plan + Progress (본 정책 결정)
- [D-ops-38](/ops/decisions) — 외부 service 2FA = vault TOTP, 내부 2FA skip
- [D-ops-26](/ops/decisions) + [D-ops-27](/ops/decisions) — SSO_ONLY=false + MP fallback
- [/architecture/auth](/architecture/auth) — Entra ID + OAuth-RP middleware
- [/architecture/secrets](/architecture/secrets) — 서비스 secret 관리 (vault item)
- [B-vault-axe.2-sso-mp-incomplete](/ops/backlog) — KDF rotation runbook
- [B-vault-collection-migration-v1](/ops/backlog) — personal vault → org collection 정리
