Я убеждён, что пароль в закреплённом сообщении Telegram или в общем экселе рано или поздно утечёт — дело не в везении, а в том, сколько людей его уже увидели и скопировали себе «на всякий случай». Полгода назад у нас в компании было ровно так: доступы к Bitrix, SSH‑ключи, лицензии на модули — россыпью по личным чатам, гуглдокам и голове того, кто это заводил. Уходит сотрудник — доступы за ним никто не забирает, потому что толком не знает, какие у него вообще были. Разбираться с этим я сел после того, как в очередной раз не смог сходу сказать, какой из трёх паролей к хостингу актуальный — это и стало последней каплей.
Мы — digital‑агентство, ведём с десяток клиентских проектов на Bitrix параллельно, три условных направления (разработка, дизайн/маркетинг, инфраструктура/CRM) и общую инфраструктуру поверх. Ставил задачу так: один инструмент, куда переезжают все пароли, ключи и лицензии, доступ строго по ролям, и чтобы это реально прижилось в команде, а не полежало неделю и умерло, как предыдущая попытка завести таблицу в Notion.
Ниже — восемь выводов, к которым я пришёл по ходу. Шаги «как поставить Vaultwarden» я тут не расписываю — это есть в вики проекта. Расскажу про решения, компромиссы и конкретные места, где я ошибся и потом переделывал.
Свой сервис для секретов — я просто скопировал то, что давно делает бигтех.
Доступ только из VPN — я закладывал такую модель ещё на аутсорсе, под требования клиентов.
Автоматическая раздача сертификатов на устройства — роскошь бигтеха, у меня на неё нет ресурса.
Caddy и Vaultwarden в одном docker‑compose — вся боевая связка, остальное — обвязка вокруг неё.
DNS-01 через acme.sh — и порт 80 наружу открывать не пришлось.
Бэкап в одну точку — это не бэкап, это отложенная катастрофа.
Непротестированное восстановление — это не восстановление, а надежда.
Технология — это 20% внедрения. Остальные 80% — регламент, роли и обучение людей.

Тезис 1. Свой сервис для секретов — я просто скопировал то, что давно делает бигтех
TL;DR: Идею не изобретал — крупные компании давно не отдают свои секреты стороннему SaaS, а держат у себя. У меня к теме был личный интерес ещё со времён Passbolt, а до Vaultwarden дошёл через отдельное сравнение вариантов: Passbolt отпал сразу по деньгам — оплата только из‑за рубежа, а мы работаем в РФ.
К паролям и хранению секретов у меня был личный интерес и до этого проекта — в своё время руками разворачивал Passbolt, разбирался, как устроены self‑hosted менеджеры паролей изнутри, какие у них модели угроз и где они ломаются. Поэтому когда встал вопрос, куда девать доступы компании, вариант «поднять своё» не обсуждался как экзотика — примерно так и работает любая компания с серьёзным отношением к безопасности: секреты остаются на своей инфраструктуре, а не улетают к стороннему SaaS‑вендору.
Passbolt по старой памяти я даже не стал разворачивать — у него биллинг завязан на оплату из‑за рубежа, а мы работаем в РФ, и городить это ради менеджера паролей смысла не было. Вместо этого прогнал более предметное сравнение self‑hosted вариантов — вместе с ИИ разобрал несколько кандидатов по функциональности, активности разработки и совместимости с готовыми клиентами, отдельно опираясь на разбор на Хабре. Остановился на Vaultwarden: переписанный на Rust совместимый бэкенд, который говорит с официальными клиентами Bitwarden (расширение, десктоп, мобильные приложения) по тому же API. Пользователь открывает знакомое расширение Bitwarden и не видит разницы — обращается оно к моему серверу, а не к серверам Bitwarden.
Тезис 2. Доступ только из VPN — я закладывал такую модель ещё на аутсорсе, под требования клиентов
TL;DR: 443-й порт наружу не открываем вообще. Правило файрвола пускает HTTPS только из подсети VPN/офиса, всё остальное отваливается на уровне сети, а не на уровне логина. Модель не новая — похожую я уже настраивал раньше по требованию клиентов.
Спросите себя: зачем вообще выставлять в интернет сервис, где хранятся пароли от всей инфраструктуры компании, если весь потенциальный список пользователей — это сотрудники за VPN? Правильный ответ — незачем. 2FA и сложный мастер‑пароль — это вторая линия обороны. Первая — сервис просто не отвечает за пределами доверенной сети.
Такую модель я закладывал не первый раз в жизни. Раньше работал в компании на аутсорсе, где часть клиентов сама требовала пускать подрядчика к своим системам только с одного конкретного статического IP офиса — без этого условия контракт просто не подписывали. Так что когда встал вопрос доступа к волту, «открывать наружу или нет» я для себя даже не рассматривал как вопрос — его для меня уже решил чужой опыт.
Реализуется одной командой на хосте (у меня это Windows‑машина, поэтому PowerShell, но принцип тот же для любого файрвола):
# 443 открыт только для локальной подсети/VPN-диапазона - не для всего интернета New-NetFirewallRule -DisplayName "Vaultwarden HTTPS (LAN)" ` -Direction Inbound -Protocol TCP -LocalPort 443 -Action Allow ` -RemoteAddress 192.168.88.0/24 -Profile Any
Без -RemoteAddress это правило открывает 443 для 0.0.0.0/0 — то есть буквально для всего интернета, а не только для офиса и VPN.
Когда VPN стал выдавать адреса из второй подсети, правило просто обновил, а не переписал заново:
Set-NetFirewallRule -DisplayName "Vaultwarden HTTPS (LAN)" ` -RemoteAddress 192.168.88.0/24,192.168.89.0/24
Отдельно на диагностике я словил мелкую, но обидную вещь: Windows Firewall по умолчанию режет входящий ICMP, так что обычный ping до хоста не проходит, даже когда с сетью всё в порядке. Временное правило для диагностики — и сразу убрать, оно не боевое:
New-NetFirewallRule -DisplayName "Allow ICMP test" -Protocol ICMPv4 -IcmpType 8 -Direction Inbound -Action Allow # ... проверили, что пинг реально не про сеть, а про firewall ... Remove-NetFirewallRule -DisplayName "Allow ICMP test"
Тезис 3. Автоматическая раздача сертификатов на устройства — роскошь бигтеха, у меня на неё нет ресурса
TL;DR: Раздать корневой сертификат на все устройства сотрудников вручную — решение на один вечер. Автоматизировать это, как делает бигтех через MDM (mobile device management) и GPO (групповые политики), — отдельный проект, ресурса на который у меня нет. Выбрал ручной путь и сознательно принял его цену.
В бигтехе раздача внутренних сертификатов на устройства сотрудников автоматизирована — MDM, GPO и прочая инфраструктура сама раскатывает корневой CA (certificate authority, центр сертификации) на весь флот устройств при подключении. У меня такой инфраструктуры нет, и строить её ради одного сервиса — несоразмерные вложения: это не бигтех по масштабу, а команда без выделенного эникея под такие задачи.
Значит, вопрос стоял не «автоматизировать или нет», а «ручная раздача или отказ от self‑signed вообще». Выбрал первое — пятнадцать минут на человека, зато сертификат готов сразу. Корневой CA создаётся один раз и живёт годами:
mkdir -p ~/vault-ca && cd ~/vault-ca # ключ корневого CA - обязательно с паролем, это ваш корень доверия openssl genrsa -aes256 -out rootCA.key 4096 openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 3650 \ -out rootCA.crt \ -subj "/C=RU/O=Example Company/CN=Example Internal Root CA"
rootCA.crt раздаём на клиентские устройства, rootCA.key держим в бэкапе и никому не показываем — потеряете его, придётся перевыпускать и раздавать корень заново на каждое устройство.
Серверный сертификат подписываем этим CA, с SAN (Subject Alternative Name) — без него современные браузеры и клиенты Bitwarden сертификат просто не примут:
openssl genrsa -out vault.key 2048 openssl req -new -key vault.key -out vault.csr \ -subj "/C=RU/O=Example Company/CN=vault.example.com" cat > vault.ext <<'EOF' authorityKeyIdentifier=keyid,issuer basicConstraints=CA:FALSE keyUsage=digitalSignature,keyEncipherment extendedKeyUsage=serverAuth subjectAltName=@alt_names [alt_names] DNS.1 = vault.example.com EOF openssl x509 -req -in vault.csr -CA rootCA.crt -CAkey rootCA.key \ -CAcreateserial -out vault.crt -days 824 -sha256 -extfile vault.ext
Раздача rootCA.crt на устройство — отдельная процедура под каждую платформу: Windows, macOS, Firefox со своим хранилищем сертификатов отдельно от системного, iOS через установку профиля. Расписывать это по шагам здесь не буду — тема на отдельную статью, а не часть истории про сам сервис.
Важно другое: на Windows, macOS и iOS ручная раздача сработала без сюрпризов. А вот с Android вышел затык. Приложение Bitwarden под Android не доверяет пользовательским CA из системного хранилища по умолчанию — это осознанное решение разработчиков, а не баг. Корень, который прекрасно встал на десктопы и iPhone, Android просто отверг. На личный Android‑телефон сотрудника этот способ не распространяется — и ради этого пришлось отдельно переезжать на настоящий Let’s Encrypt (Тезис 5).
TLS у меня терминирует не встроенный в Vaultwarden Rocket, а реверс‑прокси Caddy — файлы сертификата просто монтируются в контейнер:
# Caddyfile vault.example.com { tls /certs/vault.crt /certs/vault.key reverse_proxy vaultwarden:80 }
Тезис 4. Caddy и Vaultwarden в одном docker‑compose — вся боевая связка, остальное — обвязка вокруг неё
TL;DR: Два сервиса, два volume с данными, один .env — это уже рабочий прод. Всё остальное (бэкапы, автопродление сертификата, hardening) добавляется поверх без изменения этого скелета.
ADMIN_TOKEN — это пароль в админ‑панель Vaultwarden, и класть его в .env открытым текстом не стоит: .env лежит рядом с бэкапами и попадает в них целиком. Vaultwarden умеет принимать Argon2-хеш вместо голого токена:
docker run --rm -it vaultwarden/server /vaultwarden hash
Результат — строка вида $argon2id$v=19$m=65540,t=3,p=4$... — вставляем в .env одной строкой, без переносов и без лишних кавычек внутри самого хеша:
# .env DOMAIN=https://vault.example.com SIGNUPS_ALLOWED=true INVITATIONS_ALLOWED=false SHOW_PASSWORD_HINT=false ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$c29tZXNhbHQ$...'
DOMAIN обязан посимвольно совпадать с адресом, по которому вы реально заходите. Разойдётся протокол или поддомен — не заработают WebAuthn (аппаратные ключи для 2FA) и вложения к записям. SIGNUPS_ALLOWED=true — временная дыра, нужна только чтобы создать первый аккаунт; как только он создан, значение переключаем на false, иначе регистрация открыта для кого угодно, кто узнает адрес волта.
Сам compose — минимальный, без лишней обвязки на старте:
# docker-compose.yml services: caddy: image: caddy:2 container_name: caddy restart: unless-stopped ports: - "443:443" volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - ./certs:/certs:ro - caddy_data:/data - caddy_config:/config depends_on: - vaultwarden vaultwarden: image: vaultwarden/server:latest container_name: vaultwarden restart: unless-stopped env_file: - ./.env volumes: - ./vw-data:/data volumes: caddy_data: caddy_config:
На первом запуске я на пару минут пробрасывал 127.0.0.1:8000:80 у vaultwarden, чтобы проверить контейнер локально ещё до того, как разобрался с сертификатом и DNS. Строку убрал сразу после проверки — постоянный проброс порта в обход Caddy обнуляет весь смысл TLS‑терминации на реверс‑прокси.
Запуск и первая проверка живости:
docker compose up -d docker compose logs -f vaultwarden
В логах ищите Rocket has launched from http://0.0.0.0:80 — это сигнал, что бэкенд поднялся и Caddy есть куда проксировать. Если этой строки нет, а контейнер в docker compose ps мигает Restarting — почти всегда проблема в .env: либо ADMIN_TOKEN с переносом строки, либо DOMAIN без схемы https://.
Тезис 5. DNS-01 через acme.sh — и порт 80 наружу открывать не пришлось
TL;DR: Let’s Encrypt через DNS-01: TXT‑запись в DNS вместо открытого 80-го порта. Нужного провайдера в списке acme.sh не было — хук на его REST API написал сам.
После Android‑затыка из Тезиса 3 вопрос встал ребром: либо разрешать доступ с телефонов и держать 80/443 открытыми для HTTP-01 challenge, либо получать сертификат иначе. DNS-01 challenge — это когда Let’s Encrypt просит создать TXT‑запись _acme-challenge.<домен> с определённым значением и проверяет её через обычный DNS‑запрос, а не стучится на сам сервер. Порт наружу открывать не нужно вообще.
Проблема в том, что acme.sh (neilpang/acme.sh) из коробки умеет создавать такие TXT‑записи только у DNS‑провайдеров из своего встроенного списка. Моего там не было, значит хук пишем сами. У acme.sh контракт простой: функция dns_<provider>_add кладёт TXT‑запись, dns_<provider>_rm — убирает.
Хук под REST API произвольного DNS‑провайдера — рабочий шаблон, под свой API меняются только URL и разбор ответа:
#!/usr/bin/env sh # dnsapi/dns_myhost.sh - хук acme.sh под DNS-провайдера с собственным API. # Обязательные переменные окружения: MYHOST_AppKey, MYHOST_Login, MYHOST_Password MYHOST_Api="https://api.example-dns-provider.ru" _myhost_token() { curl -s -X POST "$MYHOST_Api/v1/access" \ -H "accept: application/json" \ -H "x-app-key: $MYHOST_AppKey" \ -u "$MYHOST_Login:$MYHOST_Password" \ | grep -o '"token":"[^"]*"' | cut -d'"' -f4 } dns_myhost_add() { fulldomain="$1"; txtvalue="$2" _token=$(_myhost_token) [ -z "$_token" ] && _err "MyHost: не получен token" && return 1 curl -s -X POST "$MYHOST_Api/v1/domains/records" \ -H "Authorization: Bearer $_token" -H "Content-Type: application/json" \ -d "{\"name\":\"$fulldomain\",\"type\":\"TXT\",\"value\":\"$txtvalue\"}" >/dev/null } dns_myhost_rm() { fulldomain="$1"; txtvalue="$2" _token=$(_myhost_token) # ищем и удаляем ровно ту запись, что добавляли - по значению txtvalue _id=$(curl -s -X GET "$MYHOST_Api/v1/domains/records?name=$fulldomain" \ -H "Authorization: Bearer $_token" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2) [ -z "$_id" ] && return 0 curl -s -X DELETE "$MYHOST_Api/v1/domains/records/$_id" \ -H "Authorization: Bearer $_token" >/dev/null }
Без dns_myhost_rm acme.sh честно отработает выпуск, но старые TXT‑записи будут копиться в DNS‑зоне до бесконечности — мелочь, но зона захламляется.
Контейнер добавляется в тот же compose как ещё один сервис, работает демоном и обновляет том с сертификатами:
acme: image: neilpang/acme.sh container_name: acme restart: unless-stopped command: daemon volumes: - vault_acme:/acme.sh - ./dnsapi:/acme.sh/dnsapi - ./certs:/certs environment: - MYHOST_AppKey=${MYHOST_AppKey} - MYHOST_Login=${MYHOST_Login} - MYHOST_Password=${MYHOST_Password} volumes: vault_acme:
Ключи от DNS‑аккаунта кладём в .env, а не в сам скрипт хука — хук уходит в git (если он у вас там), секреты — нет:
MYHOST_AppKey=<ключ_приложения> MYHOST_Login=<логин_от_панели> MYHOST_Password=<пароль_от_панели>
Отдельно скажу честно про болевую точку конкретно моего DNS‑провайдера: его legacy API не проходит через программный логин, если на аккаунте включена двухфакторка — значит, двухфакторку на этом аккаунте пришлось выключить. Дыра? Да. Закрываю её тем, что у этого аккаунта нет прав вообще ни на что, кроме управления DNS‑записями одного домена — скомпрометируют его, максимум что смогут — подменить DNS, а не залезть в сам волт.
Проверка, что автопродление реально настроено, а не просто контейнер запущен:
docker run --rm -v vault_acme:/acme.sh neilpang/acme.sh --list
В колонках Created/Renew должны быть даты, а не пустота — пустота значит, что сертификат ещё ни разу не выпускался через этот том.
Тезис 6. Бэкап в одну точку — это не бэкап, это отложенная катастрофа
TL;DR: Данные льются в двух направлениях одновременно — на выделенный SFTP‑аккаунт на своём VPS и в облако (у меня Яндекс.Диск). Упадёт один канал — точка восстановления всё равно есть.
Единственная точка бэкапа — это единственная точка отказа: если диск на том же VPS, где крутится сам сервис, откажет, вы теряете прод и бэкап одним и тем же инцидентом. Поэтому у меня два независимых бэкап‑контейнера на образе ttionya/vaultwarden‑backup — падение одной цели не роняет вторую:
backup-ssh: image: ttionya/vaultwarden-backup:latest container_name: vaultwarden_backup_ssh restart: unless-stopped volumes_from: - vaultwarden volumes: - vaultwarden-rclone-data:/config/ environment: DATA_DIR: /data RCLONE_REMOTE_NAME: vwssh RCLONE_REMOTE_DIR: backup/vaultwarden CRON: "0 3 * * *" ZIP_ENABLE: "TRUE" BACKUP_KEEP_DAYS: "30" TIMEZONE: Europe/Moscow backup-yandex: image: ttionya/vaultwarden-backup:latest container_name: vaultwarden_backup_yandex restart: unless-stopped volumes_from: - vaultwarden volumes: - vaultwarden-rclone-data:/config/ environment: DATA_DIR: /data RCLONE_REMOTE_NAME: vwyandex RCLONE_REMOTE_DIR: VaultwardenBackup CRON: "30 3 * * *" ZIP_ENABLE: "TRUE" BACKUP_KEEP_DAYS: "30" TIMEZONE: Europe/Moscow volumes: vaultwarden-rclone-data: external: true
volumes_from: vaultwarden — это то, что подтягивает боевой bind‑mount ./vw-data внутрь бэкап‑контейнера как /data, без него DATA_DIR: /data будет указывать в пустоту.
На VPS для SSH‑канала завёл отдельного пользователя без шелла и без пароля — только SFTP в свою же папку:
sudo useradd -m -d /home/vwbackup -s /usr/sbin/nologin vwbackup sudo passwd -l vwbackup sudo -u vwbackup mkdir -p /home/vwbackup/backup/vaultwarden sudo chmod 700 /home/vwbackup/backup
# /etc/ssh/sshd_config.d/vwbackup.conf Match User vwbackup ForceCommand internal-sftp AllowTcpForwarding no X11Forwarding no PermitTTY no PasswordAuthentication no AuthenticationMethods publickey
ForceCommand internal-sftp — это то, что не даёт скомпрометированному бэкап‑контейнеру превратиться в точку входа на весь VPS: даже с валидным ключом пользователь vwbackup физически не может получить интерактивный шелл, только гонять файлы внутри своей папки.
Отдельно по ретеншену: готовый образ не поддерживает правило «хранить минимум N последних копий» — только BACKUP_KEEP_DAYS. У меня было два варианта: BACKUP_KEEP_DAYS: "0" (хранить всё, чистить руками раз в квартал) — самое безопасное, но требует не забыть; либо "30" — при ежедневном бэкапе там физически всегда порядка 30 копий, «меньше трёх» не случится, пока бэкапы идут. Я взял второй вариант плюс внешний пинг‑мониторинг (PING_URL_WHEN_FAILURE на healthchecks.io) на случай, если бэкапы вдруг перестанут идти. Это закрывает мою реальную цель — не остаться совсем без копий — надёжнее, чем формальный счётчик «минимум три».
Итак, получается, что:
Одна цель бэкапа — это единая точка отказа, замаскированная под решённую задачу.
Ретеншен по дням плюс алерт на провал бэкапа практичнее искусственного счётчика копий.
Сервисный SSH‑пользователь без шелла — обязательный минимум, если бэкап льётся на свой VPS.
Тезис 7. Непротестированное восстановление — это не восстановление, а надежда
TL;DR: Первое восстановление я прогнал не в день инцидента, а заранее, на отдельной тестовой машине — и там же поймал ловушку с db.sqlite3-wal, о которую иначе разбился бы уже на боевом простое.
Бэкап, который ни разу не разворачивали, с равной вероятностью может оказаться битым архивом, базой с рассинхронизированным WAL‑журналом или просто устаревшим rclone‑конфигом — и узнаёте вы об этом не заранее, а в момент, когда бэкап реально нужен. Я тестировал восстановление отдельно, заранее, на отдельной машине — и вот что там вылезло.
Восстановление СУБД (система управления базами данных) SQLite имеет одну ловушку: если бэкап снимался через .backup или VACUUM INTO, перед восстановлением нужно вручную удалить старый db.sqlite3-wal (write‑ahead log — журнал незакоммиченных изменений). Не удалите — SQLite попробует накатить рассинхронизированный WAL поверх свежевосстановленной базы и получит битую БД на ровном месте:
docker compose stop vaultwarden Remove-Item .\vw-data\db.sqlite3-wal -ErrorAction SilentlyContinue # ... сюда распаковывается архив в vw-data ... docker compose start vaultwarden
Если бэкапили прямой копией db.sqlite3 вместе с парным db.sqlite3-wal — тогда, наоборот, восстанавливать нужно оба файла как пару, а не удалять WAL. Файл db.sqlite3-shm (shared memory) можно не бэкапить и не восстанавливать вообще — он пересоздаётся сам.
Второй урок пришёл не из документации, а из собственной ошибки: тестовую машину с восстановленной копией я поднял с тем же самым rclone‑конфигом, что и боевую. Первая же команда docker compose up -d подняла и бэкап‑контейнеры — а у них внутри свой cron, который не в курсе, что это тестовый стенд, и они полезли писать бэкапы тестовой копии поверх боевых на тех же remote. На тестовой машине с копией боевого стека бэкап‑контейнеры нужно гасить сразу, до первого up:
docker compose stop backup-ssh backup-yandex
А после того, как убедились, что восстановление работает — снести тестовый стек целиком:
docker compose down
Стало быть, тестовое восстановление — это не «распаковал архив и посмотрел, что файлы на месте», а полный цикл: остановить прод, поднять копию в изоляции, проверить логин в интерфейсе, погасить или изолировать всё, что могло бы полезть писать в боевые remote.
Тезис 8. Технология — это 20% внедрения. Остальные 80% — регламент, роли и обучение людей
TL;DR: Настроенный сервер никого не спасает, если сотрудники продолжают слать пароли текстом в чат, потому что «так привычнее». Внедрение — это письменная инструкция, обязательный VPN и 2FA для всех без исключения, и структура коллекций, в которую пароли физически некуда положить неправильно.
Грубо говоря, техническую часть я закрыл за несколько дней, а регламент утрясали с командой почти столько же. И именно регламент, не Docker Compose, определяет, приживётся инструмент или умрёт как предыдущая попытка в Notion.
Три решения, которые оказались важнее любого конфига:
Обязательный VPN и 2FA — без исключений. Не «желательно», а условие доступа к самому волту. 2FA настраивается по прямой ссылке /#/settings/security/two-factor сразу после первого входа, до того как в аккаунте появится хоть один пароль.
Передача паролей — только через Send, никогда текстом. Send — это встроенная в Bitwarden функция одноразовых ссылок с TTL (time to live, срок жизни) и опциональным паролем на сам доступ к ссылке. Правило простое: если пароль уже есть в волте — не плодим его копию через Send, отдаём ссылку на существующую запись через того, у кого есть права. Плодить дубликаты через Send “на всякий случай” запрещено отдельным пунктом инструкции — иначе через полгода в базе три протухшие копии одного и того же пароля, и непонятно, какая актуальна.
Коллекции по ролям, права выданы явно, а не унаследованы. На каждого клиента — четыре коллекции: паспорт проекта без единого пароля и три под‑коллекции по направлениям. Вложенность в названии коллекции («Клиент/Dev») — это просто текст, Vaultwarden её никак не разбирает, поэтому права на каждую коллекцию выдаются группе отдельно, а не наследуются от «родительской»:
Коллекция |
Dev |
Marketing |
Infra |
Руководители |
|---|---|---|---|---|
|
R/W |
R/W |
R/W |
R/W |
|
R/W |
- |
R/W |
R/W |
|
- |
R/W |
- |
R/W |
|
- |
- |
R/W |
R/W |
Отдельное правило — только для инфраструктурного направления: лицензии (1С‑Битрикс, платные шаблоны, CRM) хранятся исключительно там, и нигде больше, даже если формально ключ нужен разработчику для установки — он его запрашивает, а не заводит свою копию. Причина простая: если лицензии размазаны по трём коллекциям, никто не может быстро ответить на вопрос «сколько у нас вообще активных лицензий и когда какая продлевается» — а без ответа на этот вопрос легко пропустить платёж и словить блокировку модуля на боевом сайте клиента.
Чек‑лист заведения нового клиента я тоже довёл до пронумерованного списка — не потому что люблю бюрократию, а потому что без списка на третьем клиенте кто‑то обязательно забудет закрыть неиспользуемые поля шаблона:
Создать 4 коллекции, выдать права группам по таблице выше.
Импортировать CSV‑шаблон, заменив плейсхолдер на имя клиента.
Заполнить паспорт проекта — без единого пароля внутри.
Завести отдельные учётки Bitrix под разные роли, разложить по коллекциям.
Секретные поля — в Hidden, ключи и сертификаты — вложениями.
Удалить из шаблона всё, что в конкретном проекте не используется.
Отдельно закрыл вопрос ухода клиента: коллекции не удаляются, а архивируются, и перед архивацией все учётки в них меняются — иначе бывший клиент (или его бывший подрядчик) остаётся с рабочим паролем от систем, доступ к которым формально закрыт только у нас в голове.
Резюмируя внедрение:
Технически настроенный сервис без регламента — это просто ещё одно место, куда никто не пойдёт.
VPN и 2FA как жёсткое условие входа, а не рекомендация, снимают половину рисков ещё до того, как сотрудник открыл интерфейс.
Права, выданные явно на каждую коллекцию, и запрет на дублирование через Send — то, что не даёт базе превратиться в свалку копий за полгода.
Плюсы и минусы
Плюсы:
Полный контроль над данными: они физически лежат на моём железе, а не у стороннего вендора, и модель доступа — целиком моя, а не то, что предложил тарифный план.
Дешевле облачного Bitwarden Teams/Enterprise на масштабе команды от полутора‑двух десятков человек, если инфраструктура (сервер, VPN) у вас и так уже есть.
Прозрачность: любой вопрос «кто и когда получил доступ к этому паролю» закрывается логами на своей стороне, а не тикетом в поддержку вендора.
Минусы:
Вся эксплуатация — обновления образа, продление сертификата, бэкапы, мониторинг — легла на одного человека. Это удар по бас‑фактору (bus factor): если завтра я в отпуске без связи и что‑то упадёт, чинить будет некому.
Android‑клиент Bitwarden не работает с самоподписанным CA — пришлось отдельно переезжать на DNS-01 и настоящий Let’s Encrypt, это лишний виток работы, которого не было бы с публичным сертификатом с самого начала.
VPN‑only доступ требует дисциплины от всей команды: сотрудник без поднятого VPN — это уже тикет «не открывается волт», а не редкость.
Если хост с Docker Desktop не поднимется сам после перезагрузки (а такое бывает), бэкап по расписанию просто не выполнится — и без внешнего пинг‑мониторинга вы узнаете об этом не в момент сбоя, а в момент, когда бэкап реально понадобится.
От первой команды openssl genrsa до рабочего прод‑контура с бэкапами и протестированным восстановлением у меня ушло 3–4 полных рабочих дня. Ещё столько же — на регламент и обкатку с командой.
Вывод
Self‑hosted Vaultwarden обходится на порядок дешевле корпоративного тарифа Bitwarden и даёт полный контроль над тем, где лежат пароли компании — но этот контроль означает, что сертификаты, бэкапы и их восстановление теперь ваша головная боль, а не вендора. Если у вас уже есть VPN, свободный сервер и готовность нести эту эксплуатационную нагрузку — экономия и прозрачность того стоят; если нет ни того ни другого — считайте честно, во что обойдётся всё это поднять с нуля, прежде чем сравнивать цену с облачным тарифом.
Комментарии (6)

polyfusion
23.09.2026 16:20Уже более года пользуемся vaultwarden примерно по описанной в статье схеме. Что хочу отметить (в основном, косяки Bitwarden и связанные с ним, но тем не менее):
Несколько неочевидная система приглашения: на почту уходит ссылка, пользователь нажимает на неё, но после этого не получает доступ в организацию, а попадает в лимб, где администратору нужно зайти и прокликать принятие принятия им приглашения.
Вложенные коллекции — это больно. Доступ на коллекции не транзитивный, если дать доступ на родительскую коллекцию, то это не будет автоматически выдавать доступ на дочерние. Да, сесурити. Но сильно мешает, когда этих коллекций за дюжину и более.
Vaultwarden нельзя использовать в, собственно, качестве vault для хранения и доставания токенов и прочего. Можно возиться с bw cli и парсить его вывод, можно попытаться пропатчить SDK, но мы пытались прикрутить его к ansible-деплоям и выяснилось, что для этого придётся форкать, фиксить и поддерживать форк SDK.
Недавно они сломали обратную совместимость на плагинах, из-за чего пришлось удалять и переустанавливать браузерный плагин. Когда контора маленькая, то это нормально, но я представляю лицо администратора энтерпрайза на 500+ человек.
В остальном: шикарный софт, особенно сейчас, когда он нативно поддерживает переход из KeePass. И выбор SQLite как базы данных вместо MongoDB или Postgres был очень мудрым и сильно облегчил снятие бэкапов/перенос на другую машину.
Как-нибудь у меня дойдут руки и я попробую перенести деплой в кубер, но это будет не сегодня.

Grand_piano
23.09.2026 16:20Acme закрывается сильно проще через nginx 1.30.x + acme-module. Несколько строчек конфига и головная боль уходит навсегда.

AO_ZORIN
23.09.2026 16:20Фундаментально понятно, что секреты надо где-то безопасно хранить. А вот мне интересно другое, сейчас сотрудники часто могут использовать ИИ с корпоративными данными и что VPN, что выделенное хранилище паролей, они сами от внутренней утечки в компании не спасут, сотрудник кинет в харнесс доступы и скажет агенту задеплоить фичу, а сам пойдет чай пить. Как вы боретесь с этим? Локальные модели, запрет на использование ИИ?
bloomeatt
Сам активно гоняю VW, но для личного/семейного пользования. От корпоративного отказались из-за скудной поддержки разных корп фич, в частности, LDAP'а. Остановились на платных аналогах. Контора не обеднеет, а вот качество жизни улучшается заметно.
Главный минус держать волт за впн - это если упадет сам впн, а тебя рядом нет. Поэтому личный держу открытым, проблему особо любопытных пока удается сдержать настроенным fail2ban
devnna Автор
Согласен.
1) У клиента есть отдельно сис админ, который чисто сетевухой занимается и самим ВПН. Поэтому это, так скажем, его обязанность и таких случаев ещё не было.
Но на раскате проблема с availability другая: так как машина под вольт - это ранее обычный стационарный ПК (приставка), то бывает он просто вырубается. Хотя в настройках энергосбережения поставил все как надо. Дергать нужно ребят, чтобы они включали его вживую и смотрели, чтобы докер (автозапуск на него настроил) был включен.
Если бы была vps то было бы проще в этом плане.
Если есть мысли, как получать доступ к такой выключенной машине из внутреннего контура, как к ВПС, чтобы включать ее когда нету питания, то, пожалуйста, делитесь - попробую интегрировать)
2) У нас нету LDAP. Хотя потенциал внедрить есть. И за такой автоматизацией явно были бы плюсы и удобства. Хотя по идее можно что-то кастомное на VW накатить, чтобы по api и ldap внутренним общалось. Ну тут уже идея ради идеи, проще было бы что-то купить.
3) Ну и ещё минус extension от Bitwarden на self-hosted не работает на устройствах :/
polyfusion
Как человек, который очень много работал с mTLS: VPN в качестве сетевой изоляции почти всегда можно заменить на грамотно настроенный вебсервер с mTLS. Да, для этого нужно хорошо понимать x509, но окупается сторицей, особенно в российских реалиях, где один VPN почти наверняка уже включён.