Хранение паролей в современном цифровом окружении превратилось в серьёзную инженерную задачу. Браузерные хранилища ненадёжны и привязаны к одной программе. Файлы Excel и текстовые блокноты с записями паролей стали уже почти народной шуткой. Облачные сервисы вроде LastPass или 1Password удобны, но регулярно попадают в новостные ленты с очередной утечкой или сменой ценовой политики, заставляющей платить всё больше за те же возможности. На этом фоне самохостимые менеджеры паролей выглядят разумной серединой - данные остаются под собственным контролем, а удобство клиентов сохраняется.

Bitwarden - один из самых популярных менеджеров паролей с открытым исходным кодом, и у него есть серверная версия для самостоятельной установки. Но официальный сервер Bitwarden написан на C# и требует значительных ресурсов - десятки контейнеров, базы данных уровня enterprise, серьёзные требования к памяти и процессору. Для домашнего использования или маленькой команды это явный перебор.

И тут на сцену выходит Vaultwarden - неофициальный порт серверной части Bitwarden, переписанный на Rust. Проект полностью совместим с официальными клиентами Bitwarden (браузерные расширения, мобильные приложения, десктопные программы), но при этом потребляет в разы меньше ресурсов. Один маленький бинарник вместо десятка контейнеров. Десятки мегабайт памяти вместо нескольких гигабайт. Возможность развернуть на самом скромном VPS за копейки.

Vaultwarden реализует практически все возможности оригинального Bitwarden - поддержку организаций для совместной работы, прикрепление файлов к записям, встроенный аутентификатор для двухфакторной защиты, поддержку аппаратных ключей U2F и YubiKey, интеграцию с Duo, работу с сервисами одноразовых email-адресов. Ниже разобран полный путь установки на Ubuntu 22.04 через Docker Compose с обратным прокси Caddy, который автоматически выпустит и будет обновлять TLS-сертификат от Let's Encrypt.

Подготовительные требования и базовое окружение для развёртывания собственного хранилища паролей

Чтобы пройти этот путь без сюрпризов, понадобится сервер на Ubuntu 22.04. Доступ нужен с правами обычного пользователя, у которого есть возможность повышать привилегии через sudo. В системе должен быть включён и работать брандмауэр UFW. Также необходимо полное доменное имя (FQDN), направленное A-записью на IP сервера - в этом руководстве используется условный vaultwarden.example.com, заменяемый на свой реальный.

Ресурсы для Vaultwarden скромные. Минимум 1 ГБ оперативной памяти и 10 ГБ дискового пространства - этого хватит для семейного использования или небольшой команды. Реальный расход памяти после старта обычно не превышает 200 МБ, что делает проект идеальным кандидатом для самого дешёвого VPS.

Обновляем пакеты до последних версий перед началом:

$ sudo apt update && sudo apt upgrade

Команда сначала обновляет индекс репозиториев, а затем устанавливает все доступные обновления. Привычка делать update перед серьёзными работами экономит часы будущих разбирательств с устаревшими версиями.

Открытие нужных портов в брандмауэре UFW для веб-доступа к менеджеру паролей

Любая публикация веб-сервиса начинается с открытия нужных портов. Vaultwarden будет доступен через стандартные 80 и 443 - первый используется только для редиректа на HTTPS и для прохождения проверки Let's Encrypt по протоколу ACME, основной трафик идёт через 443.

Проверяем текущее состояние брандмауэра:

$ sudo ufw status

Ожидаемый вывод:

Status: active

To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW       Anywhere
OpenSSH (v6)               ALLOW       Anywhere (v6)

Видно, что разрешён только SSH - это базовая настройка любого правильно сконфигурированного сервера. Если вместо active в выводе inactive, нужно сначала включить UFW через sudo ufw enable, не забыв предварительно разрешить SSH, иначе можно потерять удалённый доступ.

Открываем порты для веб-трафика:

$ sudo ufw allow http
$ sudo ufw allow https

Использование имён http и https вместо номеров портов 80 и 443 - синтаксический сахар UFW. Брандмауэр сам подставит нужные номера из своей встроенной таблицы соответствий.

