SSD Nodes Learn 🎉 VPS od $5.50/mies.
Przewodniki Matt ConnorAutor: Matt Connor · Zaktualizowano 2026-08-13

Vaultwarden: jak wykonać kopię zapasową i przywrócić dane

Dowiedz się, jak poprawnie zabezpieczyć bazę danych sqlite3 za pomocą polecenia .backup. Zapewnij pełną integralność plików db, rsa_key oraz załączników w instalacjach Docker.

Co musi zawierać kopia zapasowa Vaultwarden

Kopia zapasowa Vaultwarden to kopia całego folderu danych, przy czym baza danych wewnątrz musi zostać skopiowana w odpowiedni sposób. Należy uruchomić sqlite3 db.sqlite3 ".backup out.sqlite3" zamiast cp, ponieważ zwykłe kopiowanie bazy danych w trakcie zapisu może skutkować plikiem, którego nie da się otworzyć. Następnie należy zachować pliki znajdujące się obok niej; jest to połowa procesu, o której często się zapomina.

W instalacji Docker folderem danych jest to, co zamontowano w /data. Jest to ścieżka na hoście lub wolumen nazwany, a różnica między bind mounts a named volumes decyduje o tym, gdzie fizycznie na dysku znajduje się sejf. Oto co zawiera ten folder:

  • db.sqlite3: każde konto, każdy element sejfu, każdy folder i każda organizacja. Utrata tego pliku oznacza utratę sejfu.
  • db.sqlite3-wal oraz db.sqlite3-shm: dziennik transakcji (WAL) oraz jego indeks pamięci współdzielonej. Ostatnie zapisy znajdują się tutaj, dopóki SQLite nie scali ich z głównym plikiem.
  • attachments/: pliki załączone przez użytkowników do elementów sejfu, zaszyfrowane, w jednym katalogu na element.
  • sends/: pliki powiązane z linkami Bitwarden Send.
  • config.json: wszystkie ustawienia zapisane z poziomu strony administratora.
  • rsa_key.pem, oraz rsa_key.der i rsa_key.pub.der w starszych instalacjach: klucz podpisujący tokeny logowania.
  • icon_cache/: pobrane ikony stron internetowych. Jest to jedyny katalog, który można pominąć, ponieważ Vaultwarden pobierze je ponownie na żądanie.

Czy baza danych Vaultwarden jest bezpieczna? Co faktycznie zawiera plik

Dwa polecenia udzielają odpowiedzi na to pytanie; oba można wykonać natychmiast.

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;"

Pierwsze z nich wyświetla adresy e-mail użytkowników w postaci jawnej. Drugie wyświetla nazwę jednego elementu, która wygląda następująco:

2.k9Qw1nQ0y7Yy2Xw==|E1r0J3l5s7d9f1g3h5j7k9==|Lm4nOp6qRs8tUv0wXy2zAb4cDe6fGh8i=

Nazwy elementów, nazwy użytkowników, hasła oraz notatki są szyfrowane przez klienta przed wysłaniem, dlatego serwer przechowuje tekst zaszyfrowany, którego nie potrafi odczytać. Prefiks 2. oznacza typ szyfrowania Bitwarden, po którym następuje wektor inicjalizacyjny (IV), tekst zaszyfrowany oraz kod uwierzytelniania wiadomości (MAC), każdy w formacie base64 i oddzielony znakiem |. Klucz deszyfrujący jest wyprowadzany z głównego hasła konta, które nigdy nie dociera do serwera w użytecznej formie. Ten mechanizm jest identyczny niezależnie od tego, czy uruchomiono Vaultwarden, czy oficjalny serwer, co opisuje porównanie Vaultwarden i samodzielnie hostowanego Bitwarden.

Pozostała część bazy danych nie jest szyfrowana. Adresy e-mail, nazwy kont, podpowiedzi do haseł oraz kody odzyskiwania dwuskładnikowego są przechowywane jako zwykły tekst, obok metadanych takich jak czas utworzenia czy informacja o tym, która organizacja jest właścicielem elementu. Z tego powodu sam plik kopii zapasowej jest poufny. Każdy, kto go posiada, dowiaduje się, kim są użytkownicy, i może przeprowadzić atak na zaszyfrowane bloki danych w trybie offline, z szybkością ograniczoną jedynie przez wydajność posiadanego sprzętu. Ten fakt determinuje zasady przechowywania opisane w dalszej części: kopia jest szyfrowana przed opuszczeniem serwera.

