↓ До тексту
  1. Статті/

Vaultwarden - менеджер паролів для офісу без адміна

cat /etc/passwd
Автор
cat /etc/passwd
Технічний блог про Linux, DevOps та Open Source технології.
Зміст
Офіс без адміна - частина серії статей.
Частина 1: ця стаття (ти тут)

У невеликому офісі паролі живуть усюди, крім правильного місця: у приватних повідомленнях, у спільній табличці, на стікері під клавіатурою, у голові в одного співробітника, який сьогодні на лікарняному. А коли хтось звільняється, ніхто навіть не пам’ятає, до чого в нього був доступ. Знайомо.

Рішення давно є - командний менеджер паролів. Але хмарні тарифи рахують гроші за кожне місце, а віддавати всі паролі компанії стороннього сервісу хочеться не завжди. Vaultwarden - це легкий сервер, повністю сумісний із застосунками Bitwarden (ті самі розширення для браузера, десктоп, мобільний), який ти ставиш на власну машину. Написаний на Rust, у спокої з’їдає якісь 10-30 МБ пам’яті, а місць не рахує взагалі - хоч 20 людей, хоч 200.

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

Що потрібно зробити?
#

Підняти Vaultwarden на одній маленькій віртуалці для офісу приблизно на 20 людей:

  • винести дані сейфа на окремий диск - щоб бекапити й переносити їх окремо від системи;
  • поставити Docker і запустити зв’язку Vaultwarden + Caddy одним docker compose;
  • зробити HTTPS без публічного домену - клієнти Bitwarden інакше просто не працюють;
  • створити організацію (спільний сейф), колекції по відділах і запросити команду;
  • підключити клієнти на пристроях і насамкінець замкнути реєстрацію, щоб нові акаунти не заводив хто завгодно.

З чим працюю?
#

Одна скромна віртуалка - рівно стільки, скільки треба, і ні гігабайтом більше:

  • 1 vCPU, 1 ГБ RAM;
  • 10 ГБ системного диска + окремий 5 ГБ диск під дані;
  • ОС Ubuntu 26.04 LTS.

Vaultwarden настільки економний, що 1 ГБ пам’яті на 20 користувачів - із великим запасом. Для певності додамо ще 1 ГБ swap, щоб система мала куди відступити під час пікового навантаження чи оновлень.

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

Окремий диск під дані
#

Перше правило домашнього (та й не лише домашнього) сервера: дані не повинні лежати там само, де система. Коли сейф із паролями живе на окремому диску, його легко забекапити, збільшити чи перенести на іншу машину - не чіпаючи саму ОС. Тому під Vaultwarden ми й підкинули віртуалці другий диск на 5 ГБ.

У системі він з’являється порожнім пристроєм /dev/sdb. Форматуємо його в ext4 з міткою vwdata, створюємо точку монтування й монтуємо:

1
2
sudo mkfs.ext4 -L vwdata /dev/sdb
sudo mkdir -p /mnt/data

Щоб диск підмонтувався сам після перезавантаження, додаємо його в /etc/fstab - і обов’язково по UUID, а не по /dev/sdb (імена пристроїв можуть помінятися місцями). UUID підкаже blkid:

1
sudo blkid /dev/sdb

Рядок у /etc/fstab (підстав свій UUID):

1
UUID=<...> /mnt/data ext4 defaults,noatime 0 2

Перевіряємо, що запис коректний, одразу монтуючи все з fstab:

1
sudo mount -a

Якщо команда відпрацювала без помилок і /mnt/data змонтувався - диск готовий і переживе перезавантаження.

Поки ми тут, додамо обіцяний 1 ГБ swap. Це дешева страховка для машини з гігабайтом пам’яті:

1
2
3
4
sudo fallocate -l 1G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

І теж закріплюємо в /etc/fstab, щоб swap вмикався сам:

1
/swapfile none swap sw 0 0

Docker з репозиторіїв Ubuntu
#

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

1
sudo apt install -y docker.io docker-compose-v2