Проверяем результат:

$ sudo ufw status

Ожидаемый вывод:

Status: active

To                         Action      From
--                         ------      ----
OpenSSH                    ALLOW       Anywhere
80/tcp                     ALLOW       Anywhere
443                        ALLOW       Anywhere
OpenSSH (v6)               ALLOW       Anywhere (v6)
80/tcp (v6)                ALLOW       Anywhere (v6)
443 (v6)                   ALLOW       Anywhere (v6)

В списке появились новые правила для HTTP и HTTPS как для IPv4, так и для IPv6. Брандмауэр готов пропускать веб-запросы.

Установка Docker Engine и плагина Docker Compose из официального репозитория

Vaultwarden распространяется как официальный Docker-образ, и это самый удобный способ его развернуть. Вместо ручной сборки бинарника на Rust и подготовки systemd-юнита получаем готовый контейнер, который запускается одной командой.

Подключаем GPG-ключ Docker:

$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker.gpg

Команда скачивает публичный ключ репозитория Docker, преобразует его в бинарный формат через gpg --dearmor и сохраняет в стандартный каталог доверенных ключей. Это часть современной модели безопасности apt - каждый сторонний репозиторий должен иметь свой ключ для проверки подлинности пакетов.

Добавляем сам репозиторий:

$ echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

Конструкция $(dpkg --print-architecture) подставляет архитектуру системы (amd64 на большинстве серверов). Параметр $(lsb_release -cs) подставляет кодовое имя релиза Ubuntu (jammy для 22.04). Это позволяет одной и той же команде корректно работать на разных архитектурах и версиях системы.

Обновляем индекс пакетов:

$ sudo apt update

Ставим Docker и плагин Compose:

$ sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin

В этой строке несколько важных компонентов. Пакет docker-ce - это сам движок Docker Community Edition. docker-ce-cli - клиент командной строки, через который происходит управление контейнерами. containerd.io - низкоуровневый рантайм контейнеров, на котором работает Docker. docker-compose-plugin - вторая версия Compose, реализованная как плагин к Docker CLI.

Стоит обратить внимание - используется именно плагин, а не отдельный бинарник docker-compose. Это критичное отличие. Команда для запуска изменилась с docker-compose (через дефис) на docker compose (через пробел). Старый бинарник официально объявлен устаревшим, и поддержка его прекращена. Все примеры в этом руководстве используют новый синтаксис.

Docker по умолчанию требует прав root для всех операций. Чтобы каждый раз не писать sudo, имеет смысл добавить текущего пользователя в группу docker:

$ sudo usermod -aG docker ${USER}

Переменная ${USER} автоматически подставляется в имя текущего залогиненного пользователя. Если нужно дать права другому пользователю, заменяем ${USER} на конкретное имя.

Стоит понимать обратную сторону этой удобной возможности. Любой член группы docker фактически получает root-эквивалент - через монтирование корневой файловой системы в контейнер можно сделать что угодно с системой. Так что добавлять в эту группу нужно только тех, кому такие права действительно положены.

Применяем новые права группы:

$ su - ${USER}

Команда перезапускает оболочку с актуальными группами. Альтернатива - выйти из SSH и зайти заново. После этого команды docker работают без sudo.

Создание файла docker-compose.yml для Vaultwarden и обратного прокси Caddy

Создаём отдельный каталог для Vaultwarden:

$ mkdir vaultwarden

Переходим в него:

$ cd vaultwarden

Создаём файл Docker Compose:

$ nano docker-compose.yml

Содержимое:

version: '3'

