SSD Nodes Learn 🎉 VPS от $4.99/мес
Руководства Matt ConnorАвтор: Matt Connor · Обновлено 2026-08-08

Насколько безопасен Vaultwarden: руководство по защите

Vaultwarden шифрует данные на стороне клиента, поэтому сервер не видит пароли. Основные риски связаны с админ-токеном, открытыми портами и незащищенными резервными копиями.

Безопасен ли Vaultwarden? Краткий ответ

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

Этот ответ подразумевает выполнение ряда условий, и слабые места обычно возникают именно там, где вы настраиваете систему самостоятельно. Панель администратора, защищённая угадываемым токеном. Порт контейнера, открытый для всего Интернета. Файл config.json в открытом виде. Резервная копия в формате tarball, лежащая в домашней директории на том же сервере. Ни одна из этих проблем не относится к криптографии. Все они — причины, по которым взламывают self-hosted хранилища.

Всё, что описано ниже, предполагает, что у вас уже есть рабочая установка. Если её ещё нет, сначала выполните настройку согласно руководству по установке Vaultwarden на VPS, а затем вернитесь и последовательно проработайте этот список.

Что на самом деле хранит сервер

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

Некоторые данные не являются зашифрованными, и вы должны точно знать, какие именно:

  • Адрес электронной почты вашей учетной записи — в открытом виде.
  • Настройки KDF (функции формирования ключа) и соль, так как они нужны клиенту для восстановления ключа при следующем входе.
  • Серверный хеш хеша мастер-пароля, который отправляет клиент; он используется для аутентификации самого входа.
  • Метаданные: членство в организациях, имена устройств, время последнего входа.
  • Секрет для метода двухфакторной аутентификации, защищающего вход в Vaultwarden. Он хранится в таблице twofactor в незашифрованном виде, так как сервер должен вычислять ожидаемый код для сравнения с вашим. Это не то же самое, что секрет TOTP (одноразового пароля на основе времени), который вы храните внутри элемента хранилища; он зашифрован, как и любое другое поле.

Папка с данными занимает мало места. При установке через Docker это тот путь, который вы примонтировали к /data.

sudo ls -l /vw-data/

В db.sqlite3 хранится почти всё состояние системы. В attachments/ хранятся загруженные файлы (по одному на каждый UUID), и это единственный важный тип данных, который не находится в таблицах базы данных. В sends/ хранятся вложения сервиса Send, они считаются временными. icon_cache/ — это временные файлы. rsa_key.pem и сопутствующие файлы используются для подписи JWT (JSON web tokens) вошедших пользователей, поэтому копия этого закрытого ключа может быть использована для подделки сессии входа в хранилище. config.json появляется только после включения страницы администратора, и проект прямо предупреждает: там хранятся токен администратора и ваши учетные данные SMTP в открытом виде.

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

Сначала настройте токен администратора

/admin — это полноценная панель управления: список пользователей, приглашения, удаление, любые настройки среды выполнения. Она защищена только одним общим секретным ключом. Имени пользователя нет. Двухфакторной аутентификации для каждого пользователя тоже нет.

В старых руководствах предлагается генерировать ADMIN_TOKEN с помощью openssl rand -base64 48. Это работает, но записывает секрет в открытом виде в config.json и в ваш compose-файл. Vaultwarden также принимает строку Argon2 PHC (password hashing competition), поэтому сохраненное значение будет хешем. Сгенерируйте его с помощью работающего контейнера:

docker exec -it vaultwarden /vaultwarden hash

Или не затрагивая работающий контейнер:

docker run --rm -it vaultwarden/server /vaultwarden hash

Система дважды запросит пароль, а затем выведет строку, начинающуюся с $argon2id$. При установке на «голое железо» выполните ./vaultwarden hash. Если вы предпочитаете использовать CLI argon2 напрямую, проект документирует минимальные параметры OWASP:

echo -n 'MySecretPassword' | argon2 "$(openssl rand -base64 32)" -e -id -k 19456 -t 2 -p 1

