Vaultwarden 백업 및 복구 방법: VPS 데이터 안전하게 보관하기
sqlite3 .backup 명령어를 사용하여 데이터베이스를 안전하게 복사하고, attachments와 config.json 등 필수 파일을 포함한 전체 백업 절차를 설명합니다. 실제 복구 테스트를 통해 데이터 손실을 방지하는 검증된 방법을 확인하십시오.
Vaultwarden 백업에 포함되어야 할 항목
Vaultwarden 백업은 전체 데이터 폴더의 복사본이어야 하며, 그 내부의 데이터베이스는 올바른 방식으로 복사해야 합니다. cp 대신 sqlite3 db.sqlite3 ".backup out.sqlite3"을 실행하십시오. 데이터베이스에 쓰기 작업이 진행 중일 때 단순 복사를 수행하면 파일을 열 수 없게 될 수 있기 때문입니다. 그다음, 함께 저장된 파일들도 보관해야 합니다. 많은 사람이 이 부분을 간과합니다.
Docker 설치 환경에서 데이터 폴더는 /data에 마운트한 경로입니다. 이는 호스트의 경로이거나 네임드 볼륨(named volume)이며, 바인드 마운트와 네임드 볼륨의 차이에 따라 실제 볼트 데이터가 디스크의 어디에 위치하는지가 결정됩니다. 데이터 폴더에는 다음 항목이 포함됩니다.
db.sqlite3: 모든 계정, 모든 볼트 항목, 모든 폴더 및 모든 조직 정보입니다. 이 파일을 잃어버리면 볼트 데이터도 손실됩니다.db.sqlite3-wal및db.sqlite3-shm: 쓰기 전 로그(WAL)와 공유 메모리 인덱스입니다. SQLite가 메인 파일에 병합하기 전까지 최근의 쓰기 작업이 이곳에 저장됩니다.attachments/: 사용자가 볼트 항목에 첨부한 파일들입니다. 항목별로 하나의 디렉터리에 암호화되어 저장됩니다.sends/: Bitwarden Send 링크를 통해 공유된 파일들입니다.config.json: 관리자 페이지에서 저장한 모든 설정값입니다.rsa_key.pem, 그리고 구버전 설치 시의rsa_key.der및rsa_key.pub.der: 로그인 토큰을 서명하는 키입니다.icon_cache/: 다운로드된 웹사이트 아이콘입니다. 이 디렉터리는 건너뛰어도 됩니다. Vaultwarden이 필요할 때 다시 가져오기 때문입니다.
Vaultwarden 데이터베이스는 안전한가? 파일에 실제로 저장되는 내용
두 가지 명령어로 이를 확인할 수 있으며, 지금 바로 실행해 볼 수 있습니다.
sudo apt update && sudo apt install -y sqlite3
sudo sqlite3 /opt/vaultwarden/data/db.sqlite3 "select email from users;"
sudo sqlite3 /opt/vaultwarden/data/db.sqlite3 "select name from ciphers limit 1;"첫 번째 명령어는 사용자의 이메일 주소를 평문으로 출력합니다. 두 번째 명령어는 항목 이름 하나를 출력하며, 그 형태는 다음과 같습니다.
2.k9Qw1nQ0y7Yy2Xw==|E1r0J3l5s7d9f1g3h5j7k9==|Lm4nOp6qRs8tUv0wXy2zAb4cDe6fGh8i=항목 이름, 사용자 이름, 비밀번호, 메모는 클라이언트가 전송하기 전에 암호화하므로 서버는 읽을 수 없는 암호문 상태로 저장합니다. 2. 접두사는 Bitwarden의 암호화 유형을 나타내며, 그 뒤에 초기화 벡터(IV), 암호문, MAC(메시지 인증 코드)이 각각 base64로 인코딩되어 |로 구분되어 이어집니다. 이를 복호화하는 키는 계정의 마스터 비밀번호에서 파생되는데, 이 비밀번호는 사용 가능한 형태로 서버에 전달되지 않습니다. 이 부분은 Vaultwarden과 자체 호스팅 Bitwarden 비교에서 다루는 바와 같이 Vaultwarden을 사용하든 공식 서버를 사용하든 동일합니다.
데이터베이스의 나머지 부분은 암호화되지 않습니다. 이메일 주소, 계정 이름, 비밀번호 힌트, 2단계 인증 복구 코드는 생성 시간이나 항목 소유 조직과 같은 메타데이터와 함께 평문으로 저장됩니다. 따라서 백업 파일 자체가 하나의 기밀 정보입니다. 파일을 확보한 사람은 누구든 사용자가 누구인지 알 수 있으며, 자신의 하드웨어가 허용하는 속도로 오프라인에서 암호화된 데이터 덩어리를 공격할 수 있습니다. 이러한 사실 때문에 아래에 기술된 저장 규칙이 중요합니다. 즉, 복사본은 서버를 떠나기 전에 반드시 암호화되어야 합니다.
Vaultwarden 실행 중에 db.sqlite3를 복사하는 것이 백업이 되지 않는 이유
Vaultwarden은 기본적으로 SQLite를 WAL 모드로 실행합니다(ENABLE_DB_WAL=true). 쓰기 작업은 먼저 db.sqlite3-wal에 기록되며, 체크포인트가 수행되어야만 db.sqlite3로 병합됩니다. db.sqlite3만 단독으로 복사하면 마지막 체크포인트 시점의 데이터베이스만 가져오게 됩니다. 따라서 10분 전에 저장한 비밀번호가 아카이브에서 누락될 수 있으며, 이에 대한 경고도 발생하지 않습니다.
cp을 사용하여 세 파일을 모두 복사하는 것도 해결책이 아닙니다. 파일들이 미세하게 다른 시점에 복사되기 때문에, 저장된 WAL 파일이 가리키는 페이지 버전과 메인 파일의 버전이 일치하지 않을 수 있습니다. 이 경우 SQLite는 서로 다른 두 파일로부터 복구를 시도하며, 결과적으로 데이터가 손상됩니다. 이 문제는 훨씬 나중에 발견하게 됩니다.
Error: database disk image is malformed.backup은 SQLite의 Online Backup API를 사용하므로 이러한 문제를 방지합니다. SQLite는 이 API를 활성 상태인 데이터베이스를 복사하는 공식적인 방법으로 명시하고 있습니다. 이 방식은 읽기 잠금 상태에서 페이지를 읽으며, 복사 도중 쓰기 작업으로 인해 파일이 변경되면 처음부터 다시 시작합니다. 따라서 디스크에 저장되는 데이터는 일관된 시점의 상태를 유지합니다.
sqlite3 .backup을 사용하여 데이터베이스 복사본 생성
sudo apt update && sudo apt install -y sqlite3
sudo install -d -m 700 /var/backups/vaultwarden
OUT=/var/backups/vaultwarden/db-$(date '+%Y%m%d-%H%M').sqlite3
sudo sqlite3 /opt/vaultwarden/data/db.sqlite3 ".backup '$OUT'"
sudo sqlite3 "$OUT" "PRAGMA integrity_check;"마지막 명령어는 ok를 별도의 줄에 출력합니다. 그 외의 결과가 나오면 복사본을 사용할 수 없으므로, 해당 파일을 보관하지 말고 이전 복사본도 삭제하지 마십시오. 전체 과정은 운영 중인 서버를 대상으로 실행되므로 사용자의 로그아웃이나 컨테이너 재시작은 발생하지 않습니다.
sqlite3 도구는 Vaultwarden 컨테이너 내부에 포함되어 있지 않습니다. 해당 이미지는 debian:trixie-slim 기반으로 ca-certificates, curl, libmariadb3, libpq5 및 openssl를 사용하여 빌드되므로, docker exec vaultwarden sqlite3 ...을 실행하면 다음과 같은 오류가 발생합니다.
exec: "sqlite3": executable file not found in $PATH대신 호스트에서 마운트된 경로를 대상으로 명령어를 실행하십시오. 위에서 제시한 명령어들이 이 방식을 따릅니다. 데이터가 명명된 볼륨(named volume)에 저장되어 있다면, docker volume inspect <name> 명령어를 통해 /var/lib/docker/volumes/ 하위의 호스트 경로를 확인할 수 있습니다.
Vaultwarden은 버전 1.32.1부터 자체 백업 명령어를 제공합니다. 서버에서 다음을 실행하십시오.
docker exec -it vaultwarden /vaultwarden backup이 명령어는 VACUUM INTO을 실행하여 db_YYYYMMDD_HHMMSS.sqlite3를 데이터 폴더에 기록합니다. 다음 두 가지 사항을 유의하십시오. 첫째, 복사본이 원본과 같은 디스크의 동일한 위치에 저장되므로 이는 백업이 아닌 준비 단계에 해당합니다. 둘째, 이 기능은 SQLite 전용입니다. MariaDB나 PostgreSQL을 사용하는 경우 The database type is not SQLite. Backups only works for SQLite databases 오류와 함께 중단됩니다.
사용자가 잊기 쉬운 파일들
attachments/은(는) 불투명한 이름으로 암호화된 데이터를 보관합니다. 각 첨부 파일에 대한 데이터베이스 행에는 암호화된 파일 이름과 클라이언트가 파일을 복호화하는 데 필요한 키 정보가 포함되어 있습니다. 데이터베이스가 없는 첨부 파일은 읽을 수 없는 데이터 덩어리에 불과하며, 첨부 파일이 없는 데이터베이스는 사용자에게 다운로드에 실패하는 항목만 제공하게 됩니다. 따라서 두 항목을 동일한 시점에 백업하십시오.
config.json은(는) 관리자 페이지에서 저장한 모든 설정을 보관하며, 이 파일의 값은 일치하는 환경 변수보다 우선합니다. 이는 양날의 검과 같습니다. 이전의 config.json을(를) 복원하면 compose 파일의 설정을 조용히 덮어쓰게 되며, 이 파일 자체에는 SMTP 비밀번호와 관리자 토큰이 포함될 수 있어 보안에 민감합니다. 해당 토큰은 일반 텍스트가 아닌 Argon2id PHC(password hashing competition) 문자열로 저장하십시오. docker run --rm -it vaultwarden/server /vaultwarden hash 명령을 사용하면 이를 생성할 수 있습니다.
rsa_key.pem은(는) 클라이언트의 로그인 상태를 유지하는 JSON web tokens (JWT)에 서명합니다. 시작 시 이 파일이 없으면 Vaultwarden은 새로운 키를 생성하며, 이로 인해 이전 키로 서명된 모든 토큰의 유효성이 상실되어 모든 클라이언트가 로그아웃됩니다. Vault 내부의 데이터는 마스터 비밀번호에서 파생된 키로 암호화되므로 안전하게 유지되지만, 키 파일을 복원하면 대규모 로그아웃 사태를 방지할 수 있습니다.
sends/은(는) Send 링크 뒤에 있는 파일들을 보관합니다. 이 파일들이 누락되면 해당 다운로드 기능만 작동하지 않으며 다른 서비스에는 영향을 주지 않습니다.
전체 과정을 하나의 스크립트로 통합하기
#!/bin/bash
set -euo pipefail
DATA=/opt/vaultwarden/data
DEST=/var/backups/vaultwarden
STAMP=$(date '+%Y%m%d-%H%M%S')
STAGE=$(mktemp -d /tmp/vw-stage.XXXXXX)
install -d -m 700 "$DEST"
sqlite3 "$DATA/db.sqlite3" ".backup '$STAGE/db.sqlite3'"
test "$(sqlite3 "$STAGE/db.sqlite3" 'PRAGMA integrity_check;')" = "ok"
cp -a "$DATA"/rsa_key* "$STAGE/"
for extra in config.json attachments sends; do
if [ -e "$DATA/$extra" ]; then cp -a "$DATA/$extra" "$STAGE/"; fi
done
tar -C "$STAGE" -czf "$DEST/vw-$STAMP.tar.gz" .
chmod 600 "$DEST/vw-$STAMP.tar.gz"
rm -rf "$STAGE"
tar -tzf "$DEST/vw-$STAMP.tar.gz"이 내용을 /usr/local/sbin/vw-backup.sh로 저장하고, chmod 700 권한을 부여한 뒤 root 계정으로 실행합니다. test 줄이 실제 작업을 수행합니다. sqlite3는 PRAGMA integrity_check이 데이터 손상을 보고하더라도 종료 코드 0을 반환하므로, 출력 결과를 ok과 비교해야 잘못된 복사본을 실패한 스크립트로 처리할 수 있습니다. set -euo pipefail은 tar가 손상된 데이터베이스를 포함한 아카이브를 생성하지 못하도록 즉시 모든 작업을 중단시킵니다.
마지막 tar -tzf은 실제로 캡처된 항목을 나열합니다. 처음 실행할 때 반드시 확인하십시오. ./db.sqlite3, ./rsa_key.pem, ./config.json, ./attachments/가 포함되어 있는지, 그리고 ./db.sqlite3-wal가 없는지 확인해야 합니다. journalctl 출력과 실패 보고 기능을 갖춘 unit을 원한다면 cron 대신 systemd 서비스 및 타이머를 사용하여 매일 밤 실행하십시오.
스크래치 디렉터리에 복원하여 백업 검증하기
테스트하지 않은 백업은 추측에 불과합니다. 스크래치 디렉터리에 복원하는 작업은 1분이면 충분하며, 운영 중인 서비스에 아무런 영향을 주지 않습니다.
sudo install -d -m 700 /tmp/vw-check
sudo tar -C /tmp/vw-check -xzf /var/backups/vaultwarden/vw-20260805-030000.tar.gz
ls -l /tmp/vw-check
sudo sqlite3 /tmp/vw-check/db.sqlite3 "PRAGMA integrity_check;"
sudo sqlite3 /tmp/vw-check/db.sqlite3 "select count(*) from users;"
sudo sqlite3 /tmp/vw-check/db.sqlite3 "select count(*) from ciphers;"
sudo du -sh /tmp/vw-check/attachments네 가지 결과가 중요합니다. integrity_check은 ok을 출력합니다. 사용자 수는 알고 있는 계정의 수와 일치해야 합니다. 암호문(cipher) 수는 sudo sqlite3 /opt/vaultwarden/data/db.sqlite3 "select count(*) from ciphers;"에서 확인한 운영 환경의 수치와 비슷해야 하며, 사용 중인 볼트(vault)라면 절대 0이 되어서는 안 됩니다. 첨부 파일 디렉터리의 크기는 예상하는 수준이어야 합니다(첨부 파일을 업로드하는 사용자가 없다면 이 단계는 건너뛰어도 됩니다). 그 후 sudo rm -rf /tmp/vw-check을 실행하십시오. 해당 디렉터리에 모든 데이터의 복사본이 하나 더 생성되었기 때문입니다.
수동으로 복사한 데이터 폴더를 복원할 때 지켜야 할 규칙이 하나 있습니다. 서버를 시작하기 전에 반드시 db.sqlite3-wal과 db.sqlite3-shm를 삭제해야 합니다. 그렇지 않으면 SQLite가 다른 복사본에 속한 로그를 사용하여 복원된 데이터베이스를 복구하려고 시도하며, 이 과정에서 온전했던 데이터베이스가 손상됩니다. 위 스크립트로 생성된 아카이브에는 이러한 파일이 포함되지 않습니다. .backup은 하나의 완전한 데이터베이스 파일만 기록하기 때문입니다.
서버로 복구
이 작업은 컨테이너를 중지한 상태에서 본인의 서버에서 수행합니다. 데이터 폴더가 변경되는 동안 Vaultwarden이 쓰기 작업을 수행해서는 안 됩니다.
cd /opt/vaultwarden
docker compose stop vaultwarden
sudo mv data data.old.$(date '+%Y%m%d-%H%M%S')
sudo install -d -m 700 data
sudo tar -C data -xzf /var/backups/vaultwarden/vw-20260805-030000.tar.gz
sudo chown -R root:root data
docker compose start vaultwarden
docker compose logs --tail 20 vaultwardenchown에는 컨테이너를 실행하는 사용자를 지정해야 합니다. 기본 이미지는 root 권한으로 실행되므로 root:root가 적절합니다. 단, compose 파일에 user:을 설정했다면 해당 uid와 gid를 사용하십시오. 서버가 데이터 폴더에 쓰기 권한을 가지지 못하면 로그인 페이지에서 모든 요청이 실패하며, 로그에 관련 내용이 기록됩니다.
정상적으로 시작되면 Rocket 라인으로 종료됩니다:
[INFO] Rocket has launched from http://0.0.0.0:80그다음 브라우저에서 로그인하여 항목을 열고 첨부 파일 하나를 다운로드하십시오. 로그인은 성공하지만 첨부 파일 다운로드가 실패한다면, 데이터베이스는 복구되었으나 attachments/이 누락된 것입니다. 모든 항목이 확인될 때까지 data.old.*을 보관한 뒤 삭제하십시오. 롤백은 디렉터리 위치를 반대로 바꾸어 동일한 세 단계를 수행하면 됩니다.
사용 중인 경로가 본 문서의 경로와 다르다면, VPS용 Vaultwarden 설치 가이드에서 이 명령어들이 가정하는 compose 파일을 확인할 수 있습니다.
백업을 저장해서는 안 되는 위치
- 데이터 폴더와 동일한 디스크에 저장하지 마십시오. 볼륨 하나만 고장 나도 두 복사본이 모두 손실되며, 잘못된 경로에
rm -rf가 발생해도 마찬가지입니다. - 동일한 서버에 저장하지 마십시오. 두 번째 볼륨이라도 마찬가지입니다. root 권한을 획득한 공격자는 동일한 세션 내에서 백업 데이터까지 접근할 수 있습니다.
- 암호화 없이 오브젝트 스토리지에 저장하지 마십시오. 아카이브에는 이메일 주소, 비밀번호 힌트, 복구 코드, 볼트 암호문이 포함되어 있어 오프라인 공격의 대상이 될 수 있습니다.
- 클라우드 제공업체의 스냅샷에만 의존하지 마십시오. 스냅샷은 복구가 빨라 유용하지만, 서버와 동일한 계정에 귀속되므로 계정 문제가 발생하면 백업도 함께 사라집니다.
restic은 오프사이트 복사본을 생성하는 데 적합합니다. restic 저장소는 데이터가 업로드되기 전에 로컬 머신에서 암호화되기 때문입니다. 서버에서의 설정은 다음과 같습니다.
sudo apt install -y restic
export RESTIC_REPOSITORY=s3:https://s3.example.com/vaultwarden-backups
export RESTIC_PASSWORD_FILE=/root/.restic-password
restic init
restic backup /var/backups/vaultwarden --tag vaultwarden
restic snapshots --tag vaultwarden
restic forget --tag vaultwarden --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prunerestic이 라이브 데이터 폴더가 아닌 아카이브 디렉터리를 가리키도록 설정하십시오. 그래야 이미 검증을 마친 일관된 복사본이 업로드됩니다. 저장소 비밀번호는 보호 대상인 서버가 아닌 다른 곳에 보관하십시오. 설계상 해당 비밀번호를 분실하면 스냅샷을 복구할 수 없습니다. 스토리지가 지원한다면, 서버에 쓰기 권한만 부여하고 삭제 권한은 부여하지 마십시오. 이렇게 하면 서버가 침해당하더라도 자신의 백업 기록을 삭제할 수 없습니다. VPS에서 restic 백업 설정하기에서 저장소와 스케줄에 대한 전체 내용을 다루며, restic과 BorgBackup 비교에서는 아직 도구를 선택하지 못한 경우를 위한 가이드를 제공합니다.
정기적인 복구 테스트 수행
한 달에 하루를 정하십시오. restic restore latest --tag vaultwarden --target /tmp/vw-check을 사용하여 최신 스냅샷을 임시 디렉터리로 가져온 뒤, 동일한 PRAGMA integrity_check을 실행하고 행 개수를 확인하여 날짜와 개수를 기록하십시오. 6개월 동안 한 번도 복구하지 않은 백업은 상태를 알 수 없는 백업과 다름없습니다. 장애가 발생한 최악의 순간에 백업 상태를 확인하게 되는 상황을 피해야 합니다.
일 년에 한 번은 전체 복구 과정을 수행하십시오. 복구된 데이터 폴더를 사용하여 여분의 포트에서 두 번째 Vaultwarden 컨테이너를 시작하고, 실제 계정으로 로그인하십시오. 이는 행 개수 확인만으로는 알 수 없는 마스터 비밀번호 경로의 전체적인 정상 작동을 증명합니다. 동일한 주기로 restic check --read-data-subset=10%를 수행하면 저장된 데이터가 단순히 목록에 존재하는 것을 넘어 실제로 읽기 가능한 상태인지 검증할 수 있습니다.
FAQ
Vaultwarden이 실행 중일 때 cp 명령어로 db.sqlite3를 복사해도 됩니까?
아니요. Vaultwarden은 SQLite를 WAL 모드로 실행하므로 최근 쓰기 작업은 db.sqlite3-wal에 저장되며 아직 db.sqlite3에는 반영되지 않습니다. 메인 파일만 cp하면 데이터가 유실되며, 두 파일을 별도로 복사하면 나중에 Error: database disk image is malformed 오류를 일으키는 불일치 상태가 발생할 수 있습니다. 대신 sqlite3 /path/db.sqlite3 ".backup '/path/out.sqlite3'"을 사용하십시오. 이 도구는 SQLite의 Online Backup API를 사용하여 서버가 계속 작동하는 동안에도 일관된 단일 파일을 생성합니다.
백업을 위해 Vaultwarden 컨테이너를 중지해야 합니까?
아니요. 그것이 바로 .backup을 사용하는 이유입니다. 서버가 실행 중일 때 데이터베이스를 복사해도 안전합니다. 첨부 파일과 Send 파일은 사용자가 업로드할 때 기록되므로, 데이터베이스 복사와 tar 사이에 추가된 파일은 해당 일자 아카이브에 포함되지 않을 수 있습니다. 최악의 경우 첨부 파일 하나가 누락될 수 있습니다. 몇 초간의 서비스 중단이 문제되지 않는다면, 스크립트 실행 전 docker compose stop을 수행하고 완료 후 docker compose start을 수행하여 이러한 불일치 가능성까지 제거할 수 있습니다.
rsa_key 파일 없이 복원하면 어떻게 됩니까?
Vaultwarden은 시작 시 새로운 키를 생성합니다. 해당 키는 세션을 유지하는 JSON web tokens (JWT)에 서명하므로, 기존의 모든 토큰은 유효성을 잃게 되며 모든 클라이언트는 로그아웃되어 다시 로그인해야 합니다. 볼트 내부의 데이터는 RSA 키가 아닌 각 사용자의 마스터 비밀번호에서 파생된 키로 암호화되므로 영향을 받지 않습니다. 데이터 폴더의 나머지 부분과 함께 rsa_key.pem를 복원하면 사용자는 복원 사실을 인지하지 못합니다.
백업 아카이브를 그대로 객체 스토리지에 업로드해도 안전합니까?
아니요. 항목 이름, 비밀번호, 메모는 암호문이지만 이메일 주소, 계정 이름, 비밀번호 힌트, 2단계 인증 복구 코드는 데이터베이스 내에서 평문으로 존재합니다. 오프라인 공격자는 암호문을 여유롭게 해독할 수 있습니다. 데이터를 서버 외부로 보내기 전에 반드시 암호화하십시오. restic 저장소는 이를 자동으로 처리하며, gpg --symmetric --cipher-algo AES256 vw-20260805-030000.tar.gz은 어떤 스토리지에든 전송할 수 있는 단일 암호화 파일을 생성합니다.
PostgreSQL이나 MariaDB에서 Vaultwarden을 어떻게 백업합니까?
SQLite용 단계는 적용되지 않으며, 내장 명령어를 사용하면 The database type is not SQLite. Backups only works for SQLite databases 오류가 발생합니다. 해당 데이터베이스의 기본 도구인 pg_dump 또는 mysqldump을 사용하여 데이터베이스를 덤프하십시오. 나머지 규칙은 모두 동일하게 유지해야 합니다. 덤프 파일은 attachments/, sends/, config.json 및 rsa_key 파일과 함께 하나의 아카이브로 묶어야 하며, 동일한 실행 시점에 생성하고 암호화한 뒤 백업을 수행한 서버가 아닌 다른 곳에 저장해야 합니다.