services:
  vaultwarden:
    image: vaultwarden/server:latest
    container_name: vaultwarden
    restart: always
    environment:
      WEBSOCKET_ENABLED: "true"  # Enable WebSocket notifications.
      DOMAIN: "https://vaultwarden.example.com"
      SMTP_HOST: "<smtp.domain.tld>"
      SMTP_FROM: "<Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript.>"
      SMTP_PORT: "587"
      SMTP_SECURITY: "starttls"
      SMTP_USERNAME: "<username>"
      SMTP_PASSWORD: "<password>"
    volumes:
      - ./vw-data:/data

  caddy:
    image: caddy:2
    container_name: caddy
    restart: always
    ports:
      - 80:80  # Needed for the ACME HTTP-01 challenge.
      - 443:443
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./caddy-config:/config
      - ./caddy-data:/data
    environment:
      DOMAIN: "https://vaultwarden.example.com"  # Your domain.
      EMAIL: "Адрес электронной почты защищен от спам-ботов. Для просмотра адреса в браузере должен быть включен Javascript."                 # The email address to use for ACME registration.
      LOG_FILE: "/data/access.log"

Этот файл описывает сразу два контейнера - сам Vaultwarden и обратный прокси Caddy перед ним. Разберём всё подробно.

Параметр image указывает, какой образ использовать с Docker Hub. vaultwarden/server:latest - это официальный образ проекта, тег latest означает самую свежую версию. В боевой эксплуатации иногда лучше прибивать конкретную версию (например, vaultwarden/server:1.30.0), чтобы случайное обновление образа не сломало рабочую систему.

Параметр container_name задаёт имя контейнера для удобной работы. Без него Docker сгенерирует случайное имя вроде vaultwarden_vaultwarden_1, что неудобно при ручных операциях.

Параметр restart: always задаёт политику перезапуска. Если контейнер по любой причине упадёт, Docker автоматически поднимет его заново. Это критично для службы, которой пользователи доверяют свои пароли - даун-тайм должен быть минимальным.

Секция environment задаёт переменные окружения для контейнера. Vaultwarden настраивается именно через них, а не через классический файл конфигурации - это типичная практика для контейнерных приложений. WEBSOCKET_ENABLED включает работу через WebSocket для мгновенных уведомлений. DOMAIN указывает полный URL сайта - без него не будет работать проверка email и некоторые другие возможности.

SMTP-параметры нужны для отправки писем - подтверждения email-адресов, приглашения в организации, сброса пароля. Стоит обратить внимание на параметр SMTP_SECURITY. Если SMTP-сервер использует порт 587, значение должно быть starttls (шифрование начинается после первоначального соединения по обычному протоколу). Если порт 465, значение force_tls (соединение сразу шифруется). Перепутать эти варианты - типичная ошибка, после которой почта не работает.

Секция volumes монтирует каталоги хоста внутрь контейнера. Локальный ./vw-data становится доступен как /data внутри контейнера. Все данные Vaultwarden живут именно там - SQLite-база с записями, прикреплённые файлы, конфигурация. Без этого монтирования при перезапуске контейнера все данные потерялись бы.

Второй блок описывает Caddy - современный веб-сервер, написанный на Go, который примечателен автоматическим получением и обновлением TLS-сертификатов. Не нужно отдельно настраивать certbot или другие инструменты - Caddy сам разберётся с Let's Encrypt при первом запуске.

Секция ports пробрасывает порты с хоста в контейнер. Порт 80 нужен для прохождения проверки ACME HTTP-01, через которую Let's Encrypt подтверждает владение доменом. Порт 443 - основной HTTPS-порт, через который пойдёт весь рабочий трафик.

В каталоги ./Caddyfile, ./caddy-config и ./caddy-data Caddy сложит свою конфигурацию, кеш и сертификаты соответственно. Параметр :ro в монтировании Caddyfile означает read-only - контейнер сможет только читать этот файл, но не изменять, что повышает безопасность.

Переменная EMAIL критична - именно этот email будет использоваться при регистрации в Let's Encrypt и получении уведомлений о проблемах с сертификатом. Если ввести несуществующий адрес, можно пропустить уведомление о грядущем истечении сертификата.

Сохраняем файл через Ctrl + X и подтверждение Y.

Дополнительная настройка Vaultwarden через переменные окружения для усиления безопасности

Дефолтная конфигурация позволяет любому желающему зарегистрироваться на сервере и получить доступ. Для домашнего использования или открытого community-проекта это нормально, но в большинстве случаев хочется ограничить регистрацию только избранными.

Запрет открытой регистрации добавляется через переменную окружения:

environment:
     WEBSOCKET_ENABLED: "true"  # Enable WebSocket notifications.
	 SIGNUPS_ALLOWED: "false"
	 ....

После этого новые пользователи могут попасть в систему только через приглашения от существующих участников.

Но есть нюанс - даже при выключенной регистрации действующие пользователи в составе организации могут приглашать новых. Чтобы запретить и это, добавляется ещё одна переменная:

INVITATIONS_ALLOWED: "false"

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

SHOW_PASSWORD_HINT: "false"

Сам веб-интерфейс Vaultwarden раздаёт собственные статические файлы. Для повышенной безопасности можно отключить эту раздачу совсем (тогда работают только клиенты Bitwarden, веб-интерфейса нет):

WEB_VAULT_ENABLED: "false"

Если хочется заменить интерфейс собственной версией, можно подмонтировать свой каталог в контейнер:

volumes:
      - ./vw-data:/data
	  - /path/to/static/files_directory:/web-vault

По умолчанию Vaultwarden пишет логи только в стандартный вывод, который виден через docker logs. Иногда удобнее иметь файл логов:

LOG_FILE: "/data/vaultwarden.log"

Для уменьшения количества записей в логе можно поднять минимальный уровень и включить расширенный формат:

LOG_LEVEL: "warn"
EXTENDED_LOGGING: "true"

После этого в логе остаются только предупреждения и ошибки, рутинные информационные сообщения отсеиваются.

Создание файла Caddyfile с настройками безопасности и проксирования трафика

Caddy использует собственный формат конфигурационного файла - намного более компактный и читаемый, чем у Nginx или Apache. Создаём его в том же каталоге, что и docker-compose.yml:

$ nano Caddyfile

Содержимое:

{$DOMAIN}:443 {
  log {
    level INFO
    output file {$LOG_FILE} {
      roll_size 10MB
      roll_keep 10
    }
  }

  # Use the ACME HTTP-01 challenge to get a cert for the configured domain.
  tls {$EMAIL}

  # This setting may have compatibility issues with some browsers
  # (e.g., attachment downloading on Firefox). Try disabling this
  # if you encounter issues.
  encode gzip

  # The file size is set to 500MB to support the Vaultwarden (Bitwarden) Send feature.
  request_body {
       max_size 500MB
  }
  
  header {
       # Enable cross-site filter (XSS) and tell browser to block detected attacks
       X-XSS-Protection "1; mode=block"
       # Disallow the site to be rendered within a frame (clickjacking protection)
       X-Frame-Options "DENY"
       # Prevent search engines from indexing (optional)
       X-Robots-Tag "none"
       # Server name removing
       -Server
  }

  # Notifications redirected to the WebSocket server
  reverse_proxy /notifications/hub vaultwarden:3012

  # Proxy everything else to Rocket
  reverse_proxy vaultwarden:80 {
       # Send the true remote IP to Rocket, so that vaultwarden can put this in the
       # log, so that fail2ban can ban the correct IP.
       header_up X-Real-IP {remote_host}
  }
}

Эта конфигурация заслуживает разбора по частям. Конструкция {$DOMAIN}:443 означает, что Caddy будет слушать 443-й порт на домене из переменной окружения DOMAIN. Caddy автоматически подменяет такие плейсхолдеры значениями из окружения - удобно для шаблонизации.

Блок log настраивает запись логов в файл с автоматической ротацией. Параметр roll_size 10MB означает, что когда файл доходит до 10 мегабайт, Caddy создаёт новый, а старый отправляет в архив. Параметр roll_keep 10 ограничивает количество хранимых архивных файлов десятью - старые автоматически удаляются.

Строка tls {$EMAIL} включает автоматическое получение TLS-сертификата от Let's Encrypt через ACME HTTP-01 challenge. Caddy сам обращается к Let's Encrypt при первом запуске, проходит проверку, получает сертификат, устанавливает его и настраивает автоматическое обновление. Никаких дополнительных команд - вся магия в одной строке.