Це дає Docker 29.x і сучасний docker compose версії 2.40.x (саме docker compose, двома словами, а не старий docker-compose). Більше нічого підключати не треба.

Vaultwarden і Caddy одним compose-файлом
#

Складемо весь сервіс в одній папці /opt/vaultwarden. Там буде три файли: .env із секретами, docker-compose.yml зі самими контейнерами і Caddyfile для HTTPS.

1
2
sudo mkdir -p /opt/vaultwarden
cd /opt/vaultwarden

Токен адмінки: argon2, а не текст
#

У Vaultwarden є сторінка /admin - звідти керують сервером. Доступ до неї закриває ADMIN_TOKEN. Раніше туди писали пароль відкритим текстом, але так робити вже не можна: сучасний Vaultwarden на це сварливо попереджає й хоче argon2-хеш. Згенерувати його допомагає сам образ Vaultwarden:

1
docker run --rm -it vaultwarden/server /vaultwarden hash --preset owasp

Команда двічі спитає пароль адмінки й надрукує довгий рядок виду $argon2id$v=19$m=.... Це і є токен. Скопіюй його повністю.

Файл .env
#

Хеш argon2 рясніє знаками $, а compose трактує $ як підстановку змінної. Тому токен кладемо не в docker-compose.yml, а в окремий .env - і беремо значення в одинарні лапки, щоб жоден $ не з’їло:

1
2
3
4
# /opt/vaultwarden/.env
DOMAIN=https://192.168.1.50
ADMIN_TOKEN='$argon2id$v=19$m=...'
SIGNUPS_ALLOWED=true

Кілька слів про кожен рядок:

  • DOMAIN - точна адреса, за якою клієнти доходять до сервера, разом зі https://. Вона має збігатися з тим, що ти введеш у застосунках, інакше частина функцій зламається.
  • ADMIN_TOKEN - той самий argon2-рядок зі згенерованого хеша.
  • SIGNUPS_ALLOWED=true - дозвіл на самостійну реєстрацію. Вмикаємо його тільки на час, поки заводимо перші акаунти, а потім вимкнемо.

Файл із секретами не повинні читати всі підряд:

1
sudo chmod 600 /opt/vaultwarden/.env

docker-compose.yml
#

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# /opt/vaultwarden/docker-compose.yml
services:
  vaultwarden:
    image: vaultwarden/server:1.37.4
    container_name: vaultwarden
    restart: unless-stopped
    env_file: .env
    environment:
      SIGNUPS_VERIFY: "false"
      INVITATIONS_ALLOWED: "true"
      LOG_FILE: "/data/vaultwarden.log"
      LOG_LEVEL: "warn"
    volumes:
      - /mnt/data/vaultwarden:/data

  caddy:
    image: caddy:2
    container_name: caddy
    restart: unless-stopped
    depends_on:
      - vaultwarden
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - /mnt/data/caddy:/data
      - /mnt/data/caddy-config:/config

Головне тут: версія Vaultwarden зафіксована на 1.37.4 (щоб оновлення не прилетіло несподівано), а дані сейфа лежать на нашому окремому диску - /mnt/data/vaultwarden усередині контейнера видно як /data. Поруч стоїть Caddy - він прийматиме HTTPS ззовні й передаватиме запити Vaultwarden. LOG_FILE ми виставили свідомо: пізніше по цьому логу можна буде нацькувати fail2ban.

Caddyfile: HTTPS без домену
#

А тепер найцікавіша частина, на якій багато хто спотикається. Клієнти Bitwarden і Vaultwarden шифрують сейф у браузері через Web Crypto API, а він працює тільки в захищеному (https) контексті. Обхідного шляху через звичайний http немає - без HTTPS застосунки просто відмовляться працювати.

Зазвичай сертифікат видає Let’s Encrypt, але для цього потрібен публічний домен. У нас його немає - сервер живе в локальній мережі за IP. Рятує Caddy: він уміє бути власним центром сертифікації й сам видати собі сертифікат. Ось весь Caddyfile:

1
2
3
4
5
6
7
8
9
{
    default_sni 192.168.1.50
}

192.168.1.50 {
    tls internal
    encode zstd gzip
    reverse_proxy vaultwarden:80
}

tls internal і каже Caddy стати власним CA. А default_sni тут не прикраса, а необхідність: коли клієнт стукає по IP-адресі, він не надсилає ім’я сервера (SNI), і Caddy не знає, який сертифікат показувати. Без цього рядка TLS-рукостискання просто обривається - класична заковика. Ми явно кажемо: за замовчуванням показуй сертифікат для 192.168.1.50.

Запускаємо все:

1
docker compose up -d

Роздаємо довіру
#

Є один компроміс життя без домену: сертифікат видав наш власний CA, якого браузери поки не знають, тож спершу вони лаятимуться «не захищено». Щоб цього не було, кореневий сертифікат Caddy треба раз установити на офісні пристрої. Лежить він тут:

1
/mnt/data/caddy/caddy/pki/authorities/local/root.crt

Цей root.crt розносимо по пристроях команди один раз:

  • Linux - покласти в довірені й виконати update-ca-certificates;
  • Windows / macOS - додати сертифікат у системне сховище довіри;
  • мобільні - встановити як профіль сертифіката.

Перевірити, що сервер відповідає по HTTPS, можна й не чекаючи роздачі - просто вказавши curl наш кореневий сертифікат:

1
curl --cacert /mnt/data/caddy/caddy/pki/authorities/local/root.crt https://192.168.1.50/alive

У відповідь прилетить мітка часу й код 200 - сервер живий. У самому сертифікаті побачиш, що він виданий на IP сервера, а видавець - Caddy Local Authority.

Коли згодом у офісу з’явиться справжній домен, уся ця морока зникає: у Caddyfile замінюєш IP на доменне ім’я й прибираєш рядок tls internal - Caddy сам візьме безкоштовний сертифікат у Let’s Encrypt, і розносити нічого не доведеться.

«Офіс»: організація та колекції
#

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

Заходимо у веб-сейф за адресою сервера, реєструємо собі акаунт власника (поки SIGNUPS_ALLOWED=true це дозволяє) і далі: Password Manager -> New organization. Назвемо її, скажімо, «Наш офіс».

Усередині організації паролі розкладають по колекціях - це наче теки з доступом по відділах. Логічно завести щось на кшталт:

  • IT - сервери, панелі, домени;
  • Бухгалтерія - банк-клієнти, податкові кабінети;
  • Спільне - те, чим користуються всі.

Тепер запрошуємо команду: Members -> Invite user. На кожного вказуємо роль і доступ до потрібних колекцій. Ролей чотири, від найбільших прав до найменших: Owner, Admin, Manager, User. Більшості співробітників вистачить User із доступом до своїх колекцій.

Важливий нюанс саме для самостійного сервера. Коли людина прийме запрошення, власник або адмін мусить її ще й підтвердити (Confirm) - це звірка відбитка ключа, без неї учасник не зможе розшифрувати спільні записи. Ми не налаштовували SMTP, тому запрошення підтверджуються руками в інтерфейсі. Для 20 людей це робити вручну разів двадцять трохи втомливо, тож налаштування SMTP - перше, що варто додати згодом: тоді запрошення приходитимуть листом, а підтвердження спрощується.

Групи (Groups) у Vaultwarden теж є, але це поки beta-можливість - для старту надійніше роздавати доступ по колекціях напряму.

Організація «Наш офіс» у Vaultwarden: колекції та учасники

Підключаємо клієнти
#

Застосунки ті самі, що й у Bitwarden - розширення для браузера, десктоп, мобільний. Єдине, що треба зробити інакше: до входу вказати застосунку, що сервер у нас власний. У налаштуваннях входу відкриваємо Settings -> Self-hosted (або поле Server URL) і вписуємо https://192.168.1.50. Аж тоді входимо - поштою й майстер-паролем.