Теперь ловушка, на которую люди тратят час. Строка PHC содержит множество символов $, а Docker Compose воспринимает $ как интерполяцию переменных. Если вставить её без экранирования в блок environment:, значение, которое попадет в контейнер, будет искажено, и /admin отклонит токен, который вы считаете верным. Есть две безопасные формы. В docker-compose.yml удвойте каждый $:

environment:
  ADMIN_TOKEN: $$argon2id$$v=19$$m=19456,t=2,p=1$$UUZxK1FZMkZoRHFQRlVrTXZvS0E3bHpNQW55c2dBN2NORzdsa0Nxd1JhND0$$cUoId+JBUsJutlG4rfDZayExfjq4TCt48aBc9qsc3UI

В файле .env экранирование не требуется, но используйте одинарные кавычки:

ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$MmeKRnGK5RW5mJS7h3TOL89GrpLPXJPAtTK8FTqj9HM$DqsstvoSAETl9YhnsXbf43WeaUwJC6JhViIvuPoig78'

Затем ограничьте частоту запросов к панели и сократите время сессии:

ADMIN_RATELIMIT_SECONDS=300
ADMIN_RATELIMIT_MAX_BURST=3
ADMIN_SESSION_LIFETIME=20

После трёх неудачных попыток в течение пяти минут панель перестанет отвечать этому клиенту. Сессия администратора истекает через 20 минут бездействия.

Лучший вариант: отключите эту страницу. Большинству инстансов она нужна один раз — для настройки SMTP и приглашения первых пользователей, а затем больше никогда. Чтобы отключить её, не задавайте ни ADMIN_TOKEN, ни DISABLE_ADMIN_TOKEN, удалите любой ключ "admin_token" из config.json, а затем пересоздайте контейнер. Удаление ключа из файла важно, так как страница администратора записывает настройки именно туда, а то, что находится в config.json, имеет приоритет над переменными окружения. Удаление только переменной оставит страницу открытой.

Закройте регистрацию до того, как кто-либо обнаружит домен

SIGNUPS_ALLOWED=false
SIGNUPS_VERIFY=true
SHOW_PASSWORD_HINT=false

SIGNUPS_ALLOWED по умолчанию имеет значение true. Оставьте его как есть, и любой, кто зайдет на ваш домен, получит учетную запись, а его данные будут храниться в той же db.sqlite3, что и ваши. Установите значение false и добавляйте пользователей через приглашения со страницы администратора, для чего потребуется настроенный SMTP. INVITATIONS_ALLOWED также по умолчанию имеет значение true, что позволяет владельцам организаций приглашать других пользователей. Это допустимо, если вы доверяете своим пользователям, но для инстанса с одним пользователем значение должно быть false. Если регистрация должна быть доступна только для определенных доменов, SIGNUPS_DOMAINS_WHITELIST=example.com является более строгим вариантом, чем открытая регистрация, но значительно менее безопасным, чем приглашения.

SHOW_PASSWORD_HINT по умолчанию имеет значение false, и его следует оставить таким. Если эта настройка включена, ввод корректного адреса электронной почты в форму входа возвращает подсказку к мастер-паролю этой учетной записи, что одновременно раскрывает подсказку и подтверждает существование адреса.

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

Порт, который вы не планировали публиковать

Docker-образ прослушивает порт 80 внутри контейнера. При установке на «голое железо» по умолчанию используется ROCKET_PORT=8000. В документации команда запуска публикует его так:

--publish 127.0.0.1:8000:80

Префикс 127.0.0.1: — это ключевой момент. Если написать -p 8000:80, Docker привяжется к 0.0.0.0, создавая правила DNAT (destination network address translation) в таблице nat. Эти правила обрабатываются раньше цепочек filter, которыми управляет ufw, поэтому ufw status сообщает, что порт закрыт, хотя он продолжает отвечать на запросы из Интернета. Полный механизм описан в руководстве по обходу ufw портами Docker.