Параметр encode gzip включает сжатие ответов, что заметно ускоряет загрузку страниц на медленных соединениях. Комментарий в файле предупреждает - на некоторых браузерах могут быть проблемы со скачиванием вложений, особенно на Firefox. Если столкнулся - попробуй отключить эту строку.

Блок request_body с max_size 500MB разрешает огромные тела запросов. Это нужно для работы с возможностью Bitwarden Send, через которую можно безопасно делиться файлами. Без подходящего лимита крупные файлы не пройдут.

Блок header добавляет защитные HTTP-заголовки к каждому ответу. X-XSS-Protection включает встроенную защиту браузера от межсайтового скриптинга. X-Frame-Options "DENY" запрещает встраивание сайта в iframe, что защищает от атаки clickjacking. X-Robots-Tag "none" просит поисковики не индексировать страницы (для приватного сервиса это разумно). Конструкция -Server убирает заголовок Server из ответов - злоумышленнику сложнее определить, какой именно веб-сервер отвечает на запросы.

Две директивы reverse_proxy реализуют само проксирование. Первая ловит запросы по пути /notifications/hub и направляет их на порт 3012 контейнера vaultwarden - это WebSocket-канал для уведомлений в реальном времени. Вторая обрабатывает весь остальной трафик и направляет его на порт 80 того же контейнера - там работает Rocket (веб-фреймворк, на котором написан Vaultwarden). Заголовок X-Real-IP передаёт настоящий IP клиента в приложение, что нужно для корректной работы fail2ban при блокировке атакующих по адресу.

Стоит обратить внимание - адреса бэкенда указаны как vaultwarden, без localhost или IP. Это работает благодаря встроенному DNS Docker - внутри сети контейнеры могут обращаться друг к другу по именам сервисов из docker-compose.yml.

Сохраняем файл.

Запуск контейнеров и проверка работоспособности через docker ps

Все файлы готовы, можно запускать:

$ docker compose up -d

Флаг -d (detach) означает запуск в фоновом режиме без привязки к текущему терминалу. Без него контейнеры работали бы в активной сессии, и при выходе из SSH остановились бы. Docker Compose прочитает файл, скачает образы (если их ещё нет), создаст сеть, запустит контейнеры в нужном порядке.

При первом запуске процесс может занять минуту-две - нужно скачать образы и Caddy должен пройти ACME-проверку для получения сертификата. Если в логах Caddy появляются сообщения об ошибках получения сертификата, проверяй несколько вещей: открыт ли 80-й порт снаружи, корректно ли направлен домен на сервер, не блокирует ли провайдер исходящие соединения к Let's Encrypt.

Проверяем статус контейнеров:

$ docker ps

Ожидаемый вывод:

CONTAINER ID   IMAGE                       COMMAND                  CREATED              STATUS                        PORTS                                                                                NAMES
4ad23954f1d5   caddy:2                     "caddy run --config …"   About a minute ago   Up About a minute             0.0.0.0:80->80/tcp, :::80->80/tcp, 0.0.0.0:443->443/tcp, :::443->443/tcp, 2019/tcp   caddy
d55a037850bc   vaultwarden/server:latest   "/usr/bin/dumb-init …"   About a minute ago   Up About a minute (healthy)   80/tcp, 3012/tcp                                                                     vaultwarden

Видно два запущенных контейнера. Caddy слушает порты 80 и 443 наружу. Vaultwarden работает только внутри Docker-сети на портах 80 (HTTP) и 3012 (WebSocket). Метка (healthy) у Vaultwarden означает, что встроенная проверка здоровья проходит успешно - сервис отвечает на внутренние запросы.

Регистрация первой учётной записи и подключение клиента Bitwarden к собственному серверу

Открываем браузер и переходим по адресу https://vaultwarden.example.com (с подменой на свой домен). Появится приветственная страница Bitwarden. Если используется свежая Caddy-инсталляция, сертификат от Let's Encrypt должен быть получен и установлен автоматически.

Кликаем Create Account для создания первой учётной записи. Заполняем форму с email, мастер-паролем (это главный пароль, который защищает все остальные), подсказкой к нему. Нажимаем Submit для отправки.