Dlaczego kopiowanie db.sqlite3 podczas działania Vaultwarden nie jest kopią zapasową

Vaultwarden domyślnie uruchamia SQLite w trybie WAL (ENABLE_DB_WAL=true). Operacja zapisu trafia najpierw do db.sqlite3-wal, a dopiero punkt kontrolny (checkpoint) scala ją z db.sqlite3. Skopiowanie samego db.sqlite3 powoduje uzyskanie stanu bazy danych z momentu ostatniego punktu kontrolnego. Hasło zapisane dziesięć minut wcześniej może więc nie znaleźć się w archiwum, a system nie wygeneruje żadnego ostrzeżenia.

Kopiowanie wszystkich trzech plików za pomocą cp również nie rozwiązuje problemu. Kopie są tworzone w nieco innych momentach, więc zapisany plik WAL może opisywać wersje stron, które nie pasują już do głównego pliku bazy danych. SQLite próbuje wtedy odtworzyć jeden plik na podstawie drugiego, co prowadzi do błędnych wyników. Problem ujawnia się dopiero po długim czasie:

Error: database disk image is malformed

.backup pozwala uniknąć tego problemu, ponieważ korzysta z interfejsu Online Backup API, który w dokumentacji SQLite jest wskazany jako właściwy sposób kopiowania aktywnie używanej bazy danych. Narzędzie to odczytuje strony bazy pod blokadą odczytu i restartuje proces, jeśli w międzyczasie proces zapisu zmodyfikuje plik. Dzięki temu zapisany na dysku plik odzwierciedla jeden spójny moment w czasie.

Wykonanie kopii bazy danych za pomocą 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;"

Ostatnie polecenie wypisuje ok w osobnej linii. Każdy inny wynik oznacza, że kopia nie nadaje się do użytku; nie należy jej zachowywać, ale nie wolno też usuwać poprzedniej wersji. Cała procedura jest wykonywana na działającym serwerze, więc nikt nie zostaje wylogowany, a kontenery nie są restartowane.

Narzędzie sqlite3 nie znajduje się wewnątrz kontenera Vaultwarden. Obraz jest budowany na debian:trixie-slim przy użyciu ca-certificates, curl, libmariadb3, libpq5 oraz openssl, dlatego docker exec vaultwarden sqlite3 ... kończy się błędem:

exec: "sqlite3": executable file not found in $PATH

Zamiast tego należy uruchomić polecenie na hoście, wskazując na zamontowaną ścieżkę, co realizują powyższe instrukcje. Jeśli dane znajdują się w nazwanym wolumenie (named volume), docker volume inspect <name> wyświetli ścieżkę na hoście wewnątrz /var/lib/docker/volumes/.

Od wersji 1.32.1 Vaultwarden posiada również własne polecenie do tworzenia kopii zapasowych. Na serwerze:

docker exec -it vaultwarden /vaultwarden backup

Wykonuje ono VACUUM INTO i zapisuje db_YYYYMMDD_HHMMSS.sqlite3 w folderze z danymi. Należy pamiętać o dwóch kwestiach. Kopia trafia obok oryginału na ten sam dysk, więc jest to etap przygotowawczy, a nie pełnoprawna kopia zapasowa. Ponadto funkcja ta obsługuje wyłącznie SQLite: w przypadku MariaDB lub PostgreSQL działanie zostanie przerwane z komunikatem The database type is not SQLite. Backups only works for SQLite databases.

Pliki, o których się zapomina

attachments/ przechowuje szyfrogramy pod nieprzejrzystymi nazwami. Wiersz bazy danych dla każdego załącznika zawiera jego zaszyfrowaną nazwę pliku oraz materiał klucza, którego klient potrzebuje do odszyfrowania pliku. Załączniki bez bazy danych są nieczytelnym szumem, a baza danych bez załączników udostępnia użytkownikom elementy, których pobieranie kończy się niepowodzeniem. Należy zabezpieczać oba elementy w tym samym przebiegu.

