Vaultwarden (AXE vault)
AXE 플랫폼의 모든 서비스 비밀의 canonical store (D-ops-9). frame · hive · blueprint · stream · magnet 의 DB password · OAuth client_secret · JWT signing key · 외부 API token 이 전부 여기에 산다. 서비스 컨테이너는 vault 를 직접 모르고, axe ship / axe secret pull 이 배포 직전 vault → env_file 로 동기화한 뒤 docker compose 가 그 파일을 읽는다 (D-ops-17, runtime vault 호출 없음).
동시에 Microsoft Entra ID OIDC RP 로서 직원이 SSO 로 로그인하는 password manager 이기도 하다 — 즉 운영자용 secret CLI 백엔드 + 직원용 password vault 두 역할을 한 인스턴스가 겸한다.
본 페이지는 서비스 정의 (image · compose · env · org 모델 · 비밀 흐름). 직원 setup 절차는 /onboard/vault-setup, 운영자 비밀 매니페스트 흐름은 /architecture/secrets, 기존 vault 보유 customer 통합은 /partner/deploy § Vault OIDC SSO 통합 를 참조.
한눈에
| 항목 | 값 |
|---|---|
| 역할 | Tier-1 secret store + 직원 password vault (OIDC RP) |
| image | ghcr.io/axelabs-ai/vault — Timshel fork + axe.2/axe.3 patch (아래 § 이미지) |
| 노출 | 127.0.0.1:8222 (HTTPS, Caddy) — 외부는 cloudflared 가 https://axe.axelabs.ai/vault 로 |
| 컨테이너 | axe-vaultwarden (core) + axe-vault-caddy (front, :443) |
| 위치 | /Users/axe/.axe/vault/ (operator macmini) |
| SSO | Microsoft Entra ID (Azure AD), single-tenant @axellc.com |
| 비밀 SoT | 자기 자신 — bootstrap_only: true (vault 가 자기 OAuth secret 못 읽음, chicken-and-egg) |
| 결정 | D-ops-9 · D-ops-10 · D-ops-17 · D-ops-37 · D-ops-40 |
이미지 — Timshel fork + axe.2 / axe.3 patch
upstream Vaultwarden (dani-garcia) 가 아니라 Timshel fork (native SSO 지원) 을 base 로, AXE 가 자체 patch 를 얹어 ghcr.io/axelabs-ai/vault 로 빌드한다 (D-ops-37 — fork build, D-ops-40 — axe.3 release).
- base: Timshel
1.34.1-6(commit80439605, 2025-07-15) - axe.2 patch (Bitwarden client ≥ 2026.4 unlock 호환, dani-garcia mainline 에서 backport):
/accounts/prelogin/passwordalias 추가 — 신 client 가 prelogin 을 password-only sub-endpoint 로 분리하는데 Timshel 은 legacy/accounts/prelogin만 노출 → 404 → 로그인 실패/identity/connect/token에AccountKeys/MasterPasswordUnlockwrapped-variant 필드 backport — 없으면 unlock 시Cannot read properties of null (reading toWrappedAccountCryptographicState)
- axe.3 patch (org permission quirk, 자체 진단):
3.
src/api/core/organizations.rs3곳의if member.access_all { continue; }skip 제거 — Owner 는access_all=true라 explicitusers_collectionsrow 가 안 생겨 downstream permission 체크 (cipher delete) 가 깨졌음 4.Cipher::to_json에cipher.permissions {response, delete, restore}필드 backport — client ≥ 2026.4 가 delete/restore 를 이 필드로 gate.can_delete= collection manage flag OR org Owner/Admin
axe.2 = KDF cutoff. axe.2 release (2026-05-26) 이전 가입 user 는 옛 schema (
client_kdf_type=0, PBKDF2) 라 SSO→MP unlock 이toWrappedAccountCryptographicState에러로 막혀 KDF rotation (PBKDF2 → Argon2id) 이 필요하다 (D-ops-40 runbook, axe-side 4명 ai/soohun/taehun/jinwoo). axe.2 이후 신규 가입 user (realchoice 측 등) 는 새 schema 로 가입돼 rotation 불필요. 상세 = /onboard/vault-setup § KDF rotation.
pin / rollback
| tag / digest | |
|---|---|
| 현재 (manifest list, multi-arch) | ghcr.io/axelabs-ai/vault:1.34.1-6-axe.3 @ sha256:a26208a0...55aeada |
| compose 의 실제 pin | digest 직접 (image: ghcr.io/axelabs-ai/vault@sha256:a26208a0...) — tag 가 아닌 digest 고정 |
| arm64 (axe-macmini 유효) | sha256:d97a6ed6...d4016 |
| amd64 | sha256:402eeac9...58572a |
| rollback (axe.2) | local tag axelabs-ai/vault:1.34.1-6-axe.2, ImageID sha256:11536845...858af9 |
- source build:
axelabs-ai/vaultrepo 의build/build.sh+.github/workflows/build.yml(GHA, buildx multi-arch linux/amd64 + linux/arm64), commit799015b. - GHCR auth:
ghcr.io/axelabs-ai/vault는 private. pull PAT 는 vault 의Platform — Service Secrets / ghcr-axelabs-ai-pull-pat에 보관 (값은 vault 안). docker login:bw get password ghcr-axelabs-ai-pull-pat | docker login ghcr.io -u axe-labs-ai --password-stdin - upstream PR (저우선): axe.3 patch 3+4 를 Timshel 에 제출 — accept 되면 자체 diff 소멸.
compose / 포트
/Users/axe/.axe/vault/docker-compose.yml — 2 컨테이너:
| 컨테이너 | image | 노출 | 역할 |
|---|---|---|---|
axe-vaultwarden | ghcr.io/axelabs-ai/vault@sha256:... | expose: 80 (host 직노출 X) | Vaultwarden core (API + SSO RP) |
axe-vault-caddy | caddy:2-alpine | 127.0.0.1:8222 → 443 | TLS 종단 + /vault sub-path redirect |
- core 는 host 포트를 안 받는다 —
expose: "80"만 (컨테이너 네트워크 내부). Caddy 가 유일한 진입점. - Caddy 는 127.0.0.1 에만 바인딩 (
127.0.0.1:8222:443) — Tailnet/공개 노출 X. bw CLI (운영자 mac) 가 mkcert 로컬 인증서로 신뢰, 외부 직원은 cloudflared 터널 origin 으로만 닿는다. - volume: core 는
./data:/data(sqlite + attachments), Caddy 는 named volume 2개.
Caddy — sub-path 호스팅 함정
/Users/axe/.axe/vault/Caddyfile 는 :443 에서 모든 hostname 을 받는다 (localhost = bw CLI mkcert cert, axe.axelabs.ai = cloudflared origin, Host 보존). Vaultwarden 은 DOMAIN env 가 canonical URL 을 가리키는 한 Host mismatch 를 무시한다.
핵심은 trailing-slash redirect:
@vault_no_slash { path /vault }
redir @vault_no_slash /vault/ permanent
reverse_proxy axe-vaultwarden:80Vaultwarden SPA 의 HTML 은 상대 asset 경로 (images/..., app/main.js) 를 쓴다. 사용자가 trailing slash 없는 /vault 로 들어오면 브라우저가 vault 를 파일로 보고 root 로 올라가 /app/main.js 를 요청 → cloudflared 가 default ingress (blueprint:3100) 로 보내 모든 static asset 이 502. /vault/ 강제 301 로 base directory 를 /vault/ 로 고정해야 한다. (customer 측 onboarding 함정 표에도 동일 항목 — /architecture/secrets.)
환경 변수
axe-vaultwarden.environment (값 일부는 ./.env, mode 600, gitignored 에서 치환):
| env | 값 | 이유 / 결정 |
|---|---|---|
DOMAIN | https://axe.axelabs.ai/vault | 외부 canonical URL (D-ops-10) |
SIGNUPS_ALLOWED | "true" | SSO 통과 신규 user JIT provisioning (D-ops-26) |
SSO_ONLY | "false" | master password emergency fallback 보존 — Entra 장애 시 운영자 lockout 방지 (D-ops-27) |
INVITATIONS_ALLOWED | "false" | manual invitation 미사용 (JIT 통일) |
ORGANIZATION_INVITE_AUTO_ACCEPT | "true" | 사용자측 “Accept invitation” 자동 — operator 의 “Confirm” (org key 암호화 전달) 은 여전히 필수 (D-ops-27) |
SHOW_PASSWORD_HINT | "false" | — |
LOG_LEVEL | "info" | SSO flow 이벤트 캡처용 (warn → info bump) |
SSO_ENABLED | "true" | Microsoft Entra ID RP (D-ops-10) |
SSO_AUTHORITY | https://login.microsoftonline.com/${AZURE_TENANT_ID}/v2.0 | single-tenant → 외부 user SSO 통과 불가 |
SSO_CLIENT_ID | ${AZURE_VAULTWARDEN_APP_ID} | Entra app (Vaultwarden 전용 등록) |
SSO_CLIENT_SECRET | ${AZURE_VAULTWARDEN_CLIENT_SECRET} | 값은 vault 안 (아래 § 자기 자신 비밀) |
SSO_SCOPES | openid profile offline_access User.Read | — |
SSO_SIGNUPS_MATCH_EMAIL | "true" | 기존 user 와 email 매칭 |
SSO_ALLOW_UNKNOWN_EMAIL_VERIFICATION | "true" | Entra id_token 에 email_verified claim 부재 — tenant 내부 검증 신원이라 허용. 없으면 첫 SSO 가 “Your provider does not send email verification status” 차단 (D-ops-24) |
SSO_AUTH_ONLY_NOT_SESSION | "true" | SSO 는 인증만, session lifecycle 은 Vaultwarden 자체 (30일 idle). false 면 Entra refresh token 따라가 MP 입력 잦음. trade-off: Entra 측 비활성/제명이 server session 에 ≤30일 lag — AXE 4명 규모는 수동 즉시 offboarding 으로 OK (D-ops-40) |
${AZURE_TENANT_ID}/${AZURE_VAULTWARDEN_APP_ID}/${AZURE_VAULTWARDEN_CLIENT_SECRET}는/Users/axe/.axe/vault/.env(mode 600) 에서 치환된다. customer macmini 의 같은 4-key 패치 (SSO_ALLOW_UNKNOWN_EMAIL_VERIFICATION/SIGNUPS_ALLOWED/SSO_ONLY=false/ORGANIZATION_INVITE_AUTO_ACCEPT) 는axe vault bootstrap {customer}가 in-place 적용 (D-ops-33, customer-onboarding D+1).
Organization 모델
비밀과 직원 password 는 단일 AXE organization 안에 산다 (org UUID 0c5d8bbd-ad85-42b4-8b8a-2849031981b1, axe CLI _AXE_VAULT_ORG_ID, MCP catalog org 와 동일 UUID).
dual-identity 계정 (D-ops-29)
org 운영은 customer 당 두 종류 계정으로 나뉜다:
| identity | 용도 | 권한 |
|---|---|---|
ai@{customer-domain} (axe 측 [email protected]) | 자동화 / bot — Graph token 발행, proactive DM, agent run, axe CLI 가 BW_SESSION 으로 쓰는 계정 | Owner + access_all=1 |
| 실제 admin 직원 (예: soohun.kang) | 사람 작업 identity | Owner + access_all=1 |
axe CLI 는 ai@ 계정의 BW_SESSION (macOS Keychain service=axe.vault.session, [email protected]) 으로 bw subprocess 를 돌린다. master password 는 출근 시 1회 bw unlock 후 Keychain 에 cache (8h) — 이후 모든 axe secret/axe vault 명령이 재입력 없이 동작.
⚠️ “axe.2 / axe.3” 는 org 계정 종류가 아니라 image patch release 다 (위 § 이미지). org 계정 모델은 위 dual-identity (
ai@bot + 사람 admin) 가 정답.
Collection 구조 v1 (D-ops-32)
| customer 형태 | collection |
|---|---|
| entity 1개 (예: realchoice) | 4-collection: Platform — Service Secrets (operator only) · Platform — Infrastructure (operator only) · {Customer Entity} (전직원 RW) · Shared Tools (전직원 RW) |
| entity 다수 (예: axe 의 axec/axev/axep) | 6-collection (entity별 분리) |
별도로 MCP connector 카탈로그용 MCP Connectors collection (1a62e754-6e47-43e0-a99a-cf71c37b8638) 이 있다 — axe mcp publish 가 connector 메타를 여기에 쓰고, 직원이 claude.ai 에 connector 추가 시 이 collection 에서 값을 본다 (/onboard/claude-connectors, D-vault-mcp-catalog).
org REST 자동화는 axe CLI 가 bw-derived API token 으로 직접 호출한다 — axe vault org-invite (POST /api/organizations/{org_id}/users/invite), axe secret prompt-store (org collection 에 hidden-text item 생성, 값이 shell history/clipboard 에 안 남음).
비밀 흐름 — customers.yaml secrets[] → vault → axe secret pull
핵심 invariant: 서비스 코드는 vault 를 모른다. customers.yaml 의 services.<svc>.secrets[] 매니페스트가 env-var ↔ vault-item 매핑의 SSOT 이고, axe secret 명령들이 그 사이를 잇는다.
customers.yaml Vaultwarden (org) service env_file
services.<svc>.secrets[] ai@ 계정, bw CLI <svc>/.env (mode 600)
- env: FRAME_DB_PASSWORD ──push──▶ item "frame/axe/db-password" ──pull──▶ FRAME_DB_PASSWORD=...
vault: frame/axe/db-password (password 필드) (docker compose env_file)| 명령 | 동작 |
|---|---|
axe secret push <ENV> --service <svc> [--customer C] | 매니페스트로 vault 경로 lookup → 신규 item 생성 or password 필드 갱신. 대형/PEM 비밀은 --value-stdin (값이 argv/ps 에 안 뜸) |
axe secret pull <svc> [--customer C] | 매니페스트의 각 secrets[] 를 bw get → env_file 에 merge (manifest 키만 덮어쓰고 나머지 config·comment·hand-edit 라인은 보존). 따옴표 없이 raw single-line 으로 write (env_file 은 literal-quote) |
axe secret check <svc> | 매니페스트 vs vault 보유 비교 → 누락 출력. axe ship 가 배포 gate 로 사용 |
axe secret send <ENV> --service <svc> [--to R] | vault GET → bw send 1회용 링크 (기본 -d 1 -a 1 --hidden) → URL stdout. 사람 전달 전용 (직원에게 client_secret 줄 때) |
axe secret rotate <ENV> --service <svc> | 새 값 (외부 provider 입력 or 자동 생성) → vault PUT → pull → ship 트리거 |
axe secret get <ENV> [-n] / axe secret list | 단건 조회 / 전체 item 표 (list 는 값 출력 X) |
vault item 네이밍 컨벤션: <service>/<customer>/<short> (예: frame/axe/db-password, blueprint/realchoice/azure-client-secret). 상세 + 전체 매니페스트 (현재 42개 비밀 등재) = /architecture/secrets.
자기 자신 비밀 — bootstrap_only
vault 의 자기 OAuth client_secret (AZURE_VAULTWARDEN_CLIENT_SECRET) 은 vault 자신이 자기를 못 읽는다 (vault 가 부팅돼야 secret 을 줄 수 있는데, 부팅에 그 secret 이 필요한 chicken-and-egg). 그래서 customers.yaml 에서 vaultwarden service 는 bootstrap_only: true — axe secret check/pull 이 skip 하고, /Users/axe/.axe/vault/.env 가 직접 그 값을 들고 있다 (vault 매니페스트엔 “manually maintained — vault is the SoT but can’t read itself” 주석).
즉 vaultwarden 의 SSO secret 1개만 매니페스트상 self-referential 예외이고, 나머지 모든 서비스 비밀은 정상 push/pull 경로를 탄다.
백업
vault 의 ./data (sqlite + attachments) 는 AXE 3-tier backup (local + ring + cold SSD, restic) 에 포함된다. 복구 절차는 vault master password 로 axe-vaultwarden 잠금 해제 후 data 복원 — 상세 = /architecture/backup.
함정
| 함정 | 결과 | 회피 |
|---|---|---|
Caddyfile /vault no trailing slash | SPA 상대 asset 이 root 옆 resolve → asset 502/503 | /vault → /vault/ permanent redirect (Caddyfile 기본 적용) |
Timshel image wget/curl 가정 오류 | compose healthcheck 실패 → unhealthy 무한 루프 | image 에 실제 존재하는 바이너리로 healthcheck (Timshel 은 wget 없음 → curl) |
| bw CLI 2026.4+ ↔ Timshel SSO 호환 | unlock 시 TypeError: ... toWrappedAccountCryptographicState | bw CLI 2025.7.0 pin (npm install -g @bitwarden/[email protected], D-ops-28) 또는 axe.2+ patch image |
| axe.2 이전 가입자 KDF | SSO→MP unlock 동일 에러 | KDF rotation PBKDF2 → Argon2id (D-ops-40, vault-setup) |
SSO_ONLY=true 설정 | Entra 장애 시 운영자 lockout (MP 진입 불가) | SSO_ONLY: "false" 유지 (D-ops-27) |
SIGNUPS_ALLOWED=false + INVITATIONS_ALLOWED=false | 첫 employee JIT 가입 “Failed to retrieve the invitation” | SIGNUPS_ALLOWED: "true" (D-ops-26) |
docker compose restart 만 함 | env 변경 미반영 (restart 는 env reload 안 함) | docker compose up -d --force-recreate axe-vaultwarden |
ghcr.io/axelabs-ai/vault private pull 실패 | image pull denied | ghcr-axelabs-ai-pull-pat 로 docker login ghcr.io 선행 |
참고
- 운영자 비밀 매니페스트 + axe secret 명령: /architecture/secrets (D-ops-17)
- 직원 vault setup (SSO/KDF/3-client): /onboard/vault-setup
- 기존 vault 보유 customer 통합: /partner/deploy § Vault OIDC SSO 통합
- customer 측 vault bootstrap: /ops/runbook/customer-onboarding § D+1
- 결정: D-ops-9 · D-ops-10 · D-ops-17 · D-ops-24 · D-ops-26 · D-ops-27 · D-ops-28 · D-ops-32 · D-ops-33 · D-ops-37 · D-ops-40
- 소스:
/Users/axe/.axe/vault/{docker-compose.yml, Caddyfile, .env}· build:axelabs-ai/vaultrepo