Проверьте, что именно прослушивает порты:

sudo ss -tlnp | grep 8000

Корректный результат — одна строка, привязанная к 127.0.0.1:8000. Строка, привязанная к 0.0.0.0:8000, означает, что хранилище доступно напрямую. Исправьте маппинг и пересоздайте контейнер, так как привязка порта фиксируется при создании контейнера, и docker compose restart её не изменит:

docker compose up -d --force-recreate

В старых руководствах встречается еще один порт: 3012, отдельный порт для WebSocket. Поддержка этого порта была удалена в Vaultwarden 1.31.0, так как трафик уведомлений был перенесен на основной HTTP-порт. Параметры WEBSOCKET_ENABLED и WEBSOCKET_PORT игнорируются начиная с версии 1.29.0. Текущий переключатель — ENABLE_WEBSOCKET, значение по умолчанию — true. Если ваш межсетевой экран или файл compose всё ещё открывают порт 3012, закройте его.

Завершайте TLS на обратном прокси, а не в Rocket

Vaultwarden может самостоятельно обрабатывать TLS (transport layer security) через Rocket, свой веб-фреймворк, однако разработчики проекта не рекомендуют делать это в production. Встроенная поддержка TLS в Rocket не имеет строгой поддержки SNI (server name indication), поэтому рекомендации по безопасности гласят: обращайтесь к своему экземпляру только по имени хоста, а не по IP-адресу. Диапазоны публичных IP-адресов сканируются постоянно, и хранилище, отвечающее по IP-адресу, будет обнаружено.

Важные части блока сервера nginx:

client_max_body_size 525M;

location / {
  proxy_pass http://127.0.0.1:8000;
  proxy_set_header Host $host;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header X-Forwarded-Proto $scheme;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection $connection_upgrade;
}

По умолчанию nginx устанавливает client_max_body_size в 1 MB, поэтому без этой строки загрузка вложений будет завершаться ошибкой 413 Request Entity Too Large в логе ошибок nginx, в то время как Vaultwarden не запишет ничего. Заголовки Upgrade и Connection обеспечивают передачу рукопожатия WebSocket в /notifications/hub. Если их убрать, хранилище продолжит работать, но изменения перестанут отображаться на других устройствах до тех пор, пока вы не обновите страницу вручную.

Конфигурация Caddy короче, и он самостоятельно получает сертификат:

vw.example.com {
  reverse_proxy 127.0.0.1:8000 {
    header_up X-Real-IP {remote_host}
  }
}

Затем сообщите об этом Vaultwarden:

DOMAIN=https://vw.example.com
IP_HEADER=X-Real-IP

IP_HEADER по умолчанию имеет значение X-Real-IP, поэтому задача состоит в том, чтобы убедиться, что прокси действительно устанавливает этот заголовок. Если этого не сделать, каждая строка лога и каждый лимит попыток входа будут видеть 127.0.0.1 (сам прокси), что означает, что ошибки одного злоумышленника будут учитываться для всех пользователей экземпляра. Установите DOMAIN в значение реального https URL, так как Vaultwarden использует его для формирования ссылок на приглашения и сброс пароля, а ключи безопасности WebAuthn привязаны к этому источнику (origin).

Деталь, которую часто упускают: соединение WebSocket передает токен сессии в строке запроса как /notifications/hub?access_token=[JWT]. Это попадает в лог доступа вашего прокси в открытом виде. Исключите параметр access_token из формата логов или убедитесь, что эти логи не передаются туда, где вы не можете обеспечить их безопасность.

Ban brute force at the login endpoint

Rate limits are on by default (LOGIN_RATELIMIT_SECONDS=60, LOGIN_RATELIMIT_MAX_BURST=10). They slow an attacker down. They do not stop one. fail2ban does, but Vaultwarden has to write a log file first, and it does not do that out of the box:

LOG_FILE=/data/vaultwarden.log
LOG_LEVEL=info
EXTENDED_LOGGING=true

