Как я поднял self‑hosted менеджер паролей и заставил компанию им пользоваться

—

от автора

Я убеждён, что пароль в закреплённом сообщении Telegram или в общем экселе рано или поздно утечёт — дело не в везении, а в том, сколько людей его уже увидели и скопировали себе «на всякий случай». Полгода назад у нас в компании было ровно так: доступы к Bitrix, SSH‑ключи, лицензии на модули — россыпью по личным чатам, гуглдокам и голове того, кто это заводил. Уходит сотрудник — доступы за ним никто не забирает, потому что толком не знает, какие у него вообще были. Разбираться с этим я сел после того, как в очередной раз не смог сходу сказать, какой из трёх паролей к хостингу актуальный — это и стало последней каплей.

Мы — digital‑агентство, ведём с десяток клиентских проектов на Bitrix параллельно, три условных направления (разработка, дизайн/маркетинг, инфраструктура/CRM) и общую инфраструктуру поверх. Ставил задачу так: один инструмент, куда переезжают все пароли, ключи и лицензии, доступ строго по ролям, и чтобы это реально прижилось в команде, а не полежало неделю и умерло, как предыдущая попытка завести таблицу в Notion.

Ниже — восемь выводов, к которым я пришёл по ходу. Шаги «как поставить Vaultwarden» я тут не расписываю — это есть в вики проекта. Расскажу про решения, компромиссы и конкретные места, где я ошибся и потом переделывал.

  1. Свой сервис для секретов — я просто скопировал то, что давно делает бигтех.

  2. Доступ только из VPN — я закладывал такую модель ещё на аутсорсе, под требования клиентов.

  3. Автоматическая раздача сертификатов на устройства — роскошь бигтеха, у меня на неё нет ресурса.

  4. Caddy и Vaultwarden в одном docker‑compose — вся боевая связка, остальное — обвязка вокруг неё.

  5. DNS-01 через acme.sh — и порт 80 наружу открывать не пришлось.

  6. Бэкап в одну точку — это не бэкап, это отложенная катастрофа.

  7. Непротестированное восстановление — это не восстановление, а надежда.

  8. Технология — это 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 4096openssl 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 2048openssl req -new -key vault.key -out vault.csr \  -subj "/C=RU/O=Example Company/CN=vault.example.com"cat > vault.ext <<'EOF'authorityKeyIdentifier=keyid,issuerbasicConstraints=CA:FALSEkeyUsage=digitalSignature,keyEnciphermentextendedKeyUsage=serverAuthsubjectAltName=@alt_names[alt_names]DNS.1 = vault.example.comEOFopenssl 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 — файлы сертификата просто монтируются в контейнер:

# Caddyfilevault.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 одной строкой, без переносов и без лишних кавычек внутри самого хеша:

# .envDOMAIN=https://vault.example.comSIGNUPS_ALLOWED=trueINVITATIONS_ALLOWED=falseSHOW_PASSWORD_HINT=falseADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$c29tZXNhbHQ$...'

DOMAIN обязан посимвольно совпадать с адресом, по которому вы реально заходите. Разойдётся протокол или поддомен — не заработают WebAuthn (аппаратные ключи для 2FA) и вложения к записям. SIGNUPS_ALLOWED=true — временная дыра, нужна только чтобы создать первый аккаунт; как только он создан, значение переключаем на false, иначе регистрация открыта для кого угодно, кто узнает адрес волта.

Сам compose — минимальный, без лишней обвязки на старте:

# docker-compose.ymlservices:  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:/datavolumes:  caddy_data:  caddy_config:

На первом запуске я на пару минут пробрасывал 127.0.0.1:8000:80 у vaultwarden, чтобы проверить контейнер локально ещё до того, как разобрался с сертификатом и DNS. Строку убрал сразу после проверки — постоянный проброс порта в обход Caddy обнуляет весь смысл TLS‑терминации на реверс‑прокси.

Запуск и первая проверка живости:

docker compose up -ddocker 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_PasswordMYHOST_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/Moscowvolumes:  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 vwbackupsudo passwd -l vwbackupsudo -u vwbackup mkdir -p /home/vwbackup/backup/vaultwardensudo chmod 700 /home/vwbackup/backup
# /etc/ssh/sshd_config.d/vwbackup.confMatch 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 vaultwardenRemove-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

<Клиент>/Dev

R/W

—

R/W

R/W

<Клиент>/Marketing

—

R/W

—

R/W

<Клиент>/Infra

—

—

R/W

R/W

Отдельное правило — только для инфраструктурного направления: лицензии (1С‑Битрикс, платные шаблоны, CRM) хранятся исключительно там, и нигде больше, даже если формально ключ нужен разработчику для установки — он его запрашивает, а не заводит свою копию. Причина простая: если лицензии размазаны по трём коллекциям, никто не может быстро ответить на вопрос «сколько у нас вообще активных лицензий и когда какая продлевается» — а без ответа на этот вопрос легко пропустить платёж и словить блокировку модуля на боевом сайте клиента.

Чек‑лист заведения нового клиента я тоже довёл до пронумерованного списка — не потому что люблю бюрократию, а потому что без списка на третьем клиенте кто‑то обязательно забудет закрыть неиспользуемые поля шаблона:

  1. Создать 4 коллекции, выдать права группам по таблице выше.

  2. Импортировать CSV‑шаблон, заменив плейсхолдер на имя клиента.

  3. Заполнить паспорт проекта — без единого пароля внутри.

  4. Завести отдельные учётки Bitrix под разные роли, разложить по коллекциям.

  5. Секретные поля — в Hidden, ключи и сертификаты — вложениями.

  6. Удалить из шаблона всё, что в конкретном проекте не используется.

Отдельно закрыл вопрос ухода клиента: коллекции не удаляются, а архивируются, и перед архивацией все учётки в них меняются — иначе бывший клиент (или его бывший подрядчик) остаётся с рабочим паролем от систем, доступ к которым формально закрыт только у нас в голове.

Резюмируя внедрение:

  • Технически настроенный сервис без регламента — это просто ещё одно место, куда никто не пойдёт.

  • 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, свободный сервер и готовность нести эту эксплуатационную нагрузку — экономия и прозрачность того стоят; если нет ни того ни другого — считайте честно, во что обойдётся всё это поднять с нуля, прежде чем сравнивать цену с облачным тарифом.

ссылка на оригинал статьи https://habr.com/ru/articles/1085790/