MAATRIX / Блог / Vaultwarden: бэкап и восстановление паролей

Vaultwarden: бэкап и восстановление паролей

Vaultwarden: бэкап и восстановление паролей

MAATRIX

Свой Vaultwarden удобен ровно до того дня, когда сервер не поднимается. Бэкап здесь нестандартный: db.sqlite3 нельзя просто скопировать на ходу, вложения лежат вообще не в базе, а без rsa_key.pem все клиенты разом попросят логин заново. Разберём, что копировать, как снять горячую копию без остановки контейнера, чем шифровать и как убедиться, что восстановление действительно сработает.

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Что лежит в каталоге данных и что из этого критично

Vaultwarden хранит всё в одном каталоге — /data внутри контейнера, обычно смонтированном в /opt/vaultwarden/data; для нативной установки путь задаёт переменная DATA_FOLDER. Команда du -sh /opt/vaultwarden/data/* | sort -h на семейном инстансе с 300–400 записями даёт примерно такое:

2.8M    /opt/vaultwarden/data/db.sqlite3
19M     /opt/vaultwarden/data/attachments
41M     /opt/vaultwarden/data/sends
187M    /opt/vaultwarden/data/icon_cache

Пропорции показательные: сама база — три мегабайта, а больше половины каталога занимает мусор, который восстанавливать не нужно.

ПутьЧто внутриБэкапить
db.sqlite3пользователи, записи, папки, организации, метаданные вложенийобязательно, только горячей копией
db.sqlite3-wal, db.sqlite3-shmжурнал WAL и разделяемая памятьнет, это состояние живой БД
attachments/зашифрованные файлы-вложенияобязательно, в базе их нет
sends/файлы Bitwarden Sendда, если Send используется
rsa_key.pemприватный ключ подписи JWTобязательно
config.jsonнастройки, сделанные через /adminобязательно
icon_cache/скачанные фавиконки сайтовнет, отрастёт сам
tmp/временные файлы загрузокнет

В старых установках рядом лежат rsa_key.der и rsa_key.pub.der — забирайте всё, что начинается с rsa_key. Потеря этого файла не уничтожает пароли, хранилище зашифровано ключом от мастер-пароля, но обнуляет все выданные токены: каждый клиент попросит вход заново вместе со вторым фактором.

И то, о чём забывают почти все: каталог данных без .env — это половина бэкапа. В .env и docker-compose.yml живут ADMIN_TOKEN, DOMAIN, настройки SMTP и пара PUSH_INSTALLATION_ID / PUSH_INSTALLATION_KEY. Восстановив только data, вы получите хранилище без писем, без пушей и без входа в админку.

Почему `cp db.sqlite3` даёт битую копию

Vaultwarden держит SQLite в режиме WAL — переменная ENABLE_DB_WAL по умолчанию true. Свежие транзакции какое-то время живут не в db.sqlite3, а в файле db.sqlite3-wal. Отсюда два способа испортить себе бэкап.

Скопировали только основной файл — получили базу без последних изменений: пароли, добавленные за минувшие часы, в копии просто отсутствуют, и заметите вы это уже после восстановления.

Скопировали все три файла на ходу — получили несогласованный набор: cp читает мегабайты не мгновенно, и конец файла относится к другому моменту, чем начало. SQLite такое обычно распознаёт, и при первом обращении вы увидите одно из:

Error: database disk image is malformed
Error: file is not a database
Error: near line 1: database is locked

Правильный способ — встроенный онлайн-бэкап SQLite: он берёт корректные блокировки и отдаёт один цельный файл, в который WAL уже влит.

sqlite3 /opt/vaultwarden/data/db.sqlite3 ".backup '/var/backups/vaultwarden/db.sqlite3'"

Контейнер останавливать не нужно — на базе в несколько мегабайт команда отрабатывает меньше чем за секунду. Альтернатива — VACUUM INTO: она дополнительно сжимает базу, но откажется писать поверх существующего файла с Error: output file already exists.

Сделать это «изнутри» контейнера не выйдет:

OCI runtime exec failed: exec: "sqlite3": executable file not found in $PATH: unknown

Образ vaultwarden/server минимальный, консольного клиента SQLite в нём нет. Ставьте его на хост (apt install -y sqlite3) и работайте с примонтированным каталогом напрямую: файл тот же самый, блокировки общие для всех процессов.

Копия без проверки бэкапом не считается. Две команды сразу после снятия:

sqlite3 /var/backups/vaultwarden/db.sqlite3 'PRAGMA integrity_check;'   # ждём ровно: ok
sqlite3 /var/backups/vaultwarden/db.sqlite3 \
  'select (select count(*) from users) u, (select count(*) from ciphers) c,
          (select count(*) from attachments) a;'

Запомните эти три числа: если завтра ciphers уменьшится вдвое, вы узнаете о проблеме до восстановления, а не во время.

Развернуть за пару минут

Готовый образ на VPS MAATRIX: NVMe, AMD EPYC, root-доступ. Локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Развернуть Vaultwarden

Скрипт бэкапа: горячая копия, вложения, шифрование

Логика простая: снять согласованную копию базы в промежуточный каталог, положить туда же мелкие критичные файлы, а вложения и Send забрать прямо из каталога данных.

#!/usr/bin/env bash
set -Eeuo pipefail

DATA=/opt/vaultwarden/data
APP=/opt/vaultwarden
STAGE=/var/backups/vaultwarden

install -d -m 700 "$STAGE"
rm -f "$STAGE/db.sqlite3"
sqlite3 "$DATA/db.sqlite3" ".backup '$STAGE/db.sqlite3'"
sqlite3 "$STAGE/db.sqlite3" 'PRAGMA integrity_check;' | grep -qx ok

cp -a "$DATA"/rsa_key.* "$DATA/config.json" "$STAGE/" 2>/dev/null || true
cp -a "$APP/.env" "$APP/docker-compose.yml" "$STAGE/" 2>/dev/null || true

export RESTIC_REPOSITORY="s3:s3.eu-central-003.backblazeb2.com/vw-backup"
export RESTIC_PASSWORD_FILE=/root/.restic-pass
restic backup --tag vaultwarden "$STAGE" "$DATA/attachments" "$DATA/sends"
restic forget --tag vaultwarden \
  --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune

Строка с grep -qx ok не косметика: при set -e скрипт остановится, если integrity_check вернул что угодно кроме ok, и заведомо мёртвая копия в репозиторий не уедет. Каталог icon_cache в список не попал — те самые 187 МБ мусора, которые незачем гонять по сети каждую ночь.

Шифрование здесь не паранойя. В db.sqlite3 лежат адреса почты всех пользователей, Argon2-хеши мастер-паролей, зашифрованные ключи организаций и полная структура хранилища. Плюс rsa_key.pem, которым подписываются токены: имея его и копию базы, можно поднять свой Vaultwarden и разбирать чужие данные офлайн, без спешки. Поэтому копия либо в restic (там AES-256 включён всегда и отключить его нельзя), либо через age: tar -C /var/backups -cf - vaultwarden | age -R /root/backup-key.pub > vw-$(date -u +%F).tar.age.

Пароль от репозитория и приватный ключ age держите вне этого сервера. Классическая ловушка: единственная копия пароля от бэкапа хранится в том самом Vaultwarden, который вы восстанавливаете. Остальные грабли шифрованных копий — потерянный ключ, забытый --password-file, репозиторий без проверки — разобраны отдельно в материале бэкап с шифрованием: частые ошибки.

Если инстанс переведён на PostgreSQL через DATABASE_URL, база снимается иначе, а вложения — так же, они всё равно на диске:

docker exec -t vw-db pg_dump -U vaultwarden -Fc vaultwarden > "$STAGE/db.dump"

Автоматизация и вывоз копии за пределы сервера

Ручной бэкап живёт недели три. Дальше — таймер systemd: он пишет в журнал, догоняет пропущенные запуски и не зависит от почты root.

# /etc/systemd/system/vaultwarden-backup.service
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/vw-backup.sh
Nice=10
IOSchedulingClass=idle

# /etc/systemd/system/vaultwarden-backup.timer
[Timer]
OnCalendar=*-*-* 03:30:00
RandomizedDelaySec=15m
Persistent=true

[Install]
WantedBy=timers.target
systemctl daemon-reload
systemctl enable --now vaultwarden-backup.timer
systemctl list-timers vaultwarden-backup.timer
journalctl -u vaultwarden-backup.service -n 50 --no-pager

Persistent=true запустит задание после включения сервера, если в 03:30 он был выключен. RandomizedDelaySec=15m разводит нагрузку, когда серверов несколько.

Тем, кто остался на cron, — про главную грабли. Cron даёт урезанный PATH и не читает ваш профиль, поэтому переменные restic до скрипта не доезжают, и в почте вы находите Fatal: Please specify repository location (-r or --repository-file). Лечится явным экспортом переменных внутри самого скрипта (как в примере выше) и абсолютными путями к бинарникам.

Копия обязана лежать не на этом сервере. Диск умирает вместе с хранилищем, а неудачный rm -rf уносит и данные, и бэкап рядом. Варианты: второй VPS в другой локации (что под это брать — в разборе VPS для бэкапов и архива), объектное хранилище S3/B2, домашний NAS через rclone. Честный риск: если сервер скомпрометирован, ключ restic из его памяти позволяет стереть и удалённый репозиторий — спасает только rest-server --append-only на приёмнике или ключ приложения B2 без права удаления.

Молчаливо сломавшийся бэкап хуже отсутствующего. Добавьте в конец скрипта пинг в мониторинг — curl -fsS -m 10 https://hc-ping.com/<uuid> — и узнаете о проблеме наутро, а не через полгода.

Восстановление: порядок действий и проверка

Порядок важен: сначала остановить сервис, потом трогать файлы. Копирование базы под работающим Vaultwarden даёт ту же несогласованность, только теперь в боевых данных.

cd /opt/vaultwarden && docker compose down
mv data data.broken
restic restore latest --tag vaultwarden --target /tmp/vw-restore

S=/tmp/vw-restore/var/backups/vaultwarden
install -d -m 750 data
cp -a "$S"/db.sqlite3 "$S"/rsa_key.* "$S"/config.json data/
cp -a /tmp/vw-restore/opt/vaultwarden/data/{attachments,sends} data/
rm -f data/db.sqlite3-wal data/db.sqlite3-shm
chown -R root:root data
docker compose up -d

Две строки чаще всего пропускают. Первая — удаление -wal и -shm: восстановленная через .backup база самодостаточна, а журнал от прежней установки здесь лишний. Вторая — chown. Официальный образ работает от root, и если архив распаковывали из-под обычного пользователя, контейнер стартует и сразу ляжет, а в docker compose logs vaultwarden окажется unable to open database file. Этот и соседние симптомы после восстановления — readonly database, 502 на прокси, пустая /admin — разобраны в статье Vaultwarden на сервере: частые ошибки.

Успешный старт выглядит однозначно: в логе появляется строка вида Rocket has launched from http://0.0.0.0:80, а curl -s http://127.0.0.1:8080/alive возвращает текущее время в UTC.

Дальше — сверка вложений. В базе и на диске их должно быть поровну:

sqlite3 /opt/vaultwarden/data/db.sqlite3 'select count(*) from attachments;'
find /opt/vaultwarden/data/attachments -type f | wc -l

Расхождение значит, что записи в хранилище есть, а файлы под ними отдадут 404 при попытке скачать.

Главное правило: бэкап, который ни разу не восстанавливали, бэкапом не является. Раз в квартал разворачивайте копию в песочнице на отдельном порту и входите в неё браузером:

docker run --rm -p 8080:80 -v /srv/vw-test/data:/data \
  -e DOMAIN=http://localhost:8080 -e PUSH_ENABLED=false \
  vaultwarden/server:1.34.3

Три детали, каждая из которых меняет результат проверки. DOMAIN обязательно свой, иначе тестовый инстанс начнёт слать письма и пуши от имени боевого; PUSH_ENABLED=false страхует от того же. И тег образа берите тот же, что в проде, а не latest: свежая версия прогонит миграции по восстановленной базе, и вы проверите не свою копию, а её обновлённый вариант. Узнать текущий тег: docker inspect --format '{{.Config.Image}}' vaultwarden. Пять минут — и вы знаете ответ на единственный важный вопрос: открываются ли пароли.

Экспорт из клиента и то, чего бэкап не спасёт

Серверная копия решает не все задачи, и границы стоит очертить заранее.

Экспорт из веб-хранилища даёт файл, читаемый без Vaultwarden вообще, но у него три ограничения. Вложения в экспорт не попадают — ни в JSON, ни в CSV. Записи организаций выгружаются отдельно и только владельцем. И самое коварное — выбор формата: вариант «привязанный к аккаунту» зашифрован ключом вашей учётной записи, и импортировать его в новый аккаунт после потери мастер-пароля невозможно. Для офлайн-копии годится только «защищённый паролем», где парольную фразу задаёте вы, — либо нешифрованный JSON, который в ту же секунду становится файлом с паролями открытым текстом и жить на диске не должен.

Забытый мастер-пароль не восстанавливается ничем. Vaultwarden его не хранит и расшифровать хранилище не может: страница /admin умеет только удалить пользователя вместе с данными. Бэкап здесь бесполезен — вы восстановите тот же нечитаемый контейнер. Работает лишь подготовка: подсказка к паролю (нужен настроенный SMTP) и Emergency Access, где доверенный контакт через заданный период ожидания получает право на takeover.

Двухфакторка отдельно. Если TOTP-код от самого Vaultwarden лежит внутри Vaultwarden, при переезде вы получите замкнутый круг. Держите резервные коды 2FA вне хранилища; в крайнем случае второй фактор снимает администратор через /admin.

Обновления и откаты. Миграции схемы односторонние: после запуска новой версии база обновляется, и вернуть старый тег образа уже нельзя — сервер откажется работать с более новой схемой. Перед docker compose pull снимайте копию базы отдельной командой: тридцать секунд и единственный путь назад.

Какой сервер под Vaultwarden с бэкапами взять в MAATRIX

Vaultwarden написан на Rust и в простое почти ничего не ест: docker stats показывает 55–80 МиБ на контейнер при десятке пользователей. Ресурсы забирает не он, а TLS-терминация, база и особенно обслуживание бэкапов.

Минимум: 1 vCPU, 1 ГБ RAM, 20 ГБ NVMe. Хватит на семью или команду до 10–15 человек с базой в несколько мегабайт. Честное ограничение: на 1 ГБ операция restic prune по репозиторию с гигабайтами вложений упирается в память и рискует быть убитой по OOM — там либо --max-repack-size 1G, либо ежедневный forget без prune и полная чистка раз в месяц. Локальный кеш restic в /root/.cache/restic тоже отъедает сотни мегабайт диска — при 20 ГБ это заметно.

Комфортный вариант: 2 vCPU, 2–4 ГБ RAM, 40 ГБ NVMe. Здесь помещаются Vaultwarden, Nginx или Caddy с сертификатом, PostgreSQL вместо SQLite, если хранилище растёт, вложения на десятки гигабайт и, главное, тестовый инстанс для проверки восстановления рядом с боевым.

Локация — Великобритания (Лондон). Менеджер паролей — это персональные данные сотрудников, и для команды, работающей с европейскими контрагентами, соседство с GDPR-периметром и предсказуемая юрисдикция важнее пары миллисекунд. Пинг из Западной Европы — 10–30 мс, из Москвы до Лондона порядка 45–60 мс, для хранилища паролей это незаметно. Если аудитория целиком российская и вы подпадаете под 152-ФЗ, берите RU; если основной поток из Франции и Бенилюкса — FR. И правило, ради которого всё затевалось: бэкап держите не в той же локации — сервер в UK, репозиторий restic в US или во внешнем S3.

Приложение из каталога apps.maatrix.io ставится на сервер автоматически при заказе — вставлять команды руками не нужно, автоустановка работает на Ubuntu и Debian. Доступы к готовому Vaultwarden появляются в личном кабинете, в разделе «Доступ»; вам останется завести первого пользователя, закрыть регистрацию через SIGNUPS_ALLOWED=false и в первый же вечер поставить таймер бэкапа из этой статьи. Если разворачиваете вручную и хотите пройти путь с нуля — порядок действий в материале как установить и настроить Vaultwarden на VPS, конфигурации и локации — на странице аренды VPS. Оплата — картой российского банка, по СБП, криптовалютой или токеном MAAT; зарубежная карта для британской локации не нужна.

Развернуть за пару минут

Готовый образ на VPS MAATRIX: NVMe, AMD EPYC, root-доступ. Локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.

Развернуть Vaultwarden

Обсудить статью, задать вопрос или начать новую тему

Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.

Перейти в сообщество →

Частые вопросы

Можно ли делать бэкап, не останавливая контейнер?

Да, если снимать базу командой sqlite3 db.sqlite3 ".backup '...'" — она берёт корректные блокировки и отдаёт согласованный файл. А вот cp или tar по живому каталогу дают битую копию: часть транзакций остаётся в db.sqlite3-wal.

Что будет, если восстановить базу без rsa_key.pem?

Пароли не пострадают — хранилище зашифровано ключом пользователя. Но Vaultwarden сгенерирует новый ключ подписи, все выданные токены станут невалидными, и каждое устройство попросит вход заново вместе со вторым фактором.

Я потерял мастер-пароль. Поможет ли бэкап сервера?

Нет. Данные зашифрованы на клиенте, и в копии лежит тот же нечитаемый контейнер; страница /admin умеет только удалить пользователя. Спасает лишь заранее настроенный Emergency Access или отдельно сохранённый экспорт «защищённый паролем».

Нужны сами нейросети для контента?

Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.