A failed login then produces exactly one line, and this is the string your filter has to match:

[2026-08-07 09:14:22][vaultwarden::api::identity][ERROR] Username or password is incorrect. Try again. IP: 203.0.113.10. Username: user@example.com.

Write the filter to /etc/fail2ban/filter.d/vaultwarden.local:

[INCLUDES]
before = common.conf

[Definition]
failregex = ^.*?Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$
ignoreregex =

And the jail to /etc/fail2ban/jail.d/vaultwarden.local:

[vaultwarden]
enabled = true
port = 80,443
filter = vaultwarden
banaction = %(banaction_allports)s
logpath = /vw-data/vaultwarden.log
maxretry = 3
bantime = 14400
findtime = 14400

If you kept the admin page, add a second jail whose failregex is ^.*Invalid admin token\. IP: <ADDR>.*$, because admin failures are logged with a different message and the login filter will never see them. Then check your work:

sudo systemctl restart fail2ban
sudo fail2ban-client status vaultwarden

A working jail lists your log file under File list and reports Currently failed: 0. Type a wrong password three times from a different network and that counter rises, then the address appears under Banned IP list. If the counter never moves, the usual cause is logpath: it must be the file's path on the host, not the /data/... path inside the container. The second usual cause is a missing X-Real-IP, which makes every ban target your own proxy. The rest of the setup, including the SSH jail you should already be running, is in the fail2ban guide for Ubuntu 24.04.

Мастер-пароль остается основой всей системы

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

PASSWORD_ITERATIONS=600000 — это количество итераций KDF, передаваемое клиентам при создании новой учетной записи. Существующие учетные записи сохраняют то значение, с которым они были созданы, поэтому его увеличение никак не повлияет на пользователей, зарегистрировавшихся в прошлом году. Им необходимо изменить его самостоятельно в настройках безопасности веб-хранилища, что приведет к повторному шифрованию их ключа. Сообщите им об этом, так как интерфейс не содержит никаких уведомлений.

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

Резервные копии — слабое место self-hosted хранилищ

tar czf папки с данными, оставленная в домашнем каталоге на том же VPS, обесценивает все предыдущие шаги. Этот архив содержит db.sqlite3 с зашифрованными данными всех пользователей, rsa_key.pem, позволяющий подделать сессии входа, и config.json с токеном администратора и паролем SMTP в открытом виде. Доступ на чтение к этому файлу означает полный доступ к хранилищу.

Существует два правила. Выносите архив за пределы сервера. Шифруйте его до отправки.

Также существует проблема целостности данных. Копирование db.sqlite3 с помощью cp во время работы сервиса может привести к созданию поврежденного файла, который невозможно будет открыть, и вы узнаете об этом только при попытке восстановления. Используйте встроенный механизм создания снимков SQLite:

sqlite3 /vw-data/db.sqlite3 ".backup '/tmp/vw-db-backup.sqlite3'"

Процесс восстановления, который почти никто не тестирует, описан в руководстве по резервному копированию и восстановлению Vaultwarden.

Чем вы жертвуете по сравнению с облачным Bitwarden

Честный подход. Облачный сервис Bitwarden обслуживают специалисты, для которых это основная работа. У них есть опубликованные сторонние аудиты и дежурные инженеры, готовые отреагировать в 3 часа ночи. При самостоятельном хостинге вы берете на себя ответственность за регулярную установку обновлений.

Vaultwarden выпускает исправления безопасности как обычные релизы. Версия 1.37.0, выпущенная 24 июля 2026 года, является актуальной по состоянию на август 2026 года, и в примечаниях к ней содержится призыв к пользователям обновиться как можно скорее. Экземпляр, который вы развернули год назад и забыли, работает на годовалом коде. Тег latest сам по себе не помогает: запущенный контейнер использует тот образ, с которого он был запущен, пока вы не выполните docker compose pull и не пересоздадите его. Настройте автоматические обновления в Ubuntu для пакетов хоста, а обновление контейнера добавьте в календарь, который вы действительно будете просматривать.