После регистрации система перенаправляет на форму входа. Вводим только что созданные учётные данные. После успешного входа предлагается верифицировать email - это нужно для разблокировки полного функционала. Нажимаем Send Email, открываем входящие, кликаем по ссылке в полученном письме. Email подтверждён, аккаунт полностью активен.

Теперь самое интересное - подключение клиентов Bitwarden к собственному серверу. Поскольку Vaultwarden совместим с официальными клиентами на 99%, ставится любой клиент с сайта Bitwarden - расширение для браузера, мобильное приложение, десктопная программа. В этом руководстве рассмотрено браузерное расширение для Chrome.

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

Кликаем по иконке шестерёнки (Settings) в верхнем левом углу формы. Открываются продвинутые настройки. В поле Server URL вводим https://vaultwarden.example.com (полный адрес собственного Vaultwarden). Жмём Save для сохранения настроек.

Возвращаемся к форме входа, вводим email и мастер-пароль, заходим. Клиент подключается к собственному серверу, синхронизирует пароли (пока пустые), после чего готов к работе. Можно создавать новые записи, импортировать пароли из других менеджеров, генерировать сложные комбинации - всё это работает через знакомый интерфейс Bitwarden, но данные хранятся под собственным контролем.

Регулярный бэкап SQLite-базы через systemd-таймер для надёжного сохранения данных

Vaultwarden хранит все записи в SQLite-базе внутри каталога ./vw-data. Простой способ резервного копирования - копировать файл .sqlite3. Но это плохая идея, поскольку во время копирования база может находиться в промежуточном состоянии, и копия окажется повреждённой. Правильный способ - использовать встроенную команду SQLite для создания консистентного снимка.

Ставим SQLite:

$ sudo apt install sqlite3

Создаём каталог для бэкапов:

$ mkdir ~/vw-backups

Снимаем права чтения и выполнения для группы и других пользователей:

$ chmod go-rwx ~/vw-backups

Это критичный шаг безопасности. Бэкап содержит зашифрованные пароли, и доступ к нему должен иметь только владелец. Без правильных прав другие пользователи системы могли бы скопировать бэкап и пытаться расшифровать его офлайн.

Создаём systemd-юнит для бэкапа:

$ sudo nano /etc/systemd/system/vaultwarden-backup.service

Содержимое:

[Unit]
Description=backup the vaultwarden sqlite database

[Service]
Type=oneshot
WorkingDirectory=/home/<username>/vw-backups
ExecStart=/usr/bin/env bash -c 'sqlite3 /home/<username>/vaultwarden/vw-data/db.sqlite3 ".backup backup-$(date -Is | tr : _).sqlite3"'
ExecStart=/usr/bin/find . -type f -mtime +30 -name 'backup*' -delete

Заменяем <username> на имя своего системного пользователя в обоих местах. Разберём содержимое.

Параметр Type=oneshot означает разовое выполнение - юнит запустился, отработал, завершился. Это правильный тип для скриптов резервного копирования.

Первая строка ExecStart запускает sqlite3 с командой .backup, которая создаёт консистентный снимок базы (внутренне это происходит через WAL-механизм SQLite). Имя файла генерируется через date -Is с заменой двоеточий подчёркиваниями (двоеточия в именах файлов на разных системах ведут себя по-разному).

Вторая строка ExecStart удаляет старые бэкапы старше 30 дней. Без такого механизма каталог быстро заполнился бы тысячами файлов. Параметр -mtime +30 находит файлы, изменённые более 30 дней назад.

Сохраняем файл и тестируем:

$  sudo systemctl start vaultwarden-backup.service

Проверяем результат:

$ ls -l ~/vw-backups

Ожидаемый вывод:

total 192
-rw-r--r-- 1 root root 196608 Jul 31 17:25 backup-2022-07-31T17_25_04+00_00.sqlite3

Появился файл бэкапа с временной меткой в имени.

Теперь настраиваем расписание через systemd-таймер. Создаём юнит таймера с тем же именем, что и сервис:

$ sudo nano /etc/systemd/system/vaultwarden-backup.timer

Содержимое:

[Unit]
Description=schedule vaultwarden backups

