Как сделать резервную копию Vaultwarden на VPS
Скопируйте базу Vaultwarden через sqlite3 .backup, сохраните attachments, config.json и rsa_key, затем проверьте восстановление до сбоя.
Что должно входить в резервную копию Vaultwarden
Резервная копия Vaultwarden — это копия всей папки данных. Базу данных внутри неё нужно копировать правильно. Выполняйте sqlite3 db.sqlite3 ".backup out.sqlite3" вместо cp, потому что обычная копия базы данных во время записи может оказаться непригодной для открытия. Также сохраняйте файлы, расположенные рядом с базой данных. Именно о них часто забывают.
При установке в Docker папкой данных считается каталог, подключённый к /data. Это может быть путь на хосте или именованный том. Разница между bind mount и именованными томами определяет, где на диске фактически находятся данные хранилища. В папке находятся следующие файлы и каталоги.
db.sqlite3: все учётные записи, элементы хранилища, папки и организации. Потеря этого файла означает потерю хранилища.db.sqlite3-walиdb.sqlite3-shm: write-ahead log (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.
Остальная часть базы данных не шифруется. Адреса электронной почты, имена учётных записей, подсказки к паролям и коды восстановления двухфакторной аутентификации хранятся в открытом виде вместе с метаданными, например временем создания и информацией о том, какой организации принадлежит элемент. Поэтому сам файл резервной копии является секретом. Любой, у кого он есть, узнает, кто пользуется системой, и сможет атаковать зашифрованные данные автономно с максимальной скоростью, которую позволяет его оборудование. Именно поэтому ниже заданы такие правила хранения: перед передачей копии за пределы сервера её необходимо зашифровать.
Почему копирование 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 этот API указан как способ копирования базы данных, которая может активно использоваться. API считывает страницы с блокировкой на чтение. Если во время операции записи изменяют файл, 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Вместо этого выполните ее на хосте для подключенного пути. Именно это делают приведенные выше команды. Если данные хранятся в именованном томе, команда docker volume inspect <name> выводит путь на хосте в каталоге /var/lib/docker/volumes/.
Начиная с версии 1.32.1, Vaultwarden также поставляется с собственной командой резервного копирования. На сервере выполните:
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 создаёт новый ключ. Поэтому все токены, подписанные старым ключом, перестают проходить проверку, и все клиенты выходят из системы. Содержимое хранилища при этом сохраняется, поскольку оно зашифровано ключами, производными от мастер-пароля. Восстановление файла ключа предотвращает массовый выход из системы.
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 в выводе и unit, сообщающий об ошибке.
Проверьте резервную копию, восстановив её во временный каталог
Непроверенная резервная копия — это только предположение. Восстановление во временный каталог занимает минуту и не затрагивает рабочие данные.
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 подходит, если только в compose file не задан параметр user:. В этом случае используйте указанные там uid и gid. Если сервер не может записывать в каталог данных, страница входа будет открываться, но каждый запрос будет завершаться ошибкой. В логах это будет указано.
Успешный запуск завершается строкой Rocket:
[INFO] Rocket has launched from http://0.0.0.0:80Затем войдите через браузер, откройте один элемент и скачайте одно вложение. Если вход выполняется, но скачивание вложений завершается ошибкой, это означает, что в архиве была база данных, но не было attachments/. Сохраняйте data.old.*, пока не проверите всё перечисленное, затем удалите его. Откат выполняется теми же тремя шагами, но каталоги используются в обратном порядке.
Если ваши пути отличаются от указанных здесь, в руководстве по установке Vaultwarden на VPS показан compose file, который предполагают эти команды.
Куда не следует помещать резервную копию
- Не на тот же диск, что и каталог с данными. Один отказ тома уничтожит обе копии. То же произойдёт при одном
rm -rf, выполненном не по тому пути. - Не на тот же сервер, даже на второй том. Злоумышленник, получивший доступ к root, в том же сеансе получит доступ и к резервным копиям.
- Не в объектное хранилище без шифрования, поскольку архив содержит адреса электронной почты, подсказки для паролей, коды восстановления и зашифрованный текст хранилища, который можно атаковать в автономном режиме.
- Не только в snapshots вашего провайдера. Они быстро восстанавливаются, поэтому их стоит использовать. Но они находятся в том же аккаунте, что и сервер, поэтому проблема с аккаунтом затронет и их.
Для offsite-копии подходит restic, поскольку repository 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 каталог с архивом, а не каталог с рабочими данными. Тогда он загрузит согласованную копию, которую вы уже проверили. Храните пароль repository не на защищаемом сервере. Если потерять этот пароль, snapshots невозможно прочитать — это предусмотрено конструкцией. Если хранилище это поддерживает, выдайте серверу credentials, которые позволяют записывать данные, но не удалять их. Тогда при компрометации сервера злоумышленник не сможет удалить историю резервных копий. В материале Настройка резервного копирования restic на VPS полностью описаны repository и расписание. В материале restic в сравнении с BorgBackup разобран выбор, если вы ещё его не сделали.
Проверяйте восстановление по расписанию
Выберите один день в месяц. Загрузите последний snapshot во временный каталог с помощью restic restore latest --tag vaultwarden --target /tmp/vw-check, выполните ту же PRAGMA integrity_check и подсчитайте те же строки. Затем запишите дату и полученные количества. Резервная копия, из которой никто не выполнял восстановление в течение шести месяцев, имеет неизвестное состояние. Это состояние выяснится во время сбоя, то есть в самый неподходящий момент.
Раз в год выполняйте полную проверку. Запустите второй контейнер Vaultwarden на свободном порту с восстановленным каталогом данных и войдите в него с помощью реальной учетной записи. Это проверяет весь путь обработки master password, чего нельзя подтвердить подсчетом строк. 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'". Эта команда использует 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 вместе с остальными данными из папки данных, и никто не заметит восстановление.
Безопасно ли загружать архив резервной копии в object storage без дополнительной защиты?
Нет. Названия элементов, пароли и заметки хранятся в зашифрованном виде, но адреса электронной почты, имена учётных записей, подсказки для паролей и коды восстановления двухфакторной аутентификации находятся в базе данных в открытом виде. Кроме того, атакующий, получивший автономный доступ к данным, может подбирать ключи шифрования в удобном для себя темпе. Зашифруйте архив до его передачи с машины. Это делает за вас repository 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. Все остальные правила остаются прежними. Дамп должен находиться в одном архиве с attachments/, sends/, config.json и файлами rsa_key. Получите их в рамках одного запуска, зашифруйте архив и сохраните его не на том сервере, где он был создан.