Далі все як у звичайному Bitwarden: зберігаєш запис на одному пристрої - і за кілька секунд він з’являється на решті. Ось це «воно справді працює» найкраще один раз побачити:

Одне чесне застереження. Вхід без пароля через passkey (WebAuthn) хоче справжнє доменне ім’я й по голому IP не заведеться. Але це не біда: при вході по IP ми покладаємося на майстер-пароль плюс двофакторку через TOTP - і те, і те працює бездоганно.

Замикаємо двері
#

Поки ми заводили акаунти, реєстрація була відкрита - і це дірка: будь-хто, хто дотягнеться до сервера, міг би собі завести акаунт. Коли команда на місці, реєстрацію треба закрити. У .env міняємо:

1
SIGNUPS_ALLOWED=false

і перезастосовуємо:

1
docker compose up -d

Тепер нові акаунти заводяться тільки через запрошення адміном. Двері замкнені.

Бекап
#

Усе цінне лежить у /mnt/data/vaultwarden - і найперше це база db.sqlite3 та ключ rsa_key.pem. Втратиш rsa_key.pem - і всіх розлогінить, доведеться входити наново. Жива база SQLite не любить, щоб її копіювали звичайним cp під час роботи, тому Vaultwarden має вбудований бекап, який знімає узгоджену копію:

1
docker exec vaultwarden /vaultwarden backup

Цю папку варто щоночі відправляти на NAS чи в інше місце поза сервером. Детальніше розкладати бекап і відновлення будемо окремо - поки що головне, щоб нічна копія просто була.

Безпека: сторінка /admin
#

Найчутливіше місце сервера - та сама сторінка /admin. Її прикриває argon2-токен, який ми згенерували, тож тримай ADMIN_TOKEN у надійному місці. Радикальніший варіант для параноїка: лишати ADMIN_TOKEN порожнім між сеансами адміністрування, а виставляти тільки тоді, коли реально заходиш в адмінку.

І згадаймо, навіщо ми виставляли LOG_FILE. По цьому логу (/data/vaultwarden.log) можна пустити fail2ban, щоб він банив тих, хто перебирає майстер-паролі - рівно так, як ми це робили для SSH у пості про fail2ban із серії «Захист сервера». Два сторожі на одному сервері чудово уживаються.

Куди далі
#

Маємо власний сейф для паролів команди: один раз розгорнули, роздали доступи - і офіс більше не ганяє паролі в чатах. Наступні очевидні кроки, коли буде настрій: SMTP для запрошень листом, справжній домен замість IP (і Let’s Encrypt замість роздачі сертифіката), повноцінна схема бекапу.

Якщо ж уся ця історія з «зроби сам» - не твоє, і хочеться, щоб хтось просто розгорнув і наглядав за цим замість тебе, напиши в teneta.tech - підкажуть і візьмуть на себе.

А серію «Офіс без адміна» продовжимо: цеглинка за цеглинкою зберемо офісну інфраструктуру, яку потягне звичайна людина без системного адміністратора за плечима.

Офіс без адміна - частина серії статей.
Частина 1: ця стаття (ти тут)

Related

CrowdSec проти fail2ban - коли сторожу треба спільнота

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

fail2ban - автоматичний вартовий проти SSH-брутфорсу

·5 хвилин
fail2ban - це сторож, що читає логи, рахує невдалі входи й автоматично банить IP, з якого ломляться. Піднімаємо його на Debian 13, брутфорсимо власний сервер із сусідньої машини й дивимось, як бан накладається в реальному часі. Плюс три заковики, специфічні саме для trixie.

Cowrie - honeypot, що ловить незваних гостей

·5 хвилин
Honeypot - це сервер-приманка: він навмисне виглядає вразливим, впускає зловмисника зі слабким паролем і тихо записує все, що той робить. Піднімаємо Cowrie в Docker, самі граємо «гостя» і дивимось, хто клюне - у реальному часі.