[Timer]
OnCalendar=04:00
Persistent=true

[Install]
WantedBy=multi-user.target

Параметр OnCalendar=04:00 запускает бэкап каждый день в 4 утра. Это типичное время для подобных задач - нагрузка минимальна, рабочий день не начался. Параметр Persistent=true означает, что если сервер был выключен в момент запланированного запуска, после включения systemd запустит пропущенную задачу. Без этого параметра пропуски просто терялись бы.

Активируем таймер:

$ sudo systemctl enable vaultwarden-backup.timer
$ sudo systemctl start vaultwarden-backup.timer

Проверяем статус:

$  systemctl status vaultwarden-backup.timer

Ожидаемый вывод:

? vaultwarden-backup.timer - schedule vaultwarden backups
     Loaded: loaded (/etc/systemd/system/vaultwarden-backup.timer; enabled; vendor preset: enabled)
     Active: active (waiting) since Sun 2022-07-31 17:27:03 UTC; 7s ago
    Trigger: Mon 2022-08-01 04:00:00 UTC; 10h left
   Triggers: ? vaultwarden-backup.service

Jul 31 17:27:03 vaultwarden systemd[1]: Started schedule vaultwarden backups.

Строка active (waiting) означает, что таймер запущен и ждёт следующего срабатывания. Параметр Trigger показывает точное время следующего запуска.

Стоит понимать - это бэкап только базы данных. Помимо неё в каталоге vw-data лежат другие важные файлы. Каталог attachments хранит прикреплённые к записям файлы. Файл config.json содержит дополнительные настройки. Файлы rsa_key* нужны для криптографических операций - без них восстановленная база не сможет работать. Эти артефакты нужно бэкапить отдельно, регулярно копируя содержимое каталога. Каталоги sends (для функции Bitwarden Send) и icon_cache (кеш иконок сайтов) опциональны - можно бэкапить, можно не бэкапить, на критичные данные это не повлияет.

Не менее важно отправлять бэкапы куда-то за пределы того же сервера. Если сервер сгорит или диск выйдет из строя, локальные бэкапы погибнут вместе с боевой базой. Копировать нужно на отдельный сервер, в облачное хранилище, на домашний NAS или хотя бы на собственный компьютер - куда угодно, лишь бы это был физически другой носитель в другом месте.

Восстановление данных из бэкапа после сбоя или миграции на новый сервер

Восстановление - процедура, к которой надо быть готовым ещё до того, как она потребуется. Хороший бэкап без проверенного восстановления стоит мало - в момент катастрофы выясняется, что бэкапы битые, неполные или несовместимы с текущей версией.

Перед восстановлением обязательно останавливаем контейнер:

$ docker compose stop vaultwarden

Заменяем содержимое каталога vw-data файлами из бэкапа. Сначала восстанавливаем SQLite-базу из бэкапного файла, потом возвращаем на место attachments, config.json, rsa_key*. При восстановлении SQLite-базы критично удалить существующий файл db.sqlite3-wal перед запуском - он содержит незавершённые транзакции, и его наличие после восстановления приведёт к повреждению базы.

После замены файлов запускаем контейнер обратно:

$ docker compose start vaultwarden

Проверяем работу через веб-интерфейс или клиент Bitwarden - все записи должны появиться на местах.

Обновление образа Vaultwarden до свежей версии без потери данных

Vaultwarden получает обновления регулярно - и с новыми возможностями, и с критичными патчами безопасности. Обновлять имеет смысл хотя бы раз в месяц-два, особенно если в новостях появляются упоминания уязвимостей в Bitwarden API.

Переходим в каталог с docker-compose.yml:

$ cd ~/vaultwarden

Останавливаем и удаляем существующие контейнеры (данные при этом сохраняются, поскольку лежат в смонтированных томах):

$ docker compose down --remove-orphans

Параметр --remove-orphans удаляет контейнеры, которые остались от предыдущих конфигураций, но больше не описаны в файле. Это поддерживает чистоту в системе.

Скачиваем свежие образы:

$ docker compose pull

