SSD Nodes Learn 🎉 VPS від $5.50/міс
Посібники Matt ConnorВід Matt Connor · Оновлено 2026-08-13

Чи безпечний Vaultwarden: як посилити захист

Vaultwarden шифрує кожен елемент сховища на клієнті, тому сервер не бачить відкритих даних. Основні ризики: admin token і файл резервної копії.

Чи безпечний Vaultwarden? Коротка відповідь

Vaultwarden безпечний у найважливішому аспекті: кожен елемент сховища шифрується на вашому пристрої, перш ніж потрапити на сервер. Сервер зберігає дані, які не може прочитати. Навіть якщо хтось скопіює всю базу даних, для отримання корисної інформації йому все одно потрібен майстер-пароль.

Ця відповідь охоплює багато аспектів, але проблеми виникають саме через те, що ви налаштовуєте. Панель адміністратора, захищена токеном, який легко вгадати. Порт контейнера, опублікований для всього Інтернету. Незашифрований config.json. Архів резервної копії у форматі tar, що зберігається в домашньому каталозі на тому самому сервері. Жодна з цих проблем не пов’язана з криптографією. Саме через них із self-hosted сховищ викрадають усі дані.

Усе нижче передбачає, що робоче встановлення вже є. Якщо ви ще не маєте його, спочатку налаштуйте Vaultwarden за інструкцією зі встановлення Vaultwarden на VPS, а потім послідовно виконайте наведені нижче кроки.

Що насправді зберігає сервер

Vaultwarden реалізує модель даних Bitwarden. Ім’я, ім’я користувача, пароль, нотатки та URI елемента сховища шифруються ключем, похідним від вашого master password, на клієнті ще до надсилання будь-якого запиту. Вміст файлів вкладень шифрується так само. Сервер отримує непрозорі дані з прив’язаним до них UUID (універсальним унікальним ідентифікатором).

Деякі дані не є шифротекстом. Потрібно точно знати, які саме:

  • Адреса електронної пошти вашого облікового запису у відкритому вигляді.
  • Налаштування KDF (функції виведення ключа) і сіль, оскільки клієнту вони потрібні для повторного побудування ключа під час наступного входу.
  • Хеш хешу master password на стороні сервера, який надсилає клієнт і який використовується для автентифікації самого входу.
  • Метадані: членство в організаціях, назви пристроїв, час останнього входу.
  • Секрет методу двофакторної автентифікації, який захищає вхід до Vaultwarden. Він зберігається в таблиці twofactor у незашифрованому вигляді, оскільки сервер має обчислити очікуваний код і порівняти його з вашим. Це не те саме, що секрет TOTP (одноразового пароля на основі часу), який ви зберігаєте всередині елемента сховища і який шифрується, як і будь-яке інше поле.

Папка даних невелика. У Docker-інсталяції це каталог, змонтований у /data.

sudo ls -l /vw-data/

db.sqlite3 містить майже весь стан. attachments/ містить завантажені файли, по одному на UUID. Це єдиний важливий клас даних, який не зберігається в таблицях бази даних. sends/ містить вкладення Send і призначений для тимчасових даних. icon_cache/ можна видалити без наслідків. rsa_key.pem та пов’язані з ним файли підписують JWT (вебтокени JSON) користувачів, які ввійшли в систему. Тому копію цього приватного ключа можна використати для підроблення сеансу входу до сховища. 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 image прослуховує порт 80 усередині контейнера. Bare-metal інсталяція типово використовує 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 означає, що vaultwarden безпосередньо доступний ззовні. Виправте зіставлення, а потім створіть контейнер заново. Прив’язка порту фіксується під час створення контейнера, і 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. Якщо у вашому firewall або compose-файлі досі відкрито 3012, закрийте цей порт.

Завершуйте TLS на reverse proxy, а не в Rocket

Vaultwarden може самостійно обслуговувати TLS (transport layer security) через Rocket — свій вебфреймворк, але проєкт не рекомендує так робити в production. Вбудована підтримка TLS у Rocket не має суворої підтримки SNI (server name indication). Саме тому в рекомендаціях із hardening радять підключатися до інстансу за hostname, а не за окремою IP-адресою. Публічні діапазони IP постійно сканують. Vault, який відповідає за IP-адресою, буде знайдено.

Важливі частини server block в 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 handshake до /notifications/hub. Якщо їх вилучити, vault і далі працюватиме, але зміни перестануть з’являтися на інших пристроях, доки ви вручну не перезавантажите сторінку.

У 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 за замовчуванням. Тому потрібно переконатися, що proxy справді встановлює цей заголовок. Якщо цього не зробити, кожен рядок журналу й кожне обмеження частоти входу бачитимуть 127.0.0.1 — сам proxy. У результаті невдалі спроби одного зловмисника зараховуватимуться всім користувачам інстансу. Також установіть DOMAIN у фактичну URL-адресу https, оскільки Vaultwarden формує з неї посилання для запрошень і скидання пароля, а ключі безпеки WebAuthn прив’язані до цього origin.

Є ще одна деталь, яку часто пропускають: WebSocket connection передає session token у query string як /notifications/hub?access_token=[JWT]. У такому вигляді він потрапляє до access log вашого proxy. Замаскуйте параметр access_token у форматі журналу або переконайтеся, що ці журнали не передаються до систем, якими ви не керуєте.