config.json przechowuje wszystko, co zapisano z poziomu panelu administratora, a jego wartości mają pierwszeństwo przed pasującymi zmiennymi środowiskowymi. Działa to w obie strony: przywrócenie starego config.json po cichu nadpisuje ustawienia w pliku compose, a sam plik jest wrażliwy, ponieważ może zawierać hasło SMTP oraz token administratora. Token ten należy przechowywać jako ciąg Argon2id PHC (password hashing competition), a nie jako zwykły tekst. docker run --rm -it vaultwarden/server /vaultwarden hash wygeneruje taki ciąg.

rsa_key.pem podpisuje tokeny JSON web tokens (JWT), które utrzymują sesje zalogowanych klientów. Jeśli plik nie istnieje podczas uruchamiania, Vaultwarden generuje nowy klucz, przez co każdy token podpisany starym kluczem przestaje być poprawny i wszyscy klienci zostają wylogowani. Zawartość skarbca przetrwa tę operację, ponieważ jest szyfrowana kluczami pochodzącymi z hasła głównego. Przywrócenie pliku klucza pozwala uniknąć masowego wylogowania.

sends/ przechowuje pliki powiązane z linkami Send. Ich brak powoduje jedynie błędy przy pobieraniu tych konkretnych plików.

Umieszczenie całości w jednym skrypcie

#!/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"

Zapisz plik jako /usr/local/sbin/vw-backup.sh, nadaj mu uprawnienia chmod 700 i uruchom jako root. Linia test wykonuje właściwą pracę: sqlite3 zwraca kod wyjścia 0 nawet wtedy, gdy PRAGMA integrity_check zgłasza uszkodzenie, dlatego porównanie wyniku z ok jest tym, co zmienia niepoprawną kopię w nieudane wykonanie skryptu. set -euo pipefail następnie przerywa działanie, zamiast pozwalać tar na utworzenie poprawnego archiwum wokół uszkodzonej bazy danych.

Końcowe polecenie tar -tzf wyświetla listę faktycznie przechwyconych danych. Przejrzyj ją za pierwszym razem. Należy szukać ./db.sqlite3, ./rsa_key.pem, ./config.json oraz ./attachments/, a także upewnić się, że nie występuje ./db.sqlite3-wal. Uruchamiaj skrypt co noc za pomocą usługi i timera systemd, zamiast cron, jeśli wymagasz logowania journalctl oraz jednostki zgłaszającej błędy.

Weryfikacja kopii zapasowej poprzez przywrócenie jej do katalogu tymczasowego

Nietestowana kopia zapasowa jest jedynie przypuszczeniem. Przywrócenie danych do katalogu tymczasowego zajmuje minutę i nie wpływa na środowisko produkcyjne.

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

Istotne są cztery wyniki. integrity_check wypisuje ok. Liczba użytkowników musi zgadzać się z liczbą znanych kont. Liczba szyfrogramów powinna być zbliżona do wartości z działającej instancji sudo sqlite3 /opt/vaultwarden/data/db.sqlite3 "select count(*) from ciphers;" i nigdy nie może wynosić zero w przypadku używanego sejfu. Katalog załączników powinien mieć zbliżony rozmiar do oczekiwanego (krok ten można pominąć, jeśli nikt nie przesyła załączników). Następnie należy uruchomić sudo rm -rf /tmp/vw-check, ponieważ katalog ten zawiera teraz drugą kopię wszystkich danych.

Podczas przywracania ręcznie skopiowanego folderu danych obowiązuje jedna zasada: przed uruchomieniem serwera należy usunąć db.sqlite3-wal oraz db.sqlite3-shm. W przeciwnym razie SQLite spróbuje odzyskać przywróconą bazę danych przy użyciu dziennika należącego do innej kopii, co doprowadzi do uszkodzenia poprawnie przywróconej bazy. Archiwa tworzone przez powyższy skrypt nigdy nie zawierają tych plików, ponieważ .backup zapisuje jedną kompletną bazę danych.