Команда обращается к Docker Hub, проверяет наличие новых версий для всех образов из конфигурации и скачивает их. Если новых версий нет, ничего не происходит.

Запускаем контейнеры с новыми образами:

$ docker compose up -d

Vaultwarden обновится, но все данные останутся на местах. Внутренняя структура базы между минорными версиями совместима - проект серьёзно подходит к миграциям.

Но перед каждым обновлением имеет смысл сделать ручной бэкап через запуск vaultwarden-backup.service. На случай, если с обновлением что-то пойдёт не так и потребуется откат.

Распространённые подводные камни и практические советы для эксплуатации Vaultwarden в боевых условиях

В копилку наблюдений из практики стоит добавить несколько моментов, на которых обжигаются те, кто впервые поднимает Vaultwarden в реальной работе.

Первая частая проблема - проблемы с email. Без рабочей SMTP-настройки не работает подтверждение почты, не отправляются приглашения, не приходят оповещения о подозрительной активности. SMTP-сервер можно поднять свой (postfix с обвязкой), но проще использовать сторонний сервис для транзакционной почты - Mailgun, SendGrid, Amazon SES. У каждого есть бесплатные лимиты, которых хватает для домашнего использования с большим запасом.

Вторая популярная грабля - забытый бэкап rsa-ключей. Многие настраивают только бэкап SQLite-базы и считают задачу решённой. Но без файлов rsa_key* восстановленная база окажется бесполезна - токены сессий не пройдут проверку, шифрованные части данных не расшифруются. Бэкапить нужно весь каталог vw-data целиком, а не только sqlite-файл.

Третий момент касается двухфакторной аутентификации. Вbitwarden есть встроенный TOTP-генератор для записей, но саму учётку Vaultwarden тоже нужно защищать вторым фактором. Настраивается через веб-интерфейс - можно подключить TOTP-приложение (Aegis, Google Authenticator, Authy), email-коды или WebAuthn-ключи (YubiKey, SoloKey). Без 2FA одного скомпрометированного мастер-пароля достаточно, чтобы потерять доступ ко всем записям.

Четвёртая тонкость - fail2ban. Vaultwarden пишет неудачные попытки входа в логи, но автоматической блокировки нет. Для защиты от перебора мастер-паролей имеет смысл настроить fail2ban с правилом, которое блокирует IP после нескольких неудачных попыток. Хорошие готовые шаблоны лежат в официальной wiki проекта на GitHub.

Пятый момент - админ-панель. У Vaultwarden есть скрытая админ-страница по адресу /admin, через которую можно управлять пользователями и просматривать настройки. По умолчанию она выключена - для активации нужно установить переменную ADMIN_TOKEN с длинным случайным значением. Без этого токена доступ к панели невозможен. Но даже с токеном открывать /admin в публичный интернет - плохая идея. Разумнее закрыть доступ к этому пути по IP в Caddyfile или просто оставлять админ-токен пустым, заходя в админку только по необходимости.

Где такая инсталляция пригодится в реальной жизни? Сценариев масса. Семейное хранилище паролей с возможностью делиться записями между членами семьи через организации. Корпоративный менеджер паролей для маленькой команды без бюджета на коммерческие решения. Личное хранилище для разработчика или системного администратора, у которого десятки серверных учёток, ключей API и сертификатов. Полная замена облачным менеджерам паролей при переходе на самохостинг по принципиальным соображениям приватности. Резервный менеджер на случай недоступности основного.

Освоение Vaultwarden даёт инженеру не просто навык установки одного приложения, а целый набор практик современной эксплуатации - работа с Docker Compose, настройка автоматических TLS-сертификатов через Caddy, организация бэкапов через systemd-таймеры, защитные HTTP-заголовки. Эти знания переносимы практически на любой современный самохостимый проект. Тот, кто разобрался с Vaultwarden, без труда поднимет Nextcloud, Gitea, FreshRSS, Plausible и десятки других подобных решений по тому же шаблону. И именно такие проекты помогают вернуть данные под собственный контроль - это не просто технический навык, а маленький шаг к большей цифровой самостоятельности в эпоху, когда всё больше повседневных активностей уезжает в чужие облака.