Вывод, который должен сделать честный читатель: криптография здесь разработана Bitwarden и она надежна, но операционные риски полностью переходят на вас. Если вы устанавливаете патчи и делаете резервные копии в стороннее хранилище, то экземпляр Vaultwarden на VPS под вашим контролем — разумное место для хранения паролей. Если эти две привычки вам не свойственны, оплатите облачный сервис и уделите внимание другим задачам. Сравнение функций представлено в сравнении Vaultwarden с self-hosted Bitwarden.

Укрепление безопасности хоста под контейнером

Vaultwarden — это один процесс на Linux-системе, и пользователь root на этой машине может прочитать /vw-data независимо от настроек приложения. Запускайте контейнер от имени непривилегированного пользователя с помощью user: "1000:1000" в вашем файле compose, установите соответствующие права владения на папку с данными и монтируйте всё, во что контейнер не должен записывать данные, в режиме только для чтения с помощью :ro. Затем закройте доступ извне: в укреплении безопасности SSH на VPS описаны вход только по ключам и отключение аутентификации по паролю — это именно то, что останавливает типовые атаки, которые обходят все вышеперечисленные меры.

FAQ

Может ли кто-то прочитать мои пароли, если украдет базу данных Vaultwarden?

Не напрямую. Каждый элемент хранилища шифруется на стороне клиента ключом, производным от мастер-пароля, поэтому db.sqlite3 содержит только зашифрованные данные. Злоумышленник сразу получит адреса электронной почты учетных записей, настройки KDF, метаданные входов и устройств, а также секреты двухфакторной аутентификации в таблице twofactor. Последние хранятся в открытом виде, так как сервер должен вычислять ожидаемый код. Злоумышленник может неограниченно долго атаковать зашифрованные данные хранилища в офлайн-режиме, поэтому длина мастер-пароля является решающим фактором безопасности.

Стоит ли использовать ADMIN_TOKEN или лучше полностью отключить страницу администратора?

Отключите её, если есть возможность, так как большинству инстансов она нужна лишь однажды для настройки SMTP и приглашения пользователей. Чтобы отключить её, не задавайте ни ADMIN_TOKEN, ни DISABLE_ADMIN_TOKEN, удалите ключ "admin_token" из config.json, а затем пересоздайте контейнер. Удаления только переменной окружения недостаточно, так как настройки, записанные через страницу администратора, сохраняются в config.json и имеют приоритет. Если вы решили оставить страницу, сохраняйте токен в виде Argon2-хеша, созданного с помощью vaultwarden hash, а не в виде случайной строки в открытом виде, и установите ADMIN_RATELIMIT_MAX_BURST=3.

Мой ADMIN_TOKEN верный, но /admin отклоняет его. В чем проблема?

Почти всегда дело в интерполяции $. Строка Argon2 PHC содержит несколько символов $, а Docker Compose интерпретирует их как переменные внутри блока docker-compose.yml environment:. В результате контейнер получает искаженное значение, хотя в файле оно выглядит корректно. Удвойте каждый символ $ до $$ в файле compose или перенесите значение в файл .env, заключив его в одинарные кавычки, где экранирование не требуется. После этого пересоздайте контейнер, так как изменения переменных окружения не применяются при обычном перезапуске.

Нужно ли мне по-прежнему открывать порт 3012 для уведомлений?

Нет. Поддержка WebSocket-трафика на порту 3012 была удалена в Vaultwarden 1.31.0, так как уведомления были перенесены на основной HTTP-порт, а параметры WEBSOCKET_ENABLED и WEBSOCKET_PORT игнорируются начиная с версии 1.29.0. Текущая настройка — ENABLE_WEBSOCKET, которая по умолчанию имеет значение true. Закройте порт 3012 в файрволе и удалите его из файла compose, затем убедитесь, что ваш reverse proxy передает заголовки Upgrade и Connection, так как именно от них теперь зависит работа синхронизации в реальном времени.