<!-- canonical: https://docs.axelabs.ai/ops/runbook/vault-recovery -->
<!-- source: content/ops/runbook/vault-recovery.mdx -->

---
title: Vaultwarden 복구
description: self-host vault 복구, OIDC 깨짐 대응, sso_nonce 수동 패치.
playbook: true
---

# Vaultwarden 복구

## AI 요청 프롬프트

```
https://docs.axelabs.ai/ops/runbook/vault-recovery 따라 axe-vaultwarden 복구 진행해줘.

진행:
1. 현재 증상 진단 — docker ps + curl /identity/.well-known/openid-configuration + 사용자 보고로 시나리오 1~6 중 어느 분기인지 식별
2. 시나리오 식별 후 추가 분기 확인 (PostgreSQL vs SQLite backend / Timshel fork vs mainline / customer 측 axe.axelabs.ai vs realchoice 측)
3. 페이지의 각 명령 실행 + 검증, 매 step 결과 받고 다음. backup 또는 force-recreate 등 destructive 명령 직전 사용자 확인
4. 함정 발생 시 페이지 "함정" 표 따라 우회 (column 명 verifier vs code_verifier / digest pin / 시나리오 6 = 영구 손실 — 복구 불가)
5. 복구 완료 검증 (SSO 로그인 + vault item 조회 + admin endpoint) + 사고 원인 /ops/known-gaps 한 줄 (재발 방지)
```

본인 AI session = Claude Code / Cursor / ChatGPT 데스크탑 / Claude.app / 기타.

페이지 본문 = 사람이 직접 read 도 가능, AI 도 참고. AI 가 본 페이지 fetch 후 위 진행 순서대로 사용자와 step-by-step interactive 풀어나감.

`axe-vaultwarden` 서비스가 죽거나 OIDC 가 깨졌을 때.

## 시나리오 1 — 컨테이너 죽음

```bash
docker ps -a | grep vault
# axe-vaultwarden Exited (1)

# 단순 재시작
cd /Users/axe/.axe/vault
docker compose up -d --force-recreate
sleep 5
curl -sk https://localhost:8222/identity/.well-known/openid-configuration | head -5
```

설정 정상이면 정상 가동.

## 시나리오 2 — OIDC 깨짐 (sso_nonce 누락)

D-ops-12 에서 다룬 알려진 이슈. Timshel fork 가 sso_nonce 테이블을 알아서 생성 안 함. **2026-05-25 정정** (Truvia realchoice 검증) — Timshel fork 의 실제 column 명 = `verifier` (NOT `code_verifier`). PostgreSQL / SQLite backend 분기 명령 별도.

증상:
```
ERROR: relation "sso_nonce" does not exist   (PostgreSQL)
또는
sqlite> .schema sso_nonce → empty                (SQLite)
```

### PostgreSQL backend (axec axe-vaultwarden 패턴)

```bash
docker exec axe-vaultwarden-postgres psql -U vaultwarden -d vaultwarden -c "
CREATE TABLE IF NOT EXISTS sso_nonce (
    state TEXT PRIMARY KEY,
    nonce TEXT NOT NULL,
    verifier TEXT NOT NULL,
    redirect_uri TEXT NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
"

docker compose restart axe-vaultwarden
```

### SQLite backend (`soohunkang/vault` repo + Vaultwarden 공식 image, 예: realchoice 의 ~/vault)

```bash
# vault-app 컨테이너 안의 sqlite3 (또는 host 측에서 docker exec)
docker exec vault-app sqlite3 /data/db.sqlite3 <<'EOF'
CREATE TABLE IF NOT EXISTS sso_nonce (
    state TEXT PRIMARY KEY,
    nonce TEXT NOT NULL,
    verifier TEXT NOT NULL,
    redirect_uri TEXT NOT NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
EOF

# 만약 위 schema 작성 후 column 명 mismatch 로 fail 보이면 (이전에 잘못된 column 이름으로 만든 경우):
docker exec vault-app sqlite3 /data/db.sqlite3 "ALTER TABLE sso_nonce RENAME COLUMN code_verifier TO verifier;"

docker compose restart vault-app
```