Przywracanie danych na serwerze

Poniższe operacje wykonuje się na własnym serwerze przy zatrzymanym kontenerze. Vaultwarden nie może zapisywać danych w momencie, gdy zawartość folderu z danymi ulega zmianie.

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 vaultwarden

W chown należy wskazać użytkownika, na którego uprawnieniach działa kontener. Standardowy obraz działa jako root, więc root:root jest poprawne, chyba że w pliku compose zdefiniowano user: – w takim przypadku należy użyć odpowiedniego uid oraz gid. Folder z danymi, do którego serwer nie ma uprawnień zapisu, spowoduje wyświetlenie strony logowania, która odrzuci każde żądanie, co zostanie odnotowane w logach.

Poprawne uruchomienie kończy się komunikatem Rocket:

[INFO] Rocket has launched from http://0.0.0.0:80

Następnie należy zalogować się przez przeglądarkę, otworzyć element i pobrać załącznik. Jeśli logowanie działa, ale pobieranie załączników kończy się błędem, oznacza to, że archiwum zawiera bazę danych, ale nie zawiera attachments/. Należy zachować data.old.* do momentu pełnej weryfikacji, a następnie usunąć plik. Wycofanie zmian (rollback) przebiega w trzech identycznych krokach, przy czym należy zamienić ścieżki katalogów miejscami.

Jeśli używane ścieżki różnią się od przedstawionych, przewodnik instalacji Vaultwarden na VPS zawiera plik compose, na którym bazują te polecenia.

Gdzie nie przechowywać kopii zapasowych

  • Nie na tym samym dysku, na którym znajduje się folder z danymi. Awaria wolumenu niszczy obie kopie, podobnie jak jeden rm -rf na niewłaściwej ścieżce.
  • Nie na tym samym serwerze, nawet jeśli jest to drugi wolumen. Atakujący, który uzyska dostęp do konta root, przejmie również kopie zapasowe w tej samej sesji.
  • Nie w pamięci obiektowej bez szyfrowania, ponieważ archiwum zawiera adresy e-mail, podpowiedzi do haseł, kody odzyskiwania oraz zaszyfrowane dane skarbca, które można atakować offline.
  • Nie tylko w migawkach dostawcy usług. Przywracają się one szybko, co jest przydatne, ale znajdują się na tym samym koncie co serwer, więc problem z kontem powoduje utratę również tych kopii.

Kopia zewnętrzna (offsite) to zastosowanie dla restic, ponieważ repozytorium restic jest szyfrowane na maszynie przed wysłaniem jakichkolwiek danych. Na serwerze:

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 --prune

Skieruj restic na katalog z archiwum, a nie na folder z aktywnymi danymi, aby przesyłana była spójna kopia, którą już zweryfikowano. Przechowuj hasło do repozytorium w innym miejscu niż serwer, który ono zabezpiecza: utrata hasła sprawia, że migawki stają się nieczytelne, zgodnie z założeniami projektowymi. Jeśli pamięć masowa na to pozwala, nadaj serwerowi poświadczenia z uprawnieniami do zapisu, ale bez uprawnień do usuwania, aby przejęcie kontroli nad maszyną nie pozwoliło na wymazanie własnej historii. Konfiguracja kopii zapasowych restic na VPS szczegółowo omawia repozytorium oraz harmonogram, a restic w porównaniu z BorgBackup omawia wybór narzędzia, jeśli decyzja jeszcze nie zapadła.

Testowanie przywracania danych według harmonogramu

Należy wyznaczyć jeden dzień w miesiącu na testy. Należy pobrać najnowszą migawkę do katalogu tymczasowego za pomocą restic restore latest --tag vaultwarden --target /tmp/vw-check, wykonać to samo polecenie PRAGMA integrity_check, sprawdzić liczbę wierszy, a następnie zapisać datę oraz uzyskane wyniki. Kopia zapasowa, której nie przywracano przez sześć miesięcy, jest kopią o nieznanym stanie. Odkrycie tego stanu podczas awarii jest najgorszym możliwym momentem.