Заблокуйте brute-force-атаки на кінцевій точці входу

Обмеження частоти запитів увімкнено за замовчуванням (LOGIN_RATELIMIT_SECONDS=60, LOGIN_RATELIMIT_MAX_BURST=10). Вони уповільнюють зловмисника, але не зупиняють його. Це робить fail2ban, але спочатку Vaultwarden має записувати файл журналу. За замовчуванням він цього не робить:

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

Після невдалої спроби входу з’являється рівно один рядок. Саме цей рядок має знаходити ваш фільтр:

[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.

Запишіть фільтр у /etc/fail2ban/filter.d/vaultwarden.local:

[INCLUDES]
before = common.conf

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

А jail — у /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

Якщо ви залишили сторінку адміністратора, додайте другий jail, у якому failregex має дорівнювати ^.*Invalid admin token\. IP: <ADDR>.*$, оскільки невдалі спроби входу до адміністративної панелі записуються іншим повідомленням, і фільтр входу їх ніколи не побачить. Потім перевірте результат:

sudo systemctl restart fail2ban
sudo fail2ban-client status vaultwarden

У робочому jail ваш файл журналу буде вказано в File list, а лічильник Currently failed: 0 матиме значення. Тричі введіть неправильний пароль з іншої мережі. Лічильник має збільшитися, після чого адреса з’явиться в Banned IP list. Якщо лічильник не змінюється, найчастіша причина — неправильний logpath: це має бути шлях до файлу на хості, а не шлях /data/... усередині контейнера. Друга поширена причина — відсутній X-Real-IP, через що кожне блокування спрямовується на ваш власний проксі. Решту налаштування, зокрема SSH jail, який у вас уже має працювати, наведено в посібнику з fail2ban для Ubuntu 24.04.

Майстер-пароль усе ще захищає всю систему

Шифрування на стороні клієнта означає, що майстер-пароль є ключем. Короткий майстер-пароль у системі, базу даних якої зловмисник скопіював, нічим із описаного в цьому матеріалі не захищений, оскільки зловмисник може атакувати цю копію офлайн із максимальною швидкістю, яку забезпечує його обладнання. Жодне налаштування сервера не впливає на комп’ютер зловмисника.

PASSWORD_ITERATIONS=600000 — це кількість ітерацій KDF, яку передають клієнтам під час створення нового облікового запису. Наявні облікові записи зберігають значення, з яким їх було створено, тому його збільшення нічого не змінює для користувачів, які зареєструвалися минулого року. Вони мають змінити це значення самостійно в налаштуваннях безпеки вебсховища. Після цього їхній ключ буде зашифровано повторно. Повідомте їм про це, оскільки інтерфейс не показує жодного такого повідомлення.

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

Де ламаються резервні копії self-hosted сховищ

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

Достатньо дотримуватися двох правил. Виводьте архів за межі сервера. Шифруйте його до передавання.

Є також проблема коректності. Копіювання db.sqlite3 за допомогою cp під час роботи сервісу може створити файл, записування якого ще не завершено, і який не відкриється. Ви дізнаєтеся про це лише під час відновлення. Використовуйте власний snapshot SQLite:

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

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

Що ви втрачаєте порівняно з hosted Bitwarden

Чесна оцінка. Hosted service Bitwarden обслуговують фахівці, для яких це основна робота. Сервіс проходить опубліковані аудити третіх сторін, а черговий фахівець доступний навіть о 3-й ночі. Self-hosting означає, що тепер ви самі відповідаєте за графік встановлення оновлень.

Vaultwarden випускає виправлення безпеки у звичайних релізах. Версія 1.37.0, випущена 24 July 2026, є актуальною станом на August 2026, а в примітках рекомендується оновити її якнайшвидше. Інстанс, який ви налаштували рік тому, а потім забули про нього, працює на коді річної давності. Тег latest сам по собі не допомагає: запущений контейнер продовжує використовувати image, з якого його запустили, доки ви не виконаєте docker compose pull і не створите контейнер заново. Налаштуйте автоматичне встановлення оновлень в Ubuntu для пакетів хоста, а оновлення контейнера додайте до нагадування в календарі, яке ви справді прочитаєте.

Чесний висновок має бути таким: криптографія тут ґрунтується на дизайні Bitwarden і залишається надійною, тоді як операційний ризик повністю переходить до вас. Якщо ви встановлюєте оновлення та створюєте резервні копії в іншому місці, інстанс Vaultwarden на VPS під вашим контролем є прийнятним місцем для зберігання паролів. Якщо ці дві звички не будуть дотримуватися, платіть за hosted service і спрямуйте свою увагу на інші завдання. Порівняння функцій наведено в матеріалі 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 його відхиляє. У чому проблема?

Майже завжди проблема пов’язана з інтерполяцією $. Рядок PHC для Argon2 містить кілька символів $, а 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 у firewall і видаліть його з compose-файлу. Потім переконайтеся, що reverse proxy передає заголовки Upgrade і Connection, оскільки саме від них тепер залежить синхронізація в реальному часі.