(db.sqlite3 경로는 `DATA_FOLDER` env 또는 `/data` volume mount 기준. compose.yaml 확인 후 적용.)

## 시나리오 3 — Microsoft SSO 갑자기 안 됨

원인 후보:
- Vaultwarden app 의 client_secret 만료
- redirect_uri 등록 잘못됨
- AZURE_TENANT_ID 또는 AZURE_VAULTWARDEN_APP_ID 잘못 설정

확인:
```bash
# .env 검증
cat /Users/axe/.axe/vault/.env | grep -E 'AZURE_(TENANT_ID|VAULTWARDEN)'

# 컨테이너 env 검증
docker exec axe-vaultwarden env | grep -E 'SSO_(AUTHORITY|CLIENT)'
```

해결:
1. customer IT 측 Azure 에서 새 client_secret 발급
2. `.env` 의 `AZURE_VAULTWARDEN_CLIENT_SECRET=` 새 값으로
3. `docker compose up -d --force-recreate`

## 시나리오 4 — 컨테이너 손상 / 데이터 손실

```bash
# 1. backup 확인
restic -r /Users/axe/.axe/backups/local snapshots \
    --password-file &lt;(security find-generic-password -w -s axe.backup.restic.local -a axe-cli) \
    --tag vault

# 2. 가장 신선한 vault snapshot 복원
# `axe restore` 는 Phase 5 stub — 현재는 restic 직접 + docker volume restore:
restic -r /Users/axe/.axe/backups/local \
  --password-file &lt;(security find-generic-password -w -s axe.backup.restic.local -a axe-cli) \
  restore &lt;snapshot_id&gt; --target /tmp/vault-restore --include '/Users/axe/.axe/vault/*'
# 그 후 docker volume 또는 .yml volume mount 위치에 복사

# 3. 컨테이너 재시작
cd /Users/axe/.axe/vault
docker compose up -d --force-recreate

# 4. Microsoft SSO 시도
open https://axe.axelabs.ai/vault
```

## 시나리오 5 — Timshel fork 업데이트

Timshel fork 의 새 release 가 나왔을 때.

⚠️ **신중하게**. Timshel 은 mainline Vaultwarden 보다 작은 PR queue → 가끔 breaking change.

```bash
# 1. 현재 digest pin 확인
grep ghcr.io /Users/axe/.axe/vault/docker-compose.yml

# 2. 새 digest 확인 (Timshel 의 GitHub Releases)
# https://github.com/Timshel/vaultwarden/releases

# 3. 비프로덕션 환경에서 테스트 (있다면)

# 4. backup 전체 (vault + DB)
# 수동 backup (현재 `axe backup` subcommand 없음 → restic 직접):
restic -r /Users/axe/.axe/backups/local \
  --password-file &lt;(security find-generic-password -w -s axe.backup.restic.local -a axe-cli) \
  backup /Users/axe/.axe/vault --tag vault-pre-upgrade

# 5. .yml 의 image: line 갱신
vim /Users/axe/.axe/vault/docker-compose.yml
# image: ghcr.io/timshel/vaultwarden@sha256:&lt;new&gt;

# 6. 재시작
docker compose up -d --force-recreate

# 7. 검증
# - admin 로그인
# - Microsoft SSO 로그인
# - 기존 collection 의 item 조회
# - 새 item 생성/조회

# 8. 실패 시 rollback
docker compose down
# .yml 의 image: 옛 digest 로 되돌림
docker compose up -d
```

## bw CLI data.json cache stale recovery (운영자 측)

서버 측 복구가 아닌 **운영자 본인 머신의 bw CLI** 회복 절차. server-side vault patch deploy 후 local `data.json` cache 가 stale 상태로 잔존하면서 발생.

**증상**:
```
[Encrypt service] MAC comparison failed
```
`bw unlock` 또는 `bw get <name>` 시 위 에러 → bw 명령 전체 실패.