Raz w roku należy wykonać pełną procedurę testową. Należy uruchomić drugi kontener Vaultwarden na wolnym porcie, wskazując na przywrócony katalog danych, a następnie zalogować się na rzeczywiste konto. Potwierdza to poprawność ścieżki głównego hasła (master password) w pełnym zakresie, czego nie jest w stanie zweryfikować samo liczenie wierszy. Wykonanie restic check --read-data-subset=10% zgodnie z tym samym harmonogramem pozwala zweryfikować, czy przechowywane dane są możliwe do odczytania, a nie tylko widoczne na liście plików.

FAQ

Czy mogę kopiować plik db.sqlite3 za pomocą cp, gdy Vaultwarden jest uruchomiony?

Nie. Vaultwarden używa SQLite w trybie WAL, więc ostatnie zapisy znajdują się w db.sqlite3-wal i nie zostały jeszcze przeniesione do db.sqlite3. Wykonanie cp samego głównego pliku spowoduje ich cichą utratę, a kopiowanie obu plików osobno może doprowadzić do powstania niespójnej pary, co ujawni się później jako Error: database disk image is malformed. Zamiast tego należy użyć sqlite3 /path/db.sqlite3 ".backup '/path/out.sqlite3'". Narzędzie to korzysta z interfejsu Online Backup API systemu SQLite i tworzy jeden spójny plik, podczas gdy serwer nieprzerwanie obsługuje żądania.

Czy muszę zatrzymywać kontener Vaultwarden, aby wykonać kopię zapasową?

Nie, i właśnie na tym polega zaleta .backup. Kopia bazy danych jest bezpieczna na działającym serwerze. Załączniki oraz pliki przesyłane przez funkcję Send są zapisywane w momencie ich wysyłania przez użytkownika, więc plik dodany między kopiowaniem bazy danych a tar może nie trafić do nocnego archiwum, co w najgorszym przypadku skutkuje utratą jednego załącznika. Jeśli kilkusekundowa przerwa w działaniu nie stanowi problemu, wykonanie docker compose stop przed skryptem i docker compose start po nim eliminuje nawet to ryzyko.

Co się stanie, jeśli przywrócę dane bez plików rsa_key?

Vaultwarden wygeneruje nowy klucz przy starcie. Klucz ten podpisuje tokeny JSON web tokens (JWT), które podtrzymują sesje, więc każdy istniejący token przestanie być ważny, a wszyscy klienci zostaną wylogowani i będą musieli zalogować się ponownie. Zawartość skarbca pozostaje nienaruszona, ponieważ jest szyfrowana kluczami pochodzącymi z głównego hasła użytkownika, a nie kluczem RSA. Przywrócenie rsa_key.pem wraz z resztą folderu danych sprawi, że nikt nie zauważy procesu odtwarzania.

Czy archiwum kopii zapasowej jest bezpieczne do przesłania do pamięci obiektowej w obecnej formie?

Nie. Nazwy elementów, hasła i notatki są szyfrogramami, ale adresy e-mail, nazwy kont, podpowiedzi do haseł i kody odzyskiwania dwuetapowego są w bazie danych zapisane tekstem jawnym, a atakujący działający offline może łamać szyfrogram we własnym tempie. Archiwum należy zaszyfrować przed opuszczeniem maszyny. Repozytorium restic wykonuje to zadanie automatycznie, a gpg --symmetric --cipher-algo AES256 vw-20260805-030000.tar.gz tworzy pojedynczy zaszyfrowany plik, który można przekazać do dowolnej pamięci masowej.

Jak wykonać kopię zapasową Vaultwarden na PostgreSQL lub MariaDB?

Procedury dla SQLite nie mają tutaj zastosowania, a wbudowane polecenie zwróci błąd The database type is not SQLite. Backups only works for SQLite databases. Należy wykonać zrzut bazy danych za pomocą natywnego narzędzia, pg_dump lub mysqldump, zachowując wszystkie pozostałe zasady. Zrzut powinien znaleźć się w jednym archiwum wraz z attachments/, sends/, config.json oraz plikami rsa_key, wykonanymi w tym samym przebiegu, zaszyfrowanymi i przechowywanymi poza serwerem, na którym zostały utworzone.