גיבוי ושחזור Vaultwarden ב־VPS עם sqlite3
למדו להעתיק כספת פעילה עם sqlite3 .backup, לשמור attachments, config.json וקובצי rsa_key, ולבדוק שהשחזור עובד לפני מקרה חירום.
מה חייב להיכלל בגיבוי של Vaultwarden
גיבוי של Vaultwarden הוא עותק של תיקיית הנתונים כולה, ויש להעתיק את מסד הנתונים שבתוכה בדרך הנכונה. הפעילו את sqlite3 db.sqlite3 ".backup out.sqlite3" במקום את cp, משום שהעתקה רגילה של מסד נתונים שנכתבים אליו נתונים עלולה ליצור קובץ שלא ייפתח. לאחר מכן שמרו גם את הקבצים הנלווים אליו. עליהם אנשים נוטים לשכוח.
בהתקנת Docker, תיקיית הנתונים היא התיקייה שמיפיתם אל /data. זו יכולה להיות נתיב במארח או volume בעל שם, וההבדל בין bind mounts לבין named volumes, המתואר ב־ההבדל בין bind mounts לבין named volumes, קובע היכן הכספת שלכם נמצאת בפועל בדיסק. אלה התכנים שלה.
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 ובין אם אתם משתמשים בשרת הרשמי, כפי שמפורט ב־השוואה בין Vaultwarden ל־Bitwarden באירוח עצמי.
שאר מסד הנתונים אינו מוצפן. כתובות דוא"ל, שמות חשבונות, רמזים לסיסמאות וקודי שחזור של אימות דו־שלבי נשמרים בטקסט גלוי, לצד מטא־נתונים כגון זמני יצירה והארגון שבבעלותו פריט. לכן קובץ הגיבוי עצמו הוא סוד. כל מי שמחזיק בו יכול לדעת מי המשתמשים שלכם ולתקוף את המידע המוצפן באופן לא מקוון, במהירות שמאפשרת החומרה שלו. עובדה זו קובעת את כללי האחסון שבהמשך: יש להצפין את העותק לפני שהוא עוזב את השרת. אסימון הניהול הוא החצי השני של אותה בעיה, וה־שלב הקשחת האבטחה של Vaultwarden באירוח עצמי מטפל בשניהם.
מדוע העתקה של db.sqlite3 בזמן ש־Vaultwarden פועל אינה גיבוי
Vaultwarden מפעיל את SQLite כברירת מחדל במצב WAL (ENABLE_DB_WAL=true). פעולת כתיבה נרשמת תחילה ב־db.sqlite3-wal, ורק פעולת checkpoint משלבת אותה ב־db.sqlite3. אם מעתיקים את db.sqlite3 לבדו, מקבלים את מסד הנתונים כפי שהיה ב־checkpoint האחרון. לכן סיסמה שנשמרה לפני עשר דקות עלולה להיעדר מהארכיון בלי שום התראה.
גם העתקה של שלושת הקבצים באמצעות cp אינה פתרון. ההעתקות מתבצעות ברגעים מעט שונים, ולכן קובץ ה־WAL שנשמר עלול לתאר גרסאות של דפים שאינן תואמות עוד לקובץ הראשי שנשמר. לאחר מכן SQLite משחזר קובץ אחד באמצעות האחר, והתוצאה שגויה. מגלים זאת רק זמן רב לאחר מכן:
Error: database disk image is malformed.backup מונע זאת משום שהוא משתמש ב־SQLite Online Backup API, ש־SQLite מתעד כמנגנון המיועד להעתקת מסד נתונים שעשוי להיות בשימוש פעיל. הוא קורא את הדפים תחת נעילת קריאה, ומתחיל מחדש אם תהליך כתיבה משנה את הקובץ בזמן ההעתקה. לכן מה שנכתב לדיסק מייצג נקודת זמן עקבית אחת.
יצירת עותק של מסד הנתונים באמצעות 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 ואת אסימון הניהול שלכם. שמרו את האסימון כמחרוזת PHC של Argon2id (password hashing competition), ולא כטקסט פשוט. docker run --rm -it vaultwarden/server /vaultwarden hash מדפיס עבורכם מחרוזת כזאת.
rsa_key.pem חותם על אסימוני JSON Web Token (JWT) שמשאירים את הלקוחות מחוברים. אם הקובץ חסר בעת ההפעלה, Vaultwarden יוצר מפתח חדש. לכן כל אסימון שנחתם במפתח הישן מפסיק לעבור אימות, וכל הלקוחות מתנתקים. תוכן הכספת נשאר תקף, משום שהוא מוצפן באמצעות מפתחות שנגזרים מסיסמת האב. שחזור קובץ המפתח מונע את ההתנתקות ההמונית.
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 מסתיים בקוד 0 גם כאשר PRAGMA integrity_check מדווח על פגימות, ולכן השוואת הפלט ל־ok היא שהופכת עותק פגום לכשל בסקריפט. לאחר מכן set -euo pipefail עוצר הכול, במקום לאפשר ל־tar ליצור ארכיון מסודר סביב מסד נתונים פגום.
הפקודה הסופית tar -tzf מציגה את מה שנלכד בפועל. קראו את הפלט בפעם הראשונה. חפשו את ./db.sqlite3, את ./rsa_key.pem, את ./config.json ואת ./attachments/, וכן ודאו ש־./db.sqlite3-wal אינו מופיע. הפעילו את הסקריפט מדי לילה באמצעות שירות ו־timer של systemd במקום cron, אם דרושים לכם פלט journalctl ויחידה שמדווחת על כשל.
שחזרו את הגיבוי לספריית עבודה זמנית כדי לאמת אותו
גיבוי שלא נבדק הוא רק השערה. שחזור לספריית עבודה זמנית נמשך דקה ואינו נוגע בנתונים פעילים.
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. מספר המשתמשים תואם למספר החשבונות הידועים לכם. מספר הצפנים קרוב לנתון הפעיל שמתקבל מ־sudo sqlite3 /opt/vaultwarden/data/db.sqlite3 "select count(*) from ciphers;", ולעולם אינו אפס בכספת שנמצאת בשימוש. ספריית הקבצים המצורפים היא בערך בגודל הצפוי; אפשר לדלג על בדיקה זו אם אף אחד אינו מעלה קבצים מצורפים. לאחר מכן הריצו את 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 vaultwardenה־chown חייב לציין את המשתמש שהמכולה פועלת באמצעותו. תמונת ברירת המחדל פועלת כ־root, לכן root:root מתאים, אלא אם הגדרתם את user: בקובץ ה־compose. במקרה כזה, השתמשו ב־uid וב־gid שהגדרתם. תיקיית נתונים שהשרת אינו יכול לכתוב אליה תציג דף התחברות שבו כל בקשה נכשלת, והלוגים יציינו זאת.
הפעלה תקינה מסתיימת בשורת Rocket:
[INFO] Rocket has launched from http://0.0.0.0:80לאחר מכן התחברו בדפדפן, פתחו פריט והורידו קובץ מצורף אחד. אם ההתחברות מצליחה אך הורדת קבצים מצורפים נכשלת, המשמעות היא שהארכיון כלל את מסד הנתונים אך לא את attachments/. השאירו את data.old.* עד שכל הבדיקות יסתיימו בהצלחה, ולאחר מכן מחקו אותו. שחזור למצב הקודם מתבצע באמצעות אותם שלושה שלבים, אך בהחלפת התיקיות בכיוון ההפוך.
אם הנתיבים שלכם שונים מאלה שמופיעים כאן, מדריך ההתקנה של Vaultwarden ב־VPS מציג את קובץ ה־compose שעליו פקודות אלה מסתמכות.
היכן לא לשמור את הגיבוי
- לא באותו דיסק שבו נמצאת תיקיית הנתונים. כשל בנפח אחסון אחד ישבית את שני העותקים, וכך גם
rm -rfאחד בנתיב שגוי. - לא באותו שרת, אפילו לא בנפח אחסון שני. תוקף שמקבל גישת
rootמגיע לגיבויים באותו סשן. - לא באחסון אובייקטים ללא הצפנה, משום שהארכיון מכיל כתובות דוא"ל, רמזים לסיסמאות, קודי שחזור וטקסט מוצפן של כספת, שאפשר לתקוף באופן לא מקוון.
- לא רק ב־snapshots של ספק האירוח. השחזור שלהם מהיר, ולכן כדאי להשתמש בהם, אך הם נמצאים באותו חשבון כמו השרת. בעיה בחשבון תשבית גם אותם.
עותק מחוץ לאתר הוא המקום שבו 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 --pruneהפנו את restic לתיקיית הארכיון ולא לתיקיית הנתונים הפעילה, כדי שהוא יעלה את העותק העקבי שכבר בדקתם. שמרו את סיסמת המאגר במקום שאינו השרת שעליו היא מגינה. אם הסיסמה תאבד, לא יהיה אפשר לקרוא את ה־snapshots, בהתאם לתכנון. כאשר האחסון תומך בכך, העניקו לשרת פרטי גישה שמאפשרים כתיבה אך לא מחיקה, כדי שפריצה לשרת לא תאפשר למחוק את היסטוריית הגיבויים שלו. הגדרת גיבויים באמצעות restic ב־VPS מסביר במלואו על המאגר ועל לוח הזמנים, ו־השוואה בין restic לבין BorgBackup מסביר כיצד לבחור אם עדיין לא החלטתם.
בדקו את השחזור לפי לוח זמנים
בחרו יום אחד בכל חודש. הורידו את ה־snapshot העדכני ביותר לתיקיית עבודה זמנית באמצעות restic restore latest --tag vaultwarden --target /tmp/vw-check, הריצו את אותו PRAGMA integrity_check, בצעו את אותן ספירות רשומות, ולאחר מכן תעדו את התאריך ואת הספירות. גיבוי שלא שוחזר במשך שישה חודשים הוא גיבוי שמצבו אינו ידוע. את מצבו תלמדו במהלך תקלה, וזהו העיתוי הגרוע ביותר לגלות זאת.
אחת לשנה בצעו את הבדיקה המלאה. הפעילו container נוסף של Vaultwarden בפורט חלופי, עם תיקיית הנתונים ששוחזרה, והתחברו באמצעות חשבון אמיתי. כך תוכיחו שמסלול הסיסמה הראשית פועל מקצה לקצה, דבר שספירת רשומות אינה יכולה להוכיח. restic check --read-data-subset=10% באותו לוח זמנים מוודא שהנתונים המאוחסנים ניתנים לקריאה, ולא רק שמופיעים ברשימה.
FAQ
האם אפשר להעתיק את db.sqlite3 באמצעות cp בזמן ש־Vaultwarden פועל?
לא. Vaultwarden מפעיל את SQLite במצב WAL, ולכן כתיבות עדכניות נמצאות ב־db.sqlite3-wal ועדיין אינן נמצאות ב־db.sqlite3. cp של הקובץ הראשי בלבד מאבד אותן בשקט, והעתקה נפרדת של שני הקבצים עלולה ליצור זוג שאינו תואם, והבעיה תתגלה מאוחר יותר כ־Error: database disk image is malformed. השתמשו במקום זאת ב־sqlite3 /path/db.sqlite3 ".backup '/path/out.sqlite3'". הוא משתמש ב־Online Backup API של SQLite ומפיק קובץ עקבי אחד, בזמן שהשרת ממשיך לספק שירות.
האם חובה לעצור את מכולת Vaultwarden כדי ליצור גיבוי?
לא, וזו בדיוק מטרתו של .backup. העתקת מסד הנתונים בטוחה בשרת פועל. קבצי Attachments ו־Send נכתבים כאשר משתמש מעלה אותם, ולכן קובץ שנוסף בין העתקת מסד הנתונים לבין tar עלול להיעדר מהארכיון של אותו לילה. במקרה הגרוע, הדבר עולה לכם בקובץ Attachment אחד. אם השבתה של כמה שניות אינה מפריעה לכם, docker compose stop לפני הסקריפט ו־docker compose start אחריו מונעים גם את האפשרות הזאת.
מה קורה אם משחזרים בלי קובצי rsa_key?
Vaultwarden יוצר מפתח חדש בעת האתחול. המפתח חותם על אסימוני JSON web (JWT) שמחזיקים את ההפעלות פעילות, ולכן כל אסימון קיים מפסיק לעבור אימות. כל הלקוחות מנותקים ועליהם להתחבר מחדש. תוכן הכספת אינו מושפע, משום שהוא מוצפן באמצעות מפתחות הנגזרים מסיסמת־האב של כל משתמש, ולא באמצעות מפתח RSA. שחזרו את rsa_key.pem יחד עם שאר תיקיית הנתונים, ואף אחד לא יבחין בשחזור.
האם ארכיון הגיבוי בטוח להעלאה לאחסון אובייקטים כפי שהוא?
לא. שמות פריטים, סיסמאות והערות הם טקסט מוצפן, אך כתובות דוא"ל, שמות חשבונות, רמזים לסיסמאות וקודי שחזור של אימות דו־שלבי נשמרים כטקסט גלוי במסד הנתונים. תוקף שפועל במצב לא מקוון יכול לנסות לפצח את הטקסט המוצפן בקצב שלו. הצפינו את הארכיון לפני שהוא יוצא מהמחשב. מאגר restic עושה זאת עבורכם, ו־gpg --symmetric --cipher-algo AES256 vw-20260805-030000.tar.gz מפיק קובץ מוצפן יחיד שאפשר למסור לכל שירות אחסון.
כיצד מגבים את Vaultwarden ב־PostgreSQL או ב־MariaDB?
השלבים עבור SQLite אינם חלים, והפקודה המובנית מסרבת עם The database type is not SQLite. Backups only works for SQLite databases. השליכו את מסד הנתונים באמצעות הכלי המקורי שלו, pg_dump או mysqldump, ושמרו על כל שאר הכללים. קובץ ה־dump צריך להיכלל בארכיון אחד עם attachments/, sends/, config.json וקובצי rsa_key, כשהכול נוצר באותה הרצה, מוצפן ומאוחסן במקום שאינו השרת שיצר אותו.