**언제 발생**: server-side vault patch deploy 후 (axe.2 → axe.3 등). bw CLI 의 local `data.json` 에 `cryptoSymmetricKey` cache 가 옛 schema 의 wrapped key 보유 → 새 server 가 내려준 패치 후 shape 와 mismatch → decrypt 시 MAC mismatch. `bw unlock` 자체는 cache 무효화 안 함.

**빈도**: server patch 마다 1회 — 2026-05-22, 2026-05-26 두 차례 재발 확정. patch shape 변경 시 매번.

**표준 회복 절차** (운영자 본인 머신, 약 30초):

```bash
bw logout
bw config server https://axe.axelabs.ai/vault
bw login   # email + MP + Entra MFA (interactive)
bw unlock  # MP — session token stdout
export BW_SESSION="...token..."
bw get password <known-secret>  # 검증
```

이후 Keychain 의 `axe.vault.session` entry 갱신:

```bash
bw unlock --raw | security add-generic-password -s 'axe.vault.session' -a "$(whoami)" -w "$(cat)" -U
```

**자동화 후보** (영구 fix, [B-bw-cache-stale-autoheal](/ops/backlog)):

- `axe vault reset` 신설 — 위 4단계를 한 명령으로
- `_bw_get_password` 헬퍼가 `MAC comparison failed` 패턴 감지 시 자동 reset + retry
- `axe ship vault` post-deploy hook 이 patch shape 변경 시 osascript 알림 — 운영자 본인에게 "logout/login 필요" pre-warn

**함정**:

| 함정 | 결과 | 회피 |
|---|---|---|
| ai@ personal vault 가 약 37 items → fresh login 시 sync 약 30초 대기 | `bw login` 직후 `bw get` 호출 시 일부 item 미반영 | login 직후 `bw sync` 명시 + 약 30초 대기 |
| `bw login` 시 `BW_CLIENTID` / `BW_CLIENTSECRET` env 잔존 시 API key 모드로 진입 | SSO 흐름과 다른 인증 경로 — 일반 사용 OK 단 본 회복 절차는 interactive 가 가장 안전 | 회복 시 `unset BW_CLIENTID BW_CLIENTSECRET` 후 `bw login` |
| `~/.config/Bitwarden CLI/` (또는 `$BITWARDENCLI_APPDATA_DIR`) 의 `data.json` 직접 삭제 시도 | clean state 안 됨 + session token 잔존 | `bw logout` 권장 (data.json + session 모두 정리) |
| Keychain 의 `axe.vault.session` 옛 token 잔존 | `axe secret *` 호출이 옛 invalid session 으로 NO_SESSION_IN_KEYCHAIN | login 후 위 `security add-generic-password ... -U` 로 갱신 필수 |

## 시나리오 6 — 운영자 master password 분실

⚠️ **이건 영구 손실**. Vaultwarden 의 master password 는 vault 의 encryption key. 모르면 모든 item 복호화 불가.

회피:
1. master password 는 종이 메모 + 신뢰할 수 있는 가족 (또는 다른 안전한 곳)
2. 매월 1회 master password 로 로그인 시도 (잊지 않게)
3. Bitwarden export 정기 + 그 export 파일 자체도 vault 의 master password 로 암호화

분실 시 — vault 폐기, 모든 item 재발급. 회사 운영 1주일 이상 마비 가능. 그래서 종이 백업 필수.

## 함정

| 함정 | 결과 | 회피 |
|---|---|---|
| Timshel digest pin 안 함 (`:latest`) | 무작위 업데이트 | 항상 `@sha256:&lt;digest&gt;` |
| backup 안 함 | sso_nonce 깨졌을 때 복구 불가 | 매일 자동 backup |
| .env 평문 commit | secret 노출 | .gitignore + Keychain |
| master password 단일 보관 | 분실 시 영구 손실 | 종이 + 가족 이중화 |
| 분기 drill 안 함 | restore 절차 실제 검증 안 됨 | `com.axe.restore-drill` 자동화 |
