diff --git a/README.md b/README.md index bb93785..067702e 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ **Точка входа:** [Архитектура и подключение](docs/architecture/architecture.md) — схема сети, IP, домены, таблица всех хостов. **Топология и риски:** [Схема сети и зависимости](docs/network/network-topology.md) — узлы, маршруты NPM, зависимости сервисов, единые точки отказа (SPOF). -**Приоритет №1:** [Бэкапы Proxmox (фаза 1)](docs/backup/proxmox-phase1-backup.md) — стратегия бэкапов LXC/VM и /etc/pve, тестовое восстановление. +**Приоритет №1:** [Бэкапы: как устроены и как восстанавливать](docs/backup/backup-howto.md) — что бэкапится, куда, когда и как восстановить. --- diff --git a/docs/architecture/architecture.md b/docs/architecture/architecture.md index 2484361..d19d836 100644 --- a/docs/architecture/architecture.md +++ b/docs/architecture/architecture.md @@ -9,8 +9,9 @@ - **Внешний IP:** 185.35.193.144 - **Домашний сервер (Proxmox):** 192.168.1.150 (LAN) - Подключение: `ssh root@192.168.1.150` +- **Прямой SSH на контейнеры и ВМ:** `ssh root@192.168.1.{100,101,103,104,105,107,108,109}`; ВМ 200: `ssh admin@192.168.1.200`. Ключи развёртываются скриптом `scripts/deploy-ssh-keys-homelab.sh`. - **DNS домена katykhin.ru:** Beget.com - - Учётная запись: логин `amauri7g`, пароль `QgkaKL3RykeI`, ID аккаунта 2536839. Режим API включён. Домен **katykhin.store** в аккаунте есть, но не используется (поддоменов нет). + - Учётная запись: логин и пароль в Vaultwarden (объект **beget**). Режим API включён. Домен **katykhin.store** в аккаунте есть, но не используется (поддоменов нет). - **Reverse proxy и SSL:** Nginx Proxy Manager (NPM) на контейнере 100. **Поддомены katykhin.ru:** diff --git a/docs/backup/backup-howto.md b/docs/backup/backup-howto.md index 76689d8..83738b0 100644 --- a/docs/backup/backup-howto.md +++ b/docs/backup/backup-howto.md @@ -433,7 +433,7 @@ restic restore SNAPSHOT_ID --target /mnt/backup/restore-db --path /mnt/backup/da Если дампы БД (Nextcloud, Paperless, Gitea), архив Vaultwarden или векторы RAG (CT 105) получаются по 20 байт — в копию попал только пустой gzip/tar, команда внутри контейнера не отдала данные. Скрипты при размере < 512 байт завершаются с ошибкой и выводят stderr. Для векторов проверьте путь `/home/rag-service/data/vectors` в CT 105: `pct exec 105 -- ls -la /home/rag-service/data/vectors`. -**Частая причина дампов БД:** PostgreSQL в контейнере требует пароль (md5/scram). Скрипты берут пароль из **Vaultwarden** (Bitwarden CLI `bw`): объекты **NEXTCLOUD** (поле `dbpassword` или пароль), **PAPERLESS**, **GITEA**. На хосте нужны: `bw`, при необходимости `jq`, разблокировка по мастер-паролю из файла `/root/.bw-master` (см. [Переключение скриптов на секреты из Vaultwarden](proxmox-phase1-backup.md#переключение-скриптов-на-секреты-из-vaultwarden) в proxmox-phase1-backup.md). +**Частая причина дампов БД:** PostgreSQL в контейнере требует пароль (md5/scram). Скрипты берут пароль из **Vaultwarden** (Bitwarden CLI `bw`): объекты **NEXTCLOUD** (поле `dbpassword` или пароль), **PAPERLESS**, **GITEA**. На хосте нужны: `bw`, при необходимости `jq`, разблокировка по мастер-паролю из файла `/root/.bw-master` (см. [vaultwarden-secrets.md](../vaultwarden-secrets.md)). **Проверка вручную (без подавления stderr):** зайти в контейнер и выполнить дамп, чтобы увидеть сообщение об ошибке: @@ -522,7 +522,7 @@ chmod 600 /root/.telegram-notify.env ## Связанные документы -- [Стратегия бэкапов (фаза 1)](proxmox-phase1-backup.md) — общий план и принятые решения. +- [Vaultwarden и секреты](../vaultwarden-secrets.md) — получение паролей через `bw` для скриптов бэкапов. - [Архитектура](../architecture/architecture.md) — хост, IP, доступ. - [VM 200 (Immich)](../containers/container-200.md) — сервисы, пути, .env. diff --git a/docs/backup/proxmox-phase1-backup.md b/docs/backup/proxmox-phase1-backup.md deleted file mode 100644 index 6bce58e..0000000 --- a/docs/backup/proxmox-phase1-backup.md +++ /dev/null @@ -1,377 +0,0 @@ -# Фаза 1: Стратегия бэкапов Proxmox - -Цель: при смерти SSD с системой или потере `/etc/pve` — развернуть Proxmox, восстановить контейнеры/ВМ, поднять сервисы без многочасового восстановления и угадывания паролей. - -**Приоритет №1.** ИБП уже есть; защищаемся от смерти диска. - ---- - -## Что бэкапить - -| Объект | Зачем | -|--------|--------| -| **LXC и VM целиком** (vzdump) | Восстановление контейнера/ВМ из одного архива: ОС, конфиги, данные на корневом томе. Не только данные внутри — сам образ для restore. | -| **/etc/pve** | Конфиги кластера, VM/LXC (ID, сеть, диски, задачи), пользователи Proxmox, права. Без этого после переустановки Proxmox не восстановить привязку дисков и настройки. | - ---- - -## Пошаговый план - -### Шаг 1. Определить хранилище для бэкапов - -**Выбранная схема:** - -| Место | Описание | Что туда | -|-------|----------|----------| -| **Локально: /dev/sdb1 (1 ТБ под бэкапы)** | Отдельный SSD 2 ТБ; под копии выделен 1 ТБ, смонтирован в `/mnt/backup`. Второй ТБ — в запас. | Proxmox vzdump (через UI), затем те же данные (dump, etc-pve, фотки, VPS) в Yandex через restic. Фотки: оригиналы + метаданные + БД Immich; остальное пересчитать можно. VPS: Amnezia — конфиг; Миран — БД бота + контент (контент можно в S3 Мирана; копию на sdb1 — опционально). Конфигурацию серверов не бэкапим — есть Ansible. | -| **Офсайт: Yandex Object Storage (S3)** | Арендованный бакет, S3-совместимый API. [Yandex Object Storage](https://yandex.cloud/ru/docs/storage/s3/). | **Restic** с хоста (cron на ноде Proxmox): выгрузка содержимого `/mnt/backup`. Retention: 3 daily, 2 weekly, 2 monthly. | - -**Принято:** Вариант A — отдельный диск/раздел на хосте (sdb1, 1 ТБ в `/mnt/backup`). Варианты B (NFS/SMB) и C (внешний USB) в текущей схеме не используются; USB опционально для параноидального 3-2-1 (см. выше). - -**3-2-1:** Три копии: (1) прод — система и данные на основном диске; (2) локальный бэкап — sdb1; (3) офсайт — Yandex. Два типа носителей: локальный SSD и облачное object storage. Один офсайт — да. **Стратегия удовлетворяет 3-2-1.** Опционально: четвёртая копия на внешнем USB/другом ПК для параноидального сценария (пожар/кража) — по желанию. - -**Принято:** Точка монтирования — `/mnt/backup`. На диске 2 ТБ выделено 1 ТБ под бэкапы; второй ТБ пока в запас, назначение не определено. - -**Действие:** Разметить 1 ТБ на /dev/sdb1, создать ФС (ext4/xfs), смонтировать в `/mnt/backup`. Структуру каталогов — см. раздел «Структура локального хранилища» ниже. - ---- - -### Шаг 2. Добавить Backup Storage в Proxmox - -1. В веб-интерфейсе: **Datacenter → Storage → Add**. -2. Тип: **Directory** (если папка на локальном диске) или **NFS/CIFS** (если сетевое). -3. Указать: - - **ID** (например `backup-local` или `backup-nfs`), - - **Directory** — путь, например `/mnt/backup/dump`, - - Включить опции **Content: VZDump backup file** (и при необходимости **ISO**, **Container template** — по желанию). -4. Сохранить. Убедиться, что storage виден и доступен для записи. - -Если используешь NFS: сначала смонтировать NFS в `/mnt/backup` на хосте (fstab или systemd mount), затем добавить Storage с Directory `/mnt/backup/dump`. - -**Для выбранной схемы:** Directory = `/mnt/backup/proxmox/dump` (см. структуру ниже). - ---- - -### Структура локального хранилища (1 ТБ на sdb1) - -«Аналог S3» на одном диске — это просто понятная иерархия каталогов. **MinIO не используем:** лишний сервис; достаточно директорий и restic с бэкендом `local` (если понадобится локальный restic) и `s3` для Yandex. Proxmox пишет в **Directory** — ему достаточно пути. - -Пример структуры под `/mnt/backup`: - -``` -/mnt/backup/ -├── proxmox/ -│ ├── dump/ ← Proxmox Backup Job (VZDump) — сюда добавляем Storage в PVE -│ └── etc-pve/ ← архивы tar.gz из cron: etc-pve-*, etc-host-configs-* (backup-etc-pve.sh) -├── restic/ -│ ├── local/ ← репозитории restic для локальных снапшотов (опционально) -│ └── ... ← или restic только в Yandex, локально только сырые копии -├── photos/ ← Immich: оригиналы фото + метаданные + БД (остальное пересчитать) -├── vps/ ← Amnezia: конфиг. Миран: БД бота (+ контент при необходимости; основной контент можно в S3 Мирана) -└── other/ ← прочие важные данные (конфиги, скрипты, что ещё решите) -``` - -Квоты: при необходимости ограничить размер по каталогам (например `proxmox/dump` — не более 500 ГБ) через отдельные подразделы или скрипты очистки (retention). - ---- - -### Шифрование диска бэкапов (LUKS) - -**LUKS** (Linux Unified Key Setup) — стандартное шифрование раздела в Linux. Если диск с бэкапами украдут или вынесут, без пароля/ключа данные не прочитать. Минусы: нужно вводить пароль при загрузке (или хранить ключ на другом носителе), небольшая нагрузка на CPU. - -**Принято:** LUKS пока не используем. Раздел sdb1 — без шифрования. При необходимости можно добавить позже (потребуется перенос данных). - ---- - -### Restic и Yandex Object Storage - -- **Restic** поддерживает бэкенд **S3**. Yandex Object Storage совместим с S3 API — используешь endpoint бакета и ключи (Access Key / Secret). -- **Retention в Yandex:** 3 daily, 2 weekly, 2 monthly — `restic forget --keep-daily 3 --keep-weekly 2 --keep-monthly 2` и затем `prune`. -- **Что гнать в Yandex через restic:** Proxmox dump (каталог `proxmox/dump`), `/etc/pve` (архивы из `proxmox/etc-pve`), фотки (оригиналы + метаданные + БД Immich), бэкапы с VPS — см. «Принятые решения: что куда» ниже. - -**Локально отдельной политики retention для restic не нужно:** на sdb1 retention задаётся в самом Proxmox Backup Job (например «keep last 7») и в скрипте бэкапа `/etc/pve` (удалять архивы старше N дней). Restic используется только для отправки в Yandex с политикой 7/4/6. Локальных restic-репозиториев можно не заводить — только каталоги и выгрузка их содержимого в облако. - -Документация Yandex: [Object Storage S3](https://yandex.cloud/ru/docs/storage/s3/). Нужны: bucket name, region, endpoint, Static Key (Access Key ID + Secret Access Key). **Бакет создан; ключи и endpoint зафиксировать при настройке restic.** - ---- - -### Хранилище паролей - -Чтобы не терять пароли при восстановлении и держать креды в одном месте. Рассматривались варианты: Vaultwarden (self-hosted), Bitwarden Cloud, KeePass/KeePassXC, 1Password и др. - -**Принято и сделано:** **Vaultwarden** развёрнут на **CT 103** LXC. Домен через NPM (HTTPS), клиенты Bitwarden на ПК/телефоне. Бэкап данных Vaultwarden включён в общий план (restic → Yandex). - ---- - -### Где запускать бэкапы (централизация) - -**С хоста Proxmox (cron на ноде):** - -- **Proxmox Backup Job** уже выполняется на хосте и пишет vzdump всех выбранных LXC/VM в `/mnt/backup/proxmox/dump` — это и есть «бэкапы всех контейнеров и ВМ», централизованно. -- **Restic** (backup/forget/prune) тоже запускаем с хоста по cron: бэкапит каталоги на хосте (например весь `/mnt/backup` или выбранные подкаталоги) в Yandex S3. Данные для бэкапа — локальные пути (dump, etc-pve, а фотки и данные с VPS нужно либо копировать на хост в `/mnt/backup` скриптами, либо монтировать и тогда restic будет их читать с хоста). Контейнеры и ВМ целиком не бэкапим через restic — ими занимается только Proxmox Backup Job. - ---- - -### Шаг 3. Настроить расписание бэкапа LXC и VM - -1. **Datacenter → Backup** (или **Backup** в меню узла). -2. **Add** — создаётся задача (Job). -3. Параметры: - - **Storage** — выбранное хранилище (шаг 2). - - **Schedule** — например `0 2 * * *` (каждую ночь в 02:00). Подстроить под окно, когда нагрузка минимальна. - - **Selection mode:** включить нужные узлы (или **All**), затем отметить **галочками** конкретные **LXC (100–108) и VM (200)**. Либо выбрать "Backup all" для всех VMs/containers. - - **Mode:** - - **Snapshot** — контейнер/ВМ не останавливается, создаётся снимок (рекомендуется для минимизации даунтайма). - - **Suspend** — ВМ приостанавливается на время бэкапа (более консистентно для БД, но даунтайм). - Для LXC обычно достаточно **Snapshot**. Для **VM 200** (PostgreSQL и др.): Snapshot **не гарантирует консистентность БД** — PostgreSQL может быть в середине транзакции. **Правильная стратегия:** внутри VM делать логический бэкап БД (`pg_dump`), а **vzdump snapshot** использовать для остального (ОС, конфиги, файлы). Итого: VM 200 — vzdump snapshot ок для образа; консистентность БД — отдельно через `pg_dump` внутри гостя. - - **Compression:** ZSTD (хороший компромисс скорость/размер). - - **Retention:** например «Keep last 7» или «Keep last 4 weekly» — чтобы не забивать диск. - -4. Сохранить job. Проверить по кнопке **Backup now**, что задача запускается и файлы появляются в Storage. - -Важно: бэкап должен включать **и LXC, и VM 200**. Не только данные внутри них (те уже описаны в документации контейнеров), а именно полный dump для restore. - ---- - -### Шаг 4. Бэкап /etc/pve и конфигов хоста - -Конфиги кластера и виртуалок лежат в `/etc/pve`. Плюс для восстановления хоста полезны: `/etc/network/interfaces`, `/etc/hosts`, `/etc/resolv.conf`. Всё это нужно копировать регулярно и хранить в безопасном месте (желательно не только на том же диске, что и система). - -**Принято: вариант A — cron на хосте Proxmox.** - -1. Создать скрипт, например `/root/scripts/backup-etc-pve.sh`: - -```bash -#!/bin/bash -BACKUP_ROOT="/mnt/backup/proxmox/etc-pve" # по структуре выше -DATE=$(date +%Y%m%d-%H%M) -mkdir -p "$BACKUP_ROOT" -tar -czf "$BACKUP_ROOT/etc-pve-$DATE.tar.gz" -C / etc/pve -tar -czf "$BACKUP_ROOT/etc-host-configs-$DATE.tar.gz" -C / etc/network/interfaces etc/hosts etc/resolv.conf -# опционально: удалять бэкапы старше N дней -# find "$BACKUP_ROOT" -name 'etc-pve-*.tar.gz' -mtime +30 -delete -# find "$BACKUP_ROOT" -name 'etc-host-configs-*.tar.gz' -mtime +30 -delete -``` - -2. Сделать исполняемым: `chmod +x /root/scripts/backup-etc-pve.sh`. -3. Добавить в cron: `crontab -e`. Окно внутренних бэкапов 01:00–03:30; пример для etc-pve: `15 2 * * * /root/scripts/backup-etc-pve.sh` - -**Вариант B:** Тот же скрипт можно вызывать из задачи в Proxmox (Script/Command в задаче типа Hook script), но проще и надёжнее — отдельный cron на хосте. - -Бэкапы (`etc-pve-*.tar.gz`, `etc-host-configs-*.tar.gz`) хранить **локально** (`/mnt/backup/proxmox/etc-pve`) и **в Yandex** — включить этот каталог в источники restic (мало весит, критично при потере хоста). Файлы с ограниченными правами (chmod 600); `/etc/pve` содержит секреты — не выкладывать в открытый доступ. - ---- - -### Шаг 5. Хранить секреты отдельно (пароли, ключи) - -Чтобы «не вспоминать пароли 3 часа» после восстановления: - -**Секретное хранилище** — Vaultwarden на **CT 103** (см. выше). Туда: root Proxmox, пользователи PVE, пароли БД и сервисов (Nextcloud, Gitea, Paperless, Immich, NPM, Galene и т.д.), API-ключи (Beget, certbot, Wallos и др.). Полный список кредов по контейнерам — в статьях container-100 … container-200; свести в один список в Vaultwarden и обновлять при смене паролей. - -Это не «шаг бэкапа», но обязательная часть восстановления: без паролей восстановленные контейнеры не войдут в сервисы. - -**Инвентаризация секретов для переноса в Vaultwarden** — ниже сводная таблица: где лежат креды сейчас и какой объект в Vaultwarden им соответствует. Команды для получения значений из Vaultwarden и переключение скриптов — в разделе «Получение секретов из Vaultwarden» ниже. - -| Хост / CT / VM | Текущее место | Объект Vaultwarden | -|----------------|----------------|---------------------| -| Proxmox (хост) | root, пользователи PVE | (в менеджере вручную) | -| Proxmox (хост) | `/root/.restic-yandex.env`, `/root/.restic-password` | **RESTIC** (поля: RESTIC_BACKUP_KEY, RESTIC_REPOSITORY, AWS_ACCESS_KEY_ID, AWS_DEFAULT_REGION, AWS_SECRET_ACCESS_KEY, TELEGRAM_SELF_CHAT_ID) | -| Proxmox (хост) | `/root/.telegram-notify.env` | **HOME_BOT_TOKEN** (пароль = токен бота), **RESTIC** (поле TELEGRAM_SELF_CHAT_ID = chat_id) | -| CT 100 | `/root/.secrets/certbot/beget.ini` | **beget** (логин, пароль) | -| CT 100 | NPM админка | (в менеджере вручную) | -| CT 100 | VPN Route Check compose | **localhost** (логин admin, пароль, поле ROUTER_TELNET_HOST) | -| CT 100 | custom_ssl / letsencrypt | (восстановление из /etc/letsencrypt; в Vaultwarden не храним) | -| CT 101 | Nextcloud compose, config.php | **NEXTCLOUD** (логин nextcloud, пароль; поля: NEXTCLOUD_TRUSTED_DOMAINS, instanceid, passwordsalt, secret, dbpassword) | -| CT 103 | Gitea compose, .env, app.ini | **GITEA** (логин gitea, пароль; поля: GITEA__database__DB_TYPE, GITEA__database__HOST, GITEA_RUNNER_REGISTRATION_TOKEN, LFS_JWT_SECRET, INTERNAL_TOKEN) | -| CT 103 | CouchDB local.ini | **OBSIDIAN** (логин obsidian, пароль) | -| CT 103 | Vaultwarden .env | **VAULTWARDEN** (пароль = ADMIN_TOKEN, поле SIGNUPS_ALLOWED) | -| CT 104 | Paperless compose, docker-compose.env | **PAPERLESS** (логин paperless, пароль; поля: PAPERLESS_URL, PAPERLESS_SECRET_KEY, PAPERLESS_TIME_ZONE, PAPERLESS_OCR_LANGUAGE, PAPERLESS_OCR_LANGUAGES) | -| CT 107 | Invidious compose | **INVIDIOUS** (логин kemal, пароль; поля: SERVER_SECRET_KEY, test) | -| CT 108 | ice-servers.json | **GALENE** (поле config — JSON TURN) | -| VM 200 | `/opt/immich/.env` | **IMMICH** (логин/пароль и поля по .env) | -| VM 200 | `/opt/immich-deduper/.env` | **IMMICH_DEDUPER** (логин postgres, пароль; поля: DEDUP_PORT, DEDUP_DATA, DEDUP_IMAGE, IMMICH_PATH, PSQL_HOST, PSQL_PORT, PSQL_DB) | -| Proxmox (хост) | `/root/.vps-miran-s3.env` | **MIRAN_S3** (поля: S3_ACCESS_KEY, S3_SECRET_KEY, S3_BUCKET_NAME) | - ---- - -#### Получение секретов из Vaultwarden - -**Требования:** установлены `bw` (Bitwarden CLI) и `jq`; настроен сервер: `bw config server https://vault.katykhin.ru`. Мастер-пароль задаётся через переменную `BW_MASTER_PASSWORD` или через **файл с доступом только для текущего пользователя** (`chmod 600`), например `/root/.bw-master`; файл не хранить в репозитории. Перед запросами: `bw sync` и разблокировка: `export BW_SESSION=$(bw unlock --passwordenv BW_MASTER_PASSWORD --raw)` или `bw unlock --passwordfile /path/to/file --raw`. - -**Команды по объектам** (выполнять после `bw unlock` в той же сессии): - -| Объект | Логин / пароль | Кастомное поле | -|--------|----------------|----------------| -| **beget** | `bw get username "beget"`, `bw get password "beget"` | — | -| **GALENE** | — | `bw get item "GALENE" \| jq -r '.fields[] \| select(.name=="config") \| .value'` | -| **GITEA** | `bw get username "GITEA"`, `bw get password "GITEA"` | `bw get item "GITEA" \| jq -r '.fields[] \| select(.name=="ИМЯ_ПОЛЯ") \| .value'` — подставить GITEA_RUNNER_REGISTRATION_TOKEN, LFS_JWT_SECRET, INTERNAL_TOKEN, GITEA__database__DB_TYPE, GITEA__database__HOST | -| **HOME_BOT_TOKEN** | — | пароль = токен: `bw get password "HOME_BOT_TOKEN"` | -| **IMMICH** | по структуре в Vaultwarden | `bw get item "IMMICH" \| jq '.fields'` | -| **IMMICH_DEDUPER** | `bw get username "IMMICH_DEDUPER"`, `bw get password "IMMICH_DEDUPER"` | поля DEDUP_*, IMMICH_PATH, PSQL_* — через `jq -r '.fields[] \| select(.name=="X") \| .value'` | -| **INVIDIOUS** | `bw get username "INVIDIOUS"`, `bw get password "INVIDIOUS"` | `bw get item "INVIDIOUS" \| jq -r '.fields[] \| select(.name=="SERVER_SECRET_KEY") \| .value'` | -| **localhost** | `bw get username "localhost"`, `bw get password "localhost"` | `bw get item "localhost" \| jq -r '.fields[] \| select(.name=="ROUTER_TELNET_HOST") \| .value'` | -| **MIRAN_S3** | — | S3_ACCESS_KEY: `bw get item "MIRAN_S3" \| jq -r '.fields[] \| select(.name=="S3_ACCESS_KEY") \| .value'`; аналогично S3_SECRET_KEY, S3_BUCKET_NAME | -| **NEXTCLOUD** | `bw get username "NEXTCLOUD"`, `bw get password "NEXTCLOUD"` | secret: `bw get item "NEXTCLOUD" \| jq -r '.fields[] \| select(.name=="secret") \| .value'`; dbpassword, passwordsalt, instanceid, NEXTCLOUD_TRUSTED_DOMAINS — то же с `.name=="..."` | -| **OBSIDIAN** | `bw get username "OBSIDIAN"`, `bw get password "OBSIDIAN"` | — | -| **PAPERLESS** | `bw get username "PAPERLESS"`, `bw get password "PAPERLESS"` | PAPERLESS_SECRET_KEY, PAPERLESS_URL и др. — `bw get item "PAPERLESS" \| jq -r '.fields[] \| select(.name=="PAPERLESS_SECRET_KEY") \| .value'` | -| **RESTIC** | — | RESTIC_BACKUP_KEY: `bw get item "RESTIC" \| jq -r '.fields[] \| select(.name=="RESTIC_BACKUP_KEY") \| .value'`; RESTIC_REPOSITORY, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_DEFAULT_REGION, TELEGRAM_SELF_CHAT_ID — то же с нужным `.name` | -| **VAULTWARDEN** | — | пароль = ADMIN_TOKEN: `bw get password "VAULTWARDEN"`; SIGNUPS_ALLOWED — из полей | - -**Универсальный шаблон для поля по имени:** -`bw get item "ИМЯ_ОБЪЕКТА" | jq -r '.fields[] | select(.name=="ИМЯ_ПОЛЯ") | .value'` - ---- - -#### Переключение скриптов на секреты из Vaultwarden - -Ниже — как перейти с чтения из файлов на подстановку из `bw` на **хосте Proxmox**. Мастер-пароль хранить **только в файле с доступом для текущего пользователя:** `chmod 600 /root/.bw-master` (владелец root — только root читает/пишет); в репозиторий файл не коммитить. Либо задавать переменную окружения при запуске по крону. - -**1. Restic (backup-restic-yandex.sh, backup-restic-yandex-photos.sh, restore-one-vzdump-from-restic.sh)** - -Сейчас: `source /root/.restic-yandex.env`, пароль из `/root/.restic-password`. - -Переключение: в начале скрипта (после `set -e`) разблокировать BW и выставить переменные из объекта **RESTIC**: - -```bash -# Разблокировать Vaultwarden (мастер-пароль из файла с chmod 600 или env) -BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}" -if [ -f "$BW_MASTER_PASSWORD_FILE" ]; then - export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw) -fi -# Подставить секреты из RESTIC -ITEM=$(bw get item "RESTIC") -export RESTIC_REPOSITORY=$(echo "$ITEM" | jq -r '.fields[] | select(.name=="RESTIC_REPOSITORY") | .value') -export AWS_ACCESS_KEY_ID=$(echo "$ITEM" | jq -r '.fields[] | select(.name=="AWS_ACCESS_KEY_ID") | .value') -export AWS_SECRET_ACCESS_KEY=$(echo "$ITEM" | jq -r '.fields[] | select(.name=="AWS_SECRET_ACCESS_KEY") | .value') -export AWS_DEFAULT_REGION=$(echo "$ITEM" | jq -r '.fields[] | select(.name=="AWS_DEFAULT_REGION") | .value') -RESTIC_PASSWORD=$(echo "$ITEM" | jq -r '.fields[] | select(.name=="RESTIC_BACKUP_KEY") | .value') -export RESTIC_PASSWORD_FILE=$(mktemp -u) -echo -n "$RESTIC_PASSWORD" > "$RESTIC_PASSWORD_FILE" -chmod 600 "$RESTIC_PASSWORD_FILE" -trap 'rm -f "$RESTIC_PASSWORD_FILE"' EXIT -``` - -Убрать из скрипта: проверку/чтение `ENV_FILE` и `RESTIC_PASSWORD_FILE` из файлов; оставить использование переменных `RESTIC_REPOSITORY`, `AWS_*`, `RESTIC_PASSWORD_FILE` как выше. Для restore-one-vzdump-from-restic.sh — тот же блок в начале. - -**2. Telegram (notify-telegram.sh)** - -Сейчас: `source /root/.telegram-notify.env`, переменные `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID`. - -Переключение: читать токен из **HOME_BOT_TOKEN** (пароль), chat_id из объекта **RESTIC** (поле TELEGRAM_SELF_CHAT_ID). В начале скрипта (если нет уже разблокировки): - -```bash -BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}" -if [ -f "$BW_MASTER_PASSWORD_FILE" ]; then - export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw) -fi -TELEGRAM_BOT_TOKEN=$(bw get password "HOME_BOT_TOKEN") -TELEGRAM_CHAT_ID=$(bw get item "RESTIC" | jq -r '.fields[] | select(.name=="TELEGRAM_SELF_CHAT_ID") | .value') -``` - -Дальше в скрипте использовать `TELEGRAM_BOT_TOKEN` и `TELEGRAM_CHAT_ID` как раньше (проверка на пустоту, вызов curl). Файл `/root/.telegram-notify.env` можно не использовать. - -**3. Дампы БД (backup-ct101-pgdump.sh, backup-ct104-pgdump.sh, backup-ct103-gitea-pgdump.sh)** - -Скрипты уже берут PGPASSWORD из Vaultwarden: в начале разблокировка `bw unlock --passwordfile "$BW_MASTER_PASSWORD_FILE" --raw`, затем для pg_dump передаётся `-e PGPASSWORD=...` в `docker exec`. Источники паролей: - -- **Nextcloud (CT 101):** объект **NEXTCLOUD** — поле `dbpassword` или пароль записи (`bw get password "NEXTCLOUD"`). -- **Paperless (CT 104):** объект **PAPERLESS** — пароль (`bw get password "PAPERLESS"`). -- **Gitea (CT 103):** объект **GITEA** — пароль (`bw get password "GITEA"`). - -Требования на хосте: `bw`, для Nextcloud — `jq`; файл `/root/.bw-master` с мастер-паролем (chmod 600). При ошибке (дамп < 512 байт) скрипт завершается с кодом 1 и выводит stderr; уведомление в Telegram при ошибке не отправляется. - -**4. Остальные места** - -Конфиги сервисов (Nextcloud config.php, Gitea compose, Paperless .env, Immich .env и т.д.) подставлять вручную при восстановлении или написать небольшие скрипты-обёртки, которые один раз получают нужные значения через `bw get item` / `bw get password` и пишут в .env или конфиг. Имена объектов и полей — по таблице выше. - -После переключения: обновить чек-лист (отметить «Секреты перенесены в Vaultwarden») и при необходимости добавить в cron установку `BW_MASTER_PASSWORD_FILE` или вызов разблокировки в общем wrapper’е. - ---- - -### Шаг 6. Тестовое восстановление одного контейнера - -Без проверки восстановления нельзя считать стратегию рабочей. - -1. Выбрать **некритичный** контейнер (например 105 — RAG, или 107 — Invidious), для которого краткий даунтайм допустим. -2. Убедиться, что есть свежий backup этого контейнера в Storage (после шага 3). -3. **Восстановление:** - - В Proxmox UI: **Datacenter → Backup** → выбрать storage → найти backup нужного CT → **Restore**. Указать **new VMID** (например 999 для теста) и **Storage** для дисков. - - Или с CLI: - `qmrestore` для VM, для LXC — через GUI или `pct restore` (см. справку `pct restore`). - Для LXC типично: Backup → Restore → задать новый ID (например 999), node, storage. -4. Запустить восстановленный контейнер (ID 999), проверить: - - заходит по SSH/консоль; - - сервисы внутри запускаются (docker/ systemd); - - с хоста пинг и при необходимости один сервис по порту. -5. После проверки удалить тестовый контейнер (999), освободить место. - -Если что-то пошло не так (не находится диск, ошибка прав, сеть) — зафиксировать и поправить стратегию (пути storage, режим backup, права). - ---- - -### Шаг 7. Документировать процедуру восстановления «с нуля» - -Кратко зафиксировать в отдельном разделе (здесь или в architecture): - -1. Установка Proxmox на новое железо (или новый диск). -2. Восстановление конфигов: распаковать последний `etc-pve-*.tar.gz` в `/etc/pve` (с учётом того, что нужно корректно подставить ноды и storage; при одномузловой установке обычно достаточно скопировать файлы). -3. Подключение storage с backup (или копирование последних vzdump на новый storage). -4. Восстановление контейнеров и ВМ из backup по одному (Restore с указанием VMID и storage). -5. Запуск контейнеров/ВМ, проверка сети и сервисов. -6. Использование сохранённых паролей/ключей для входа и проверки сервисов. - -После первого успешного тестового восстановления (шаг 6) эту процедуру можно уточнить и дописать по факту. - ---- - -## Чек-лист фазы 1 - -- [x] Разметка: 1 ТБ на sdb1, ФС, монтирование в `/mnt/backup` (без LUKS). *(скрипт `scripts/backup-setup-sdb1-mount.sh`, каталоги созданы.)* -- [x] В Proxmox добавлен Storage для VZDump → `/mnt/backup/proxmox/dump`. -- [x] Настроена регулярная задача Backup: LXC (100–108), расписание ночь (02:00), retention задан. *VM 200 исключена из задания (образ ~380 ГБ); восстановление VM 200 — по инструкции «с нуля» в [backup-howto](backup-howto.md).* -- [x] Проверен ручной запуск Backup now — файлы появляются в storage. *(рекомендуется проверить разово.)* -- [x] Настроен бэкап `/etc/pve` (скрипт + cron) → `/mnt/backup/proxmox/etc-pve`. *(backup-etc-pve.sh, 02:15, 30 дней.)* -- [x] Restic: cron на хосте, выгрузка каталогов из `/mnt/backup` в Yandex S3. *(backup-restic-yandex.sh 04:00, backup-restic-yandex-photos.sh 04:10, retention 3 daily / 2 weekly / 2 monthly.)* -- [x] Yandex: ключи и endpoint зафиксированы в `/root/.restic-yandex.env`, restic пишет в бакет. -- [x] Vaultwarden развёрнут (CT 103). -- [ ] Секреты перенесены в Vaultwarden. *(на усмотрение: root PVE, пароли БД, API и т.д.)* -- [x] Бэкап данных Vaultwarden включён в restic (Yandex S3). *Локально: backup-vaultwarden-data.sh → `/mnt/backup/other/vaultwarden/`; restic выгружает весь `/mnt/backup` (кроме photos), каталог vaultwarden входит в снимок.* -- [x] Выполнено тестовое восстановление одного контейнера (другой VMID), проверена работоспособность. *(26.02.2026: восстановлен CT 107 в слот 999 из `/mnt/backup/proxmox/dump/dump/vzdump-lxc-107-*.tar.zst`, проверены консоль, пинг, Docker, Invidious на 3000; тестовый CT удалён.)* -- [x] В документации зафиксирована процедура полного восстановления Proxmox «с нуля». *[backup-howto.md](backup-howto.md): восстановление из vzdump, конфигов, БД, VM 200 с нуля, Vaultwarden, VPS и др.* - ---- - -## Ссылки - -- [Архитектура и подключение](../architecture/architecture.md) — хосты, IP, домены. -- [Схема сети и зависимости](../network/network-topology.md) — SPOF, зависимость от Proxmox и бэкапов. -- [Vaultwarden и использование секретов](../vaultwarden-secrets.md) — установка bw, разблокировка, получение секретов в скриптах. -- Документация контейнеров (100–108, 200) — бэкапы *данных внутри* сервисов (БД, тома); фаза 1 дополняет это бэкапом на уровне PVE. - ---- - -## Принятые решения (сводка) - -| Вопрос | Решение | -|--------|---------| -| Точка монтирования, второй ТБ | `/mnt/backup`; второй ТБ на sdb1 — в запас, назначение позже. | -| Шифрование (LUKS) | Пока не делаем; раздел без шифрования. | -| Proxmox vzdump | Локально в `proxmox/dump` + дублировать в Yandex через restic. | -| Фотки | Оригиналы + метаданные + БД Immich; остальное пересчитать. | -| VPS | Amnezia — конфиг. Миран — БД бота + контент (контент можно в S3 Мирана; копия на sdb1 — по желанию). Конфиг серверов не бэкапим — Ansible. | -| Где запускать бэкапы | Cron на хосте Proxmox: Backup Job (vzdump) + restic в Yandex. | -| Retention локально | Только в Proxmox Job и в скрипте etc-pve; отдельного restic-репозитория локально не делаем. | -| /etc/pve + конфиги хоста (interfaces, hosts, resolv.conf) | Вариант A: cron на хосте → `etc-pve` и `etc-host-configs` в `/mnt/backup/proxmox/etc-pve`; локально и в Yandex (restic). | -| Пароли | Vaultwarden на CT 103. | -| VM 200 (БД PostgreSQL) | vzdump snapshot — для образа ВМ; консистентность БД — отдельно: внутри VM логический бэкап (`pg_dump`). | -| Yandex | Бакет создан; ключи и endpoint зафиксировать при настройке restic. | -| MinIO | Не используем; директории + restic (s3 для Yandex). | - ---- - -## Осталось сделать - -- **Проверка ручного Backup:** один раз запустить «Backup now» в Proxmox UI (Datacenter → Backup) и убедиться, что файлы появляются в `/mnt/backup/proxmox/dump/dump/`. -- **Секреты (по желанию):** перенести пароли/ключи (root PVE, БД, API) в Vaultwarden и обновлять при смене. - -*Выполнено ранее: Yandex + Restic (cron, retention 3/2/2), тестовое восстановление CT 107 → 999 (26.02.2026).* diff --git a/docs/containers/container-100.md b/docs/containers/container-100.md index b5aa517..afc2cb9 100644 --- a/docs/containers/container-100.md +++ b/docs/containers/container-100.md @@ -18,9 +18,9 @@ ## Доступ и логины -- **Debian (CT 100):** логин `root` (или консольный пользователь Debian), пароль `waccEk-fyqbux-rarja3`. -- **AdGuard Home (через домен):** https://adguard.katykhin.ru (через NPM, сертификат Let's Encrypt), пользователь `kerrad`, пароль `waccEk-fyqbux-rarja3`. Прямой доступ по порту 3000 больше не используется. -- **Nginx Proxy Manager:** http://192.168.1.100:81, имя `Kerrad`, email `j3tears100@gmail.com`, пароль `kqEUubVq02DJTS8`. +- **Debian (CT 100):** логин `root`. Пароль — в Vaultwarden (объект **CT_100_ROOT_PASSWORD**). +- **AdGuard Home (через домен):** https://adguard.katykhin.ru (через NPM, сертификат Let's Encrypt). Пользователь и пароль — в Vaultwarden (объект **ADGUARD**). Прямой доступ по порту 3000 больше не используется. +- **Nginx Proxy Manager:** http://192.168.1.100:81. Имя, email и пароль — в Vaultwarden (объект **NPM_ADMIN**). --- @@ -56,7 +56,7 @@ **Certbot на хосте (внутри CT 100):** - Установлен в системе, таймер `certbot.timer` (проверка продления дважды в день). -- Учётные данные Beget API: `/root/.secrets/certbot/beget.ini`. +- Учётные данные Beget API: `/root/.secrets/certbot/beget.ini` (генерируется из Vaultwarden скриптом `deploy-beget-credentials.sh` с хоста Proxmox). - Deploy-hook’и: `/etc/letsencrypt/renewal-hooks/deploy/` — скрипты `copy-*-to-npm.sh` (video, docs, immich, mini-lm, vault и т.д.) копируют `fullchain.pem` и `privkey.pem` в соответствующий каталог `custom_ssl/npm-/` и делают `docker exec npm nginx -s reload`. **vault.katykhin.ru:** сертификат выпускается certbot’ом в `/etc/letsencrypt/live/vault.katykhin.ru/`, deploy-hook `copy-vault-to-npm.sh` копирует его в `custom_ssl/npm-18/`. В NPM у proxy host’а vault.katykhin.ru должен быть выбран именно этот сертификат (Custom SSL → каталог npm-18). Если в NPM по ошибке привязать другой сертификат (например от другого домена), браузер покажет ошибку «нет сертификата» или неверный домен; тогда в конфиге proxy host’а должны быть пути `ssl_certificate /data/custom_ssl/npm-18/...`. @@ -181,14 +181,23 @@ docker restart wallos Проверяет, идут ли запросы к заданным доменам через VPN или через основное подключение (подключение к роутеру по telnet, разбор маршрутов). Результаты отдаёт на порту **8765** (на хосте). В Homepage добавлена ссылка на http://192.168.1.100:8765. -**Переменные окружения в compose:** `ROUTER_TELNET_HOST`, `ROUTER_TELNET_USER`, `ROUTER_TELNET_PASSWORD` — **заданы в самом файле** (не в .env). Рекомендация: вынести в `.env` и не коммитить пароль (см. TODO). +**Секреты:** `ROUTER_TELNET_HOST`, `ROUTER_TELNET_USER`, `ROUTER_TELNET_PASSWORD` берутся из Vaultwarden (объект **localhost**). Деплой — единым скриптом на Proxmox: + +```bash +/root/scripts/deploy-vpn-route-check.sh +``` + +Скрипт: разблокирует bw, получает креды из Vaultwarden, атомарно пишет `.env` в CT 100, запускает `docker compose up -d`. Режим проверки без записи: `--dry-run`. Шаблон compose: `scripts/vpn-route-check/docker-compose.yml`. **Том:** volume `vpn-route-check-data` → `/data` (в контейнере). **Команды:** ```bash -cd /opt/docker/vpn-route-check && docker compose up -d -docker logs vpn-route-check +# Деплой (с хоста Proxmox) +/root/scripts/deploy-vpn-route-check.sh + +# Логи +pct exec 100 -- docker logs vpn-route-check ``` --- @@ -224,7 +233,7 @@ docker logs vpn-route-check 1. Создать сеть (если ещё нет): `docker network create proxy_network`. 2. NPM: `cd /opt/docker/nginx-proxy && docker compose up -d`. 3. AdGuard: `cd /opt/docker/adguard && docker compose up -d` (создаёт свою сеть и подключается к proxy_network). -4. VPN Route Check: `cd /opt/docker/vpn-route-check && docker compose up -d`. +4. VPN Route Check: `/root/scripts/deploy-vpn-route-check.sh` (с хоста Proxmox). 5. Log-dashboard: при необходимости запустить контейнер с монтом html и портом 8088. После изменений в NPM (proxy, SSL): перезагрузка nginx внутри контейнера — `docker exec npm nginx -s reload`. Certbot продлевает сертификаты по таймеру; deploy-hook’и копируют их в NPM и перезагружают nginx. @@ -234,7 +243,7 @@ docker logs vpn-route-check ## Уязвимости и риски 1. **Пароли и креды в конфигах:** В `services.yaml` (Homepage) хранятся пароли виджетов (AdGuard, NPM, Proxmox). Файл лежит только на сервере; не помещать в публичный репозиторий. -2. **VPN Route Check:** Логин и пароль роутера прописаны в `docker-compose.yml`. Доступ к compose = доступ к роутеру. Рекомендуется вынести в `.env` и ограничить права на файл. +2. **VPN Route Check:** Креды роутера в `.env` (генерируется из Vaultwarden скриптом `deploy-vpn-route-check.sh`). Файл `.env` не коммитить. 3. **AdGuard на 3000:** Веб-интерфейс доступен по порту 3000 на хосте. Доступ из LAN; при необходимости закрыть фаерволом снаружи или использовать только через NPM (proxy). 4. **NPM на 81:** Админка NPM по порту 81. Убедиться, что с интернета доступ только через VPN или не пробрасывать 81 наружу. 5. **Логи NPM:** Часть логов (fallback_*) не ротируется — возможен рост и заполнение диска (см. TODO). @@ -245,7 +254,7 @@ docker logs vpn-route-check - [x] **Логи NPM:** Добавить в logrotate ротацию для `fallback_http_access.log`, `fallback_http_error.log` (и при необходимости других fallback_*) по размеру или по дням — настроено в `npm-nginx.conf` (30 дней / ~512 MB). - [x] **Логи AdGuard:** Ограничить хранение логов запросов/статистики — настроено в `AdGuardHome.yaml` (`querylog.interval = 336h`, `statistics.interval = 336h` ≈ 14 дней). -- [ ] **VPN Route Check:** Вынести `ROUTER_TELNET_*` в `.env`, подключать в compose через `env_file`, не коммитить .env в репозиторий. +- [x] **VPN Route Check:** Секреты из Vaultwarden (объект localhost), деплой через `deploy-vpn-route-check.sh`. - [ ] **Log-dashboard:** Зафиксировать способ запуска контейнера (отдельный compose или скрипт) и добавить его в документацию/автозапуск при перезагрузке CT. - [ ] **Мониторинг диска:** Настроить оповещение (например, из Prometheus/Alertmanager или скрипт по крону) при заполнении корня или `/opt/docker` выше порога (например 80%). - [ ] **Резервное копирование:** Регулярный бэкап критичных папок (оценка размеров на момент документации): diff --git a/docs/containers/container-107.md b/docs/containers/container-107.md index e92e455..cde9995 100644 --- a/docs/containers/container-107.md +++ b/docs/containers/container-107.md @@ -43,20 +43,22 @@ **Порты:** 3000 (хост) → 3000 (контейнер). NPM (контейнер 100) проксирует https://video.katykhin.ru → 192.168.1.107:3000. **Тома и конфиги:** -- Invidious не использует отдельные bind‑тома для конфигов/данных — данные хранятся в PostgreSQL (`invidious_postgresdata`), а конфиг задаётся через переменную `INVIDIOUS_CONFIG` в compose (inline YAML). +- Invidious не использует отдельные bind‑тома для конфигов/данных — данные хранятся в PostgreSQL (`invidious_postgresdata`), а конфиг задаётся через переменную `INVIDIOUS_CONFIG` в compose. - Отдельных каталогов с логами Invidious на хосте нет — логи идут в stdout контейнера (см. раздел «Логи и ротация»). -**Основная конфигурация (в docker-compose.yml, секция `environment / INVIDIOUS_CONFIG`):** -- `db`: dbname=invidious, user=kemal, password=kemal, host=invidious-db, port=5432, check_tables=true. -- `invidious_companion`: URL сервиса companion (`http://companion:8282/companion`). -- `invidious_companion_key` и `SERVER_SECRET_KEY` (в companion) — общий секрет между Invidious и Companion (сейчас заданы прямо в compose; **не выкладывать в публичный репозиторий**). -- `external_port: 443`, `domain: "video.katykhin.ru"`, `https_only: true` — Invidious знает про внешний домен и порт, отдаёт ссылки на https. -- Прочие опции (feeds, captions, hmac_key, default_user_preferences и т.д.). +**Секреты:** `POSTGRES_USER`, `POSTGRES_PASSWORD`, `INVIDIOUS_COMPANION_KEY`, `HMAC_KEY` берутся из Vaultwarden (объект **INVIDIOUS**). Деплой с хоста Proxmox: +```bash +/root/scripts/deploy-invidious-credentials.sh +``` +Скрипт генерирует `.env` из Vaultwarden, атомарно пушит в CT 107, запускает `docker compose up -d --force-recreate`. **Ротация:** сменил пароль/ключи в Vaultwarden → запустил скрипт. **Команды:** ```bash -cd /opt/invidious && docker compose up -d -docker logs invidious-invidious-1 +# Деплой (с хоста Proxmox) +/root/scripts/deploy-invidious-credentials.sh + +# Логи +pct exec 107 -- docker logs invidious-invidious-1 curl -s http://127.0.0.1:3000/api/v1/stats ``` @@ -71,7 +73,7 @@ curl -s http://127.0.0.1:3000/api/v1/stats - volume `companioncache` → `/var/tmp/youtubei.js` (кэш js‑ресурсов YouTube / youtubei). **Безопасность:** -- `SERVER_SECRET_KEY` совпадает с `invidious_companion_key` в конфиге Invidious — это shared secret для обмена. +- `SERVER_SECRET_KEY` совпадает с `invidious_companion_key` — оба берутся из `.env` (генерируется из Vaultwarden). - Контейнер запущен с `read_only: true`, `cap_drop: [ALL]`, `no-new-privileges:true` — хорошая практика sandboxing. **Команды:** @@ -89,7 +91,7 @@ docker logs invidious-companion-1 - `/opt/invidious/config/sql` → `/config/sql` — SQL‑скрипты инициализации/миграций из репозитория Invidious (~40 KB). - `/opt/invidious/docker/init-invidious-db.sh` → `/docker-entrypoint-initdb.d/init-invidious-db.sh` — скрипт инициализации БД при первом запуске. -**Переменные окружения:** POSTGRES_DB=invidious, POSTGRES_USER=kemal, POSTGRES_PASSWORD=kemal (заданы в compose; не публиковать). +**Переменные окружения:** из `.env` (генерируется `deploy-invidious-credentials.sh` из Vaultwarden). **Команды:** ```bash @@ -124,16 +126,10 @@ Companion и PostgreSQL доступны только внутри docker-сет ## Запуск и порядок поднятия -1. Зайти в каталог: `cd /opt/invidious`. -2. Проверить/при необходимости подредактировать `docker-compose.yml` (секция `INVIDIOUS_CONFIG`, домен video.katykhin.ru, секреты). -3. Запуск/перезапуск: - ```bash - docker compose up -d - ``` - Порядок: сначала поднимается `invidious-db`, затем `invidious` (depends_on с healthcheck), параллельно Companion. +1. С хоста Proxmox: `/root/scripts/deploy-invidious-credentials.sh` (генерирует `.env` из Vaultwarden, пушит в CT 107, запускает compose). +2. Порядок: `invidious-db` → `invidious` (depends_on с healthcheck), параллельно Companion. -После изменения конфигурации (секция `INVIDIOUS_CONFIG` или окружения Companion/DB): -`cd /opt/invidious && docker compose up -d` — конфигурация применяется при перезапуске контейнеров. +После изменения секретов в Vaultwarden: запустить `deploy-invidious-credentials.sh` снова. --- diff --git a/docs/containers/container-108.md b/docs/containers/container-108.md index c6f11d9..aec8bb0 100644 --- a/docs/containers/container-108.md +++ b/docs/containers/container-108.md @@ -18,7 +18,7 @@ ## Доступ и логины -- **Debian (CT 108):** логин `root`, пароль `Galene108!`. +- **Debian (CT 108):** логин `root`. Пароль — в Vaultwarden (объект **CT_108_ROOT_PASSWORD**). - **Galene (веб):** https://call.katykhin.ru (через NPM → 192.168.1.108:8443). Вход в группы — по паролям, заданным в конфигах групп в `/opt/galene-data/groups/` (операторы и участники). --- diff --git a/docs/network/ssl-letsencrypt-dns01.md b/docs/network/ssl-letsencrypt-dns01.md index daa9e3e..1c5e649 100644 --- a/docs/network/ssl-letsencrypt-dns01.md +++ b/docs/network/ssl-letsencrypt-dns01.md @@ -41,6 +41,12 @@ chmod 600 /root/.secrets/certbot/beget.ini ``` + **Homelab (Vaultwarden):** креды хранятся в Vaultwarden (объект **beget**). Деплой с хоста Proxmox: + ```bash + /root/scripts/deploy-beget-credentials.sh + ``` + Скрипт генерирует `beget.ini` из Vaultwarden, атомарно пушит в CT 100, ставит права 600 и pre-hook проверки. **Ротация:** сменил пароль в Vaultwarden → запустил `deploy-beget-credentials.sh` → готово. + 3. **Запрос сертификата:** ```bash certbot certonly \ diff --git a/docs/vaultwarden-secrets.md b/docs/vaultwarden-secrets.md index 5503034..fb5cc49 100644 --- a/docs/vaultwarden-secrets.md +++ b/docs/vaultwarden-secrets.md @@ -191,6 +191,53 @@ TELEGRAM_CHAT_ID=$(bw get item "RESTIC" | jq -r '.fields[] | select(.name=="TELE # Дальше: curl к Telegram API с TELEGRAM_BOT_TOKEN и TELEGRAM_CHAT_ID ``` +### Пример: Beget (certbot DNS-01, CT 100) + +Скрипт `deploy-beget-credentials.sh` на Proxmox генерирует `beget.ini` из объекта **beget** (username → `dns_beget_api_username`, password → `dns_beget_api_password`), атомарно пушит в CT 100 (`beget.ini.tmp` → `mv` → `beget.ini`), ставит chmod 600. Pre-hook certbot проверяет наличие файла и права перед каждым renew. **Ротация:** сменил пароль в Vaultwarden → `deploy-beget-credentials.sh` → готово. + +### Пример: Invidious (CT 107) + +Скрипт `deploy-invidious-credentials.sh` генерирует `.env` из объекта **INVIDIOUS** (username, password, поля `SERVER_SECRET_KEY`, `HMAC_KEY`), атомарно пушит в CT 107, запускает `docker compose up -d --force-recreate`. **Ротация:** сменил пароль/ключи в Vaultwarden → запустил скрипт. + +### Пример: Paperless (CT 104) + +Скрипт `deploy-paperless-credentials.sh` генерирует `docker-compose.env` из объекта **PAPERLESS** (password = POSTGRES_PASSWORD; поля `PAPERLESS_URL`, `PAPERLESS_SECRET_KEY`, `PAPERLESS_TIME_ZONE`, `PAPERLESS_OCR_LANGUAGE`, `PAPERLESS_OCR_LANGUAGES`), пушит compose и env в CT 104, запускает `docker compose up -d --force-recreate`. **Ротация:** сменил пароль/ключи в Vaultwarden → запустил скрипт. + +### Пример: RAG-service (CT 105) + +Скрипт `deploy-rag-credentials.sh` генерирует `.env` из объекта **RAG_SERVICE** (поле `RAG_API_KEY`), атомарно пушит в CT 105, запускает `docker compose up -d --force-recreate`. **Перед первым запуском:** создать в Vaultwarden запись **RAG_SERVICE** (тип Login), добавить кастомное поле `RAG_API_KEY` (hidden) с текущим ключом из `/home/rag-service/.env`. **Ротация:** сменил ключ в Vaultwarden → запустил скрипт. + +### Пример: Gitea (CT 103) + +Скрипт `deploy-gitea-credentials.sh` генерирует `.env` из объекта **GITEA** (password = POSTGRES_PASSWORD; поле `GITEA_RUNNER_REGISTRATION_TOKEN`), пушит compose и env в CT 103, запускает `docker compose up -d --force-recreate`. **Ротация:** сменил пароль/токен в Vaultwarden → запустил скрипт. + +### Пример: Nextcloud (CT 101) + +Скрипт `deploy-nextcloud-credentials.sh` генерирует `.env` и `docker-compose.yml` из объекта **NEXTCLOUD** (password = POSTGRES_PASSWORD; поля `dbpassword`, `secret`, `passwordsalt`, `instanceid`), пушит в CT 101, обновляет config.php через occ, запускает compose. **Ротация:** сменил в Vaultwarden → запустил скрипт. + +### Пример: Galene (CT 108) + +Скрипт `deploy-galene-credentials.sh` берёт поле `config` (JSON ice-servers) из объекта **GALENE**, записывает в `/opt/galene-data/data/ice-servers.json`, перезапускает `galene.service`. **Ротация:** сменил TURN username/credential в Vaultwarden → запустил скрипт. + +### Пример: Immich (VM 200) + +Скрипт `deploy-immich-credentials.sh` генерирует `.env` для Immich и immich-deduper из объектов **IMMICH** и **IMMICH_DEDUPER**, пушит по SSH на VM 200, запускает compose. **Требования:** SSH без пароля root@Proxmox → admin@192.168.1.200. **Ротация:** сменил в Vaultwarden → запустил скрипт. + +### Пример: WireGuard (CT 109) + +Скрипт `deploy-wireguard-credentials.sh` берёт поле `wg0_conf` (полный конфиг) из объекта **LOCAL_VPN_SERVER_WG**, записывает в `/etc/wireguard/wg0.conf`, перезапускает `wg-quick@wg0`. **Перед первым запуском:** создать в Vaultwarden запись **LOCAL_VPN_SERVER_WG**, добавить кастомное поле `wg0_conf` (hidden) с содержимым текущего `/etc/wireguard/wg0.conf` (скопировать с CT 109). **Ротация:** сменил ключи в Vaultwarden → запустил скрипт. + +### Пример: VPN Route Check (деплой с Proxmox в CT 100) + +Скрипт `deploy-vpn-route-check.sh` на хосте Proxmox: + +1. Разблокирует bw (или переиспользует сессию). +2. Получает из объекта **localhost**: `ROUTER_TELNET_HOST` (кастомное поле), `ROUTER_TELNET_USER` (username), `ROUTER_TELNET_PASSWORD` (password). +3. Генерирует `.env` во временный файл, атомарно (`mv .env.tmp .env`) пушит в CT 100. +4. Запускает `docker compose up -d` в каталоге vpn-route-check. + +Режим проверки без записи: `deploy-vpn-route-check.sh --dry-run`. Подробнее: [Контейнер 100](containers/container-100.md#7-vpn-route-check). + ### Fallback на старые конфиги Если Vaultwarden недоступен или разблокировка не удалась, скрипты могут загружать креды из прежних файлов (например `/root/.telegram-notify.env`, `/root/.restic-yandex.env`). Так можно обеспечить работу бэкапов даже при временной недоступности vault. @@ -208,14 +255,33 @@ TELEGRAM_CHAT_ID=$(bw get item "RESTIC" | jq -r '.fields[] | select(.name=="TELE ## Инвентаризация записей и полей -В Vaultwarden удобно хранить записи с именами, совпадающими с сервисами: **RESTIC**, **GITEA**, **PAPERLESS**, **NEXTCLOUD**, **HOME_BOT_TOKEN**, **VAULTWARDEN**, **MIRAN_S3** и т.д. У записей типа «логин» — логин/пароль; у записей с множеством значений — кастомные поля (например `RESTIC_REPOSITORY`, `AWS_ACCESS_KEY_ID`). +В Vaultwarden удобно хранить записи с именами, совпадающими с сервисами: **RESTIC**, **GITEA**, **PAPERLESS**, **NEXTCLOUD**, **HOME_BOT_TOKEN**, **VAULTWARDEN**, **MIRAN_S3**, **RAG_SERVICE**, **ADGUARD**, **NPM_ADMIN** и т.д. У записей типа «логин» — логин/пароль; у записей с множеством значений — кастомные поля (например `RESTIC_REPOSITORY`, `AWS_ACCESS_KEY_ID`, `RAG_API_KEY`). -Полная таблица «где лежат креды сейчас → какой объект в Vaultwarden» и готовые команды `bw get ...` / `jq` по каждому объекту описаны в [Фаза 1: Стратегия бэкапов](backup/proxmox-phase1-backup.md) — разделы «Инвентаризация секретов для переноса в Vaultwarden», «Получение секретов из Vaultwarden» и «Переключение скриптов на секреты из Vaultwarden». +**ADGUARD** — веб-интерфейс AdGuard Home (https://adguard.katykhin.ru): username = логин администратора, password = пароль. Тип записи: Login. + +**NPM_ADMIN** — админка Nginx Proxy Manager (http://192.168.1.100:81): username = email (используется как identity при входе), password = пароль. Тип записи: Login. Скрипты `npm-add-proxy.sh`, `npm-add-proxy-vault.sh` используют `NPM_EMAIL` и `NPM_PASSWORD` — брать из этого объекта. + +### Команды bw по объектам (для скриптов бэкапов и деплоя) + +| Объект | Логин / пароль | Кастомные поля | +|--------|----------------|----------------| +| **ADGUARD** | `bw get username "ADGUARD"`, `bw get password "ADGUARD"` | — | +| **beget** | `bw get username "beget"`, `bw get password "beget"` | — | +| **GALENE** | — | `bw get item "GALENE" \| jq -r '.fields[] \| select(.name=="config") \| .value'` | +| **GITEA** | `bw get username "GITEA"`, `bw get password "GITEA"` | GITEA_RUNNER_REGISTRATION_TOKEN и др. | +| **HOME_BOT_TOKEN** | — | пароль = токен: `bw get password "HOME_BOT_TOKEN"` | +| **localhost** | `bw get username "localhost"`, `bw get password "localhost"` | ROUTER_TELNET_HOST | +| **NEXTCLOUD** | `bw get username "NEXTCLOUD"`, `bw get password "NEXTCLOUD"` | dbpassword, secret, passwordsalt, instanceid | +| **NPM_ADMIN** | username = email, `bw get password "NPM_ADMIN"` | — | +| **PAPERLESS** | `bw get password "PAPERLESS"` (= POSTGRES_PASSWORD) | PAPERLESS_SECRET_KEY, PAPERLESS_URL и др. | +| **RESTIC** | — | RESTIC_BACKUP_KEY, RESTIC_REPOSITORY, AWS_*, TELEGRAM_SELF_CHAT_ID | +| **VAULTWARDEN** | — | пароль = ADMIN_TOKEN: `bw get password "VAULTWARDEN"` | + +Универсальный шаблон для поля: `bw get item "ИМЯ" | jq -r '.fields[] | select(.name=="ПОЛЕ") | .value'` --- ## См. также - [Контейнер 103 (Gitea, Vaultwarden)](containers/container-103.md) — развёртывание Vaultwarden, порты, домен, NPM. -- [Фаза 1: Стратегия бэкапов](backup/proxmox-phase1-backup.md) — инвентаризация секретов, команды по объектам, переключение скриптов на Vaultwarden. - [backup-howto](backup/backup-howto.md) — общий план бэкапов и восстановления, в том числе данных Vaultwarden. diff --git a/docs/vps/vps-miran-bots.md b/docs/vps/vps-miran-bots.md index 91ef73a..5c1ea33 100644 --- a/docs/vps/vps-miran-bots.md +++ b/docs/vps/vps-miran-bots.md @@ -7,8 +7,8 @@ VPS в ЦОД Миран (Санкт-Петербург). Развёрнуты ## Доступ и логины - **SSH:** `ssh -p 15722 deploy@185.147.80.190` (пользователь deploy, в группе docker). IP: 185.147.80.190, хостнейм vm220416.vds.miran.ru, ОС Ubuntu. -- **S3 (контент ботов):** URL https://api.s3.miran.ru, порт 443. Access key: `j3tears100@gmail.com`, Secret key: `wQ1-6sZEPs92sbZTSf96` (полная таблица — в разделе «S3» ниже). -- **Админка Миран (панель хостинга VPS):** логин `j3tears100@gmail.com`, пароль `gonPok-xifrys-4nuxde`. +- **S3 (контент ботов):** URL https://api.s3.miran.ru, порт 443. Access key и Secret key — в Vaultwarden (объект **MIRAN_S3**). +- **Админка Миран (панель хостинга VPS):** логин и пароль — в Vaultwarden (отдельная запись для панели Миран). - **Grafana, Uptime Kuma, админки ботов:** логины и пароли — в `.env` проекта prod или в менеджере паролей. --- @@ -50,8 +50,8 @@ VPS в ЦОД Миран (Санкт-Петербург). Развёрнуты |-------------|----------| | URL | https://api.s3.miran.ru | | Порт | 443 (HTTPS) | -| Access key | j3tears100@gmail.com | -| Secret key | wQ1-6sZEPs92sbZTSf96 | +| Access key | см. Vaultwarden, объект **MIRAN_S3** | +| Secret key | см. Vaultwarden, объект **MIRAN_S3** | В ботаx (переменные окружения prod) заданы `S3_ENDPOINT_URL=https://api.s3.miran.ru`, регион и креды для загрузки/выдачи контента. Для локальной разработки или других клиентов использовать те же endpoint и ключи. @@ -119,12 +119,6 @@ docker compose logs -f telegram-bot **Что нужно на Proxmox:** - **SSH:** с хоста (root) должен работать вход без пароля на `deploy@185.147.80.190 -p 15722` (добавить публичный ключ хоста в `~/.ssh/authorized_keys` пользователя deploy на VPS). -- **S3:** установить `awscli` (`apt install awscli`) и создать файл `/root/.vps-miran-s3.env` с содержимым (подставить свои креды): - ```bash - S3_ACCESS_KEY=j3tears100@gmail.com - S3_SECRET_KEY=... - S3_BUCKET_NAME=9829-telegram-helper-bot - ``` - Файл читается только root; в репозиторий не коммитить. +- **S3:** установить `awscli` (`apt install awscli`). Креды S3 — в Vaultwarden (объект **MIRAN_S3**). Файл `/root/.vps-miran-s3.env` с `S3_ACCESS_KEY`, `S3_SECRET_KEY`, `S3_BUCKET_NAME` генерируется скриптами или создаётся вручную из Vaultwarden. Файл читается только root; в репозиторий не коммитить. Подробности и восстановление — в [Бэкапы: как устроены и как восстанавливать](../backup/backup-howto.md). diff --git a/scripts/backup-ct101-pgdump.sh b/scripts/backup-ct101-pgdump.sh index 79593d5..9ee6618 100644 --- a/scripts/backup-ct101-pgdump.sh +++ b/scripts/backup-ct101-pgdump.sh @@ -27,7 +27,7 @@ OUTPUT="$BACKUP_DIR/nextcloud-db-$DATE.sql.gz" ERR=$(mktemp) trap "rm -f '$ERR'" EXIT -# PGPASSWORD из Vaultwarden (объект NEXTCLOUD: поле dbpassword или пароль). См. docs/backup/proxmox-phase1-backup.md +# PGPASSWORD из Vaultwarden (объект NEXTCLOUD: поле dbpassword или пароль). См. docs/vaultwarden-secrets.md PG_ENV_ARGS="" BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}" if [ -f "$BW_MASTER_PASSWORD_FILE" ] && command -v bw >/dev/null 2>&1 && command -v jq >/dev/null 2>&1; then diff --git a/scripts/backup-ct103-gitea-pgdump.sh b/scripts/backup-ct103-gitea-pgdump.sh index fa81128..8e8fba5 100644 --- a/scripts/backup-ct103-gitea-pgdump.sh +++ b/scripts/backup-ct103-gitea-pgdump.sh @@ -24,7 +24,7 @@ OUTPUT="$BACKUP_DIR/gitea-db-$DATE.sql.gz" ERR=$(mktemp) trap "rm -f '$ERR'" EXIT -# PGPASSWORD из Vaultwarden (объект GITEA, пароль). См. docs/backup/proxmox-phase1-backup.md +# PGPASSWORD из Vaultwarden (объект GITEA, пароль). См. docs/vaultwarden-secrets.md PG_ENV_ARGS="" BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}" if [ -f "$BW_MASTER_PASSWORD_FILE" ] && command -v bw >/dev/null 2>&1; then diff --git a/scripts/backup-ct104-pgdump.sh b/scripts/backup-ct104-pgdump.sh index 9bdea1a..b1ec104 100644 --- a/scripts/backup-ct104-pgdump.sh +++ b/scripts/backup-ct104-pgdump.sh @@ -24,7 +24,7 @@ OUTPUT="$BACKUP_DIR/paperless-db-$DATE.sql.gz" ERR=$(mktemp) trap "rm -f '$ERR'" EXIT -# PGPASSWORD из Vaultwarden (объект PAPERLESS, пароль). См. docs/backup/proxmox-phase1-backup.md +# PGPASSWORD из Vaultwarden (объект PAPERLESS, пароль). См. docs/vaultwarden-secrets.md PG_ENV_ARGS="" BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}" if [ -f "$BW_MASTER_PASSWORD_FILE" ] && command -v bw >/dev/null 2>&1; then diff --git a/scripts/backup-restic-yandex-photos.sh b/scripts/backup-restic-yandex-photos.sh index ae870ae..1c1d27c 100644 --- a/scripts/backup-restic-yandex-photos.sh +++ b/scripts/backup-restic-yandex-photos.sh @@ -18,7 +18,7 @@ fi # Секреты из Vaultwarden (объект RESTIC) BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}" if [ ! -f "$BW_MASTER_PASSWORD_FILE" ]; then - echo "Нет файла с мастер-паролем Vaultwarden: $BW_MASTER_PASSWORD_FILE (chmod 600). См. docs/backup/proxmox-phase1-backup.md" + echo "Нет файла с мастер-паролем Vaultwarden: $BW_MASTER_PASSWORD_FILE (chmod 600). См. docs/vaultwarden-secrets.md" exit 1 fi if ! command -v bw >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then diff --git a/scripts/backup-restic-yandex.sh b/scripts/backup-restic-yandex.sh index 7ed18e3..99a3ec4 100644 --- a/scripts/backup-restic-yandex.sh +++ b/scripts/backup-restic-yandex.sh @@ -22,7 +22,7 @@ fi # Секреты из Vaultwarden (объект RESTIC) BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}" if [ ! -f "$BW_MASTER_PASSWORD_FILE" ]; then - echo "Нет файла с мастер-паролем Vaultwarden: $BW_MASTER_PASSWORD_FILE (chmod 600). См. docs/backup/proxmox-phase1-backup.md" + echo "Нет файла с мастер-паролем Vaultwarden: $BW_MASTER_PASSWORD_FILE (chmod 600). См. docs/vaultwarden-secrets.md" exit 1 fi if ! command -v bw >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then diff --git a/scripts/certbot-hooks/check-beget-credentials.sh b/scripts/certbot-hooks/check-beget-credentials.sh new file mode 100644 index 0000000..3aef7b8 --- /dev/null +++ b/scripts/certbot-hooks/check-beget-credentials.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# Pre-hook для certbot: проверка beget.ini перед renew +# Путь: /etc/letsencrypt/renewal-hooks/pre/check-beget-credentials.sh +# При отсутствии файла или неверных правах — exit 1, certbot не выполнит renew. + +BEGET_INI="/root/.secrets/certbot/beget.ini" + +if [ ! -f "$BEGET_INI" ]; then + echo "check-beget-credentials: $BEGET_INI not found" >&2 + exit 1 +fi + +mode=$(stat -c '%a' "$BEGET_INI" 2>/dev/null) +owner=$(stat -c '%u' "$BEGET_INI" 2>/dev/null) + +if [ "$mode" != "600" ]; then + echo "check-beget-credentials: $BEGET_INI has mode $mode, expected 600" >&2 + exit 1 +fi + +if [ "$owner" != "0" ]; then + echo "check-beget-credentials: $BEGET_INI owner $owner, expected root (0)" >&2 + exit 1 +fi + +exit 0 diff --git a/scripts/deploy-beget-credentials.sh b/scripts/deploy-beget-credentials.sh new file mode 100644 index 0000000..71156a2 --- /dev/null +++ b/scripts/deploy-beget-credentials.sh @@ -0,0 +1,116 @@ +#!/bin/bash +# deploy-beget-credentials.sh — деплой кредов Beget для certbot DNS-01 в CT 100 +# Секреты из Vaultwarden (объект beget). Атомарная запись beget.ini. +# +# Использование: +# /root/scripts/deploy-beget-credentials.sh # деплой +# /root/scripts/deploy-beget-credentials.sh --dry-run # проверка без записи +# +# Ротация: сменил пароль в Vaultwarden → запустил скрипт → готово. +# +# Требования: bw, jq, /root/.bw-master (chmod 600) + +set -e + +CT_ID=100 +BEGET_INI_PATH="/root/.secrets/certbot/beget.ini" +BW_MASTER_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}" +DRY_RUN=false + +for arg in "$@"; do + case "$arg" in + --dry-run) DRY_RUN=true ;; + esac +done + +export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}" + +log() { echo "[$(date -Iseconds)] $*"; } +err() { echo "[$(date -Iseconds)] ERROR: $*" >&2; } + +ensure_bw_unlocked() { + local status + status=$(bw status 2>/dev/null | jq -r '.status' 2>/dev/null || echo "unknown") + if [ "$status" = "unlocked" ]; then + log "bw already unlocked, reusing session" + return 0 + fi + if [ ! -f "$BW_MASTER_FILE" ]; then + err "Missing $BW_MASTER_FILE" + exit 1 + fi + export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_FILE" --raw 2>/dev/null) || { + err "bw unlock failed" + exit 1 + } + log "bw unlocked" +} + +get_secrets() { + BEGET_USER=$(bw get username "beget" 2>/dev/null) + BEGET_PASS=$(bw get password "beget" 2>/dev/null) + if [ -z "$BEGET_USER" ] || [ -z "$BEGET_PASS" ]; then + err "beget: missing username or password in Vaultwarden" + exit 1 + fi +} + +gen_ini() { + local tmp + tmp=$(mktemp) + cat > "$tmp" << EOF +dns_beget_api_username = ${BEGET_USER} +dns_beget_api_password = ${BEGET_PASS} +EOF + echo "$tmp" +} + +push_ini_atomic() { + local tmp="$1" + local dir + dir=$(dirname "$BEGET_INI_PATH") + pct exec "$CT_ID" -- mkdir -p "$dir" + pct push "$CT_ID" "$tmp" "${BEGET_INI_PATH}.tmp" + pct exec "$CT_ID" -- bash -c "mv ${BEGET_INI_PATH}.tmp ${BEGET_INI_PATH} && chmod 600 ${BEGET_INI_PATH} && chown root:root ${BEGET_INI_PATH}" + log "beget.ini written (atomic), chmod 600, owner root" +} + +deploy_pre_hook() { + local hook_path="/etc/letsencrypt/renewal-hooks/pre/check-beget-credentials.sh" + local hook_src + hook_src="$(cd "$(dirname "$0")" && pwd)/certbot-hooks/check-beget-credentials.sh" + if [ ! -f "$hook_src" ]; then + log "pre-hook source not found ($hook_src), skip" + return 0 + fi + if pct exec "$CT_ID" -- test -f "$hook_path" 2>/dev/null; then + pct push "$CT_ID" "$hook_src" "$hook_path" + pct exec "$CT_ID" -- chmod +x "$hook_path" + log "pre-hook updated" + else + pct push "$CT_ID" "$hook_src" "$hook_path" + pct exec "$CT_ID" -- chmod +x "$hook_path" + log "pre-hook deployed" + fi +} + +main() { + log "deploy-beget-credentials start (dry_run=$DRY_RUN)" + ensure_bw_unlocked + get_secrets + + if [ "$DRY_RUN" = true ]; then + log "DRY-RUN: would push beget.ini and deploy pre-hook" + log " dns_beget_api_username=$BEGET_USER" + log " dns_beget_api_password=***" + exit 0 + fi + + tmp=$(gen_ini) + trap "rm -f $tmp" EXIT + push_ini_atomic "$tmp" + deploy_pre_hook + log "done" +} + +main diff --git a/scripts/deploy-galene-credentials.sh b/scripts/deploy-galene-credentials.sh new file mode 100644 index 0000000..c96a1e1 --- /dev/null +++ b/scripts/deploy-galene-credentials.sh @@ -0,0 +1,94 @@ +#!/bin/bash +# deploy-galene-credentials.sh — деплой TURN-кредов Galene в CT 108 +# Секреты из Vaultwarden (объект GALENE, поле config — JSON ice-servers). +# +# Использование: +# /root/scripts/deploy-galene-credentials.sh +# /root/scripts/deploy-galene-credentials.sh --dry-run +# +# Ротация: сменил TURN username/credential в Vaultwarden → запустил скрипт → systemctl restart galene +# +# Требования: bw, jq, /root/.bw-master (chmod 600) + +set -e + +CT_ID=108 +ICE_SERVERS_PATH="/opt/galene-data/data/ice-servers.json" +BW_MASTER_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}" +DRY_RUN=false + +for arg in "$@"; do + case "$arg" in + --dry-run) DRY_RUN=true ;; + esac +done + +export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}" + +log() { echo "[$(date -Iseconds)] $*"; } +err() { echo "[$(date -Iseconds)] ERROR: $*" >&2; } + +ensure_bw_unlocked() { + local status + status=$(bw status 2>/dev/null | jq -r '.status' 2>/dev/null || echo "unknown") + if [ "$status" = "unlocked" ]; then + log "bw already unlocked, reusing session" + return 0 + fi + if [ ! -f "$BW_MASTER_FILE" ]; then + err "Missing $BW_MASTER_FILE" + exit 1 + fi + export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_FILE" --raw 2>/dev/null) || { + err "bw unlock failed" + exit 1 + } + log "bw unlocked" +} + +get_secrets() { + local config + config=$(bw get item "GALENE" 2>/dev/null | jq -r '.fields[] | select(.name=="config") | .value // empty') + if [ -z "$config" ]; then + err "GALENE: missing config field (JSON ice-servers)" + exit 1 + fi + if ! echo "$config" | jq . >/dev/null 2>&1; then + err "GALENE config: invalid JSON" + exit 1 + fi + ICE_CONFIG="$config" +} + +push_ice_servers() { + local tmp + tmp=$(mktemp) + echo "$ICE_CONFIG" | jq -c . > "$tmp" + pct push "$CT_ID" "$tmp" "${ICE_SERVERS_PATH}.tmp" + rm -f "$tmp" + pct exec "$CT_ID" -- bash -c "chmod 600 ${ICE_SERVERS_PATH}.tmp && mv ${ICE_SERVERS_PATH}.tmp ${ICE_SERVERS_PATH}" + log "ice-servers.json written (atomic), chmod 600" +} + +restart_galene() { + pct exec "$CT_ID" -- systemctl restart galene + log "galene restarted" +} + +main() { + log "deploy-galene-credentials start (dry_run=$DRY_RUN)" + ensure_bw_unlocked + get_secrets + + if [ "$DRY_RUN" = true ]; then + log "DRY-RUN: would push ice-servers.json and restart galene" + log " config: $(echo "$ICE_CONFIG" | jq -c .)" + exit 0 + fi + + push_ice_servers + restart_galene + log "done" +} + +main diff --git a/scripts/deploy-gitea-credentials.sh b/scripts/deploy-gitea-credentials.sh new file mode 100644 index 0000000..10c7467 --- /dev/null +++ b/scripts/deploy-gitea-credentials.sh @@ -0,0 +1,117 @@ +#!/bin/bash +# deploy-gitea-credentials.sh — деплой кредов Gitea в CT 103 +# Секреты из Vaultwarden (объект GITEA). Атомарная запись .env. +# +# Использование: +# /root/scripts/deploy-gitea-credentials.sh +# /root/scripts/deploy-gitea-credentials.sh --dry-run +# +# Ротация: сменил пароль/токен в Vaultwarden → запустил скрипт → docker compose up -d --force-recreate +# +# Требования: bw, jq, /root/.bw-master (chmod 600) + +set -e + +CT_ID=103 +GITEA_PATH="/opt/gitea" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BW_MASTER_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}" +DRY_RUN=false + +for arg in "$@"; do + case "$arg" in + --dry-run) DRY_RUN=true ;; + esac +done + +export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}" + +log() { echo "[$(date -Iseconds)] $*"; } +err() { echo "[$(date -Iseconds)] ERROR: $*" >&2; } + +ensure_bw_unlocked() { + local status + status=$(bw status 2>/dev/null | jq -r '.status' 2>/dev/null || echo "unknown") + if [ "$status" = "unlocked" ]; then + log "bw already unlocked, reusing session" + return 0 + fi + if [ ! -f "$BW_MASTER_FILE" ]; then + err "Missing $BW_MASTER_FILE" + exit 1 + fi + export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_FILE" --raw 2>/dev/null) || { + err "bw unlock failed" + exit 1 + } + log "bw unlocked" +} + +get_secrets() { + local item + item=$(bw get item "GITEA" 2>/dev/null) + POSTGRES_PASSWORD=$(bw get password "GITEA" 2>/dev/null) + GITEA_RUNNER_REGISTRATION_TOKEN=$(echo "$item" | jq -r '.fields[] | select(.name=="GITEA_RUNNER_REGISTRATION_TOKEN") | .value // empty') + + if [ -z "$POSTGRES_PASSWORD" ]; then + err "GITEA: missing password (POSTGRES_PASSWORD)" + exit 1 + fi + if [ -z "$GITEA_RUNNER_REGISTRATION_TOKEN" ]; then + err "GITEA: missing GITEA_RUNNER_REGISTRATION_TOKEN field" + exit 1 + fi +} + +gen_env() { + local tmp + tmp=$(mktemp) + cat > "$tmp" << EOF +POSTGRES_PASSWORD=${POSTGRES_PASSWORD} +GITEA_RUNNER_REGISTRATION_TOKEN=${GITEA_RUNNER_REGISTRATION_TOKEN} +EOF + echo "$tmp" +} + +push_env_atomic() { + local tmp="$1" + < "$tmp" pct exec "$CT_ID" -- bash -c "cat > ${GITEA_PATH}/.env.tmp && chmod 600 ${GITEA_PATH}/.env.tmp && mv ${GITEA_PATH}/.env.tmp ${GITEA_PATH}/.env" + log ".env written (atomic), chmod 600" +} + +push_compose() { + local compose_src="${SCRIPT_DIR}/gitea/docker-compose.yml" + if [ -f "$compose_src" ]; then + pct push "$CT_ID" "$compose_src" "${GITEA_PATH}/docker-compose.yml" + log "docker-compose.yml pushed" + else + log "WARN: ${compose_src} not found, skipping compose push" + fi +} + +run_compose() { + pct exec "$CT_ID" -- bash -c "cd ${GITEA_PATH} && docker compose up -d --force-recreate" + log "Gitea started" +} + +main() { + log "deploy-gitea-credentials start (dry_run=$DRY_RUN)" + ensure_bw_unlocked + get_secrets + + if [ "$DRY_RUN" = true ]; then + log "DRY-RUN: would push .env and run compose" + log " POSTGRES_PASSWORD=***" + log " GITEA_RUNNER_REGISTRATION_TOKEN=***" + exit 0 + fi + + tmp=$(gen_env) + trap "rm -f $tmp" EXIT + push_env_atomic "$tmp" + push_compose + run_compose + log "done" +} + +main diff --git a/scripts/deploy-immich-credentials.sh b/scripts/deploy-immich-credentials.sh new file mode 100644 index 0000000..866aebb --- /dev/null +++ b/scripts/deploy-immich-credentials.sh @@ -0,0 +1,176 @@ +#!/bin/bash +# deploy-immich-credentials.sh — деплой кредов Immich и immich-deduper на VM 200 +# Секреты из Vaultwarden (объекты IMMICH, IMMICH_DEDUPER). +# +# Использование: +# /root/scripts/deploy-immich-credentials.sh +# /root/scripts/deploy-immich-credentials.sh --dry-run +# +# Требования: bw, jq, /root/.bw-master, SSH без пароля root@host → admin@192.168.1.200 +# +# Vaultwarden: IMMICH — поля DB_PASSWORD, IMMICH_API_KEY, GEMINI_API_KEY и др. (см. .env). +# IMMICH_DEDUPER — поля PSQL_PASS, DEDUP_*, IMMICH_PATH, PSQL_*. + +set -e + +VM_SSH="admin@192.168.1.200" +IMMICH_PATH="/opt/immich" +DEDUPER_PATH="/opt/immich-deduper" +BW_MASTER_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}" +DRY_RUN=false + +for arg in "$@"; do + case "$arg" in + --dry-run) DRY_RUN=true ;; + esac +done + +export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}" + +log() { echo "[$(date -Iseconds)] $*"; } +err() { echo "[$(date -Iseconds)] ERROR: $*" >&2; } + +ensure_bw_unlocked() { + local status + status=$(bw status 2>/dev/null | jq -r '.status' 2>/dev/null || echo "unknown") + if [ "$status" = "unlocked" ]; then + log "bw already unlocked, reusing session" + return 0 + fi + if [ ! -f "$BW_MASTER_FILE" ]; then + err "Missing $BW_MASTER_FILE" + exit 1 + fi + export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_FILE" --raw 2>/dev/null) || { + err "bw unlock failed" + exit 1 + } + log "bw unlocked" +} + +get_field() { + local item="$1" name="$2" + echo "$item" | jq -r ".fields[] | select(.name==\"$name\") | .value // empty" +} + +get_immich_secrets() { + local id + id=$(bw list items --search IMMICH 2>/dev/null | jq -r '.[] | select(.name=="IMMICH") | .id' | head -1) || true + [ -z "$id" ] && id=$(bw list items 2>/dev/null | jq -r '.[] | select(.name=="IMMICH") | .id' | head -1) || true + [ -z "$id" ] && { err "IMMICH not found in Vaultwarden"; exit 1; } + IMMICH_ITEM=$(bw get item "$id" 2>/dev/null) || { err "IMMICH get item failed for id=$id"; exit 1; } + DB_PASSWORD=$(get_field "$IMMICH_ITEM" "DB_PASSWORD") + IMMICH_API_KEY=$(get_field "$IMMICH_ITEM" "IMMICH_API_KEY") + GEMINI_API_KEY=$(get_field "$IMMICH_ITEM" "GEMINI_API_KEY") + if [ -z "$DB_PASSWORD" ]; then err "IMMICH: missing DB_PASSWORD field"; exit 1; fi + if [ -z "$IMMICH_API_KEY" ]; then err "IMMICH: missing IMMICH_API_KEY field"; exit 1; fi +} + +get_deduper_secrets() { + local id + id=$(bw list items 2>/dev/null | jq -r '.[] | select(.name=="IMMICH_DEDUPER") | .id' | head -1) + [ -z "$id" ] && { err "IMMICH_DEDUPER not found in Vaultwarden"; exit 1; } + DEDUP_ITEM=$(bw get item "$id" 2>/dev/null) || { + err "IMMICH_DEDUPER not found in Vaultwarden" + exit 1 + } + PSQL_PASS=$(get_field "$DEDUP_ITEM" "PSQL_PASS") + [ -z "$PSQL_PASS" ] && PSQL_PASS=$(echo "$DEDUP_ITEM" | jq -r '.login.password // empty') + DEDUP_PORT=$(get_field "$DEDUP_ITEM" "DEDUP_PORT") + DEDUP_DATA=$(get_field "$DEDUP_ITEM" "DEDUP_DATA") + DEDUP_IMAGE=$(get_field "$DEDUP_ITEM" "DEDUP_IMAGE") + IMMICH_PATH_FIELD=$(get_field "$DEDUP_ITEM" "IMMICH_PATH") + PSQL_HOST=$(get_field "$DEDUP_ITEM" "PSQL_HOST") + PSQL_PORT=$(get_field "$DEDUP_ITEM" "PSQL_PORT") + PSQL_DB=$(get_field "$DEDUP_ITEM" "PSQL_DB") + [ -z "$PSQL_PASS" ] && PSQL_PASS="${DB_PASSWORD:-}" + DEDUP_PORT="${DEDUP_PORT:-8086}" + DEDUP_DATA="${DEDUP_DATA:-/opt/immich-deduper/data}" + DEDUP_IMAGE="${DEDUP_IMAGE:-razgrizhsu/immich-deduper:latest-cpu}" + IMMICH_PATH_FIELD="${IMMICH_PATH_FIELD:-/mnt/data/library}" + PSQL_HOST="${PSQL_HOST:-database}" + PSQL_PORT="${PSQL_PORT:-5432}" + PSQL_DB="${PSQL_DB:-immich}" +} + +gen_immich_env() { + local tmp + tmp=$(mktemp) + cat > "$tmp" << EOF +# Immich .env (generated from Vaultwarden) +UPLOAD_LOCATION=/mnt/data/library +DB_DATA_LOCATION=/mnt/data/postgres +IMMICH_VERSION=v2 +DB_PASSWORD=${DB_PASSWORD} +DB_USERNAME=postgres +DB_DATABASE_NAME=immich +IMMICH_URL=http://immich-server:2283 +IMMICH_API_KEY=${IMMICH_API_KEY} +DB_HOST=immich_postgres +DB_PORT=5432 +EXTERNAL_IMMICH_URL=https://immich.katykhin.ru +GEMINI_API_KEY=${GEMINI_API_KEY} +EOF + echo "$tmp" +} + +gen_deduper_env() { + local tmp + tmp=$(mktemp) + cat > "$tmp" << EOF +# Deduper .env (generated from Vaultwarden) +DEDUP_PORT=${DEDUP_PORT} +DEDUP_DATA=${DEDUP_DATA} +DEDUP_IMAGE=${DEDUP_IMAGE} +IMMICH_PATH=${IMMICH_PATH_FIELD} +PSQL_HOST=${PSQL_HOST} +PSQL_PORT=${PSQL_PORT} +PSQL_DB=${PSQL_DB} +PSQL_USER=postgres +PSQL_PASS=${PSQL_PASS} +EOF + echo "$tmp" +} + +push_to_vm() { + local local_file="$1" remote_path="$2" + scp -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new -q "$local_file" "${VM_SSH}:/tmp/deploy-env.tmp" || { + err "scp to ${VM_SSH} failed. Ensure SSH key from Proxmox: ssh-copy-id ${VM_SSH}" + exit 1 + } + ssh -o BatchMode=yes -o ConnectTimeout=10 "$VM_SSH" "sudo mv /tmp/deploy-env.tmp ${remote_path} && sudo chmod 600 ${remote_path}" || { + err "ssh to ${VM_SSH} failed" + exit 1 + } +} + +run_compose() { + ssh -o BatchMode=yes "$VM_SSH" "cd ${IMMICH_PATH} && sudo docker compose up -d --force-recreate" + ssh -o BatchMode=yes "$VM_SSH" "cd ${DEDUPER_PATH} && sudo docker compose up -d --force-recreate" + log "Immich and deduper started" +} + +main() { + log "deploy-immich-credentials start (dry_run=$DRY_RUN)" + ensure_bw_unlocked + get_immich_secrets + get_deduper_secrets + + if [ "$DRY_RUN" = true ]; then + log "DRY-RUN: would push .env files and run compose" + log " DB_PASSWORD=*** IMMICH_API_KEY=***" + exit 0 + fi + + tmp_immich=$(gen_immich_env) + tmp_deduper=$(gen_deduper_env) + trap "rm -f $tmp_immich $tmp_deduper" EXIT + push_to_vm "$tmp_immich" "${IMMICH_PATH}/.env" + log "Immich .env written" + push_to_vm "$tmp_deduper" "${DEDUPER_PATH}/.env" + log "Deduper .env written" + run_compose + log "done" +} + +main diff --git a/scripts/deploy-invidious-credentials.sh b/scripts/deploy-invidious-credentials.sh new file mode 100644 index 0000000..d17a570 --- /dev/null +++ b/scripts/deploy-invidious-credentials.sh @@ -0,0 +1,116 @@ +#!/bin/bash +# deploy-invidious-credentials.sh — деплой кредов Invidious в CT 107 +# Секреты из Vaultwarden (объект INVIDIOUS). Атомарная запись .env. +# +# Использование: +# /root/scripts/deploy-invidious-credentials.sh +# /root/scripts/deploy-invidious-credentials.sh --dry-run +# +# Ротация: сменил пароль/ключи в Vaultwarden → запустил скрипт → docker compose up -d --force-recreate +# +# Требования: bw, jq, /root/.bw-master (chmod 600) + +set -e + +CT_ID=107 +INVIDIOUS_PATH="/opt/invidious" +BW_MASTER_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}" +DRY_RUN=false + +for arg in "$@"; do + case "$arg" in + --dry-run) DRY_RUN=true ;; + esac +done + +export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}" + +log() { echo "[$(date -Iseconds)] $*"; } +err() { echo "[$(date -Iseconds)] ERROR: $*" >&2; } + +ensure_bw_unlocked() { + local status + status=$(bw status 2>/dev/null | jq -r '.status' 2>/dev/null || echo "unknown") + if [ "$status" = "unlocked" ]; then + log "bw already unlocked, reusing session" + return 0 + fi + if [ ! -f "$BW_MASTER_FILE" ]; then + err "Missing $BW_MASTER_FILE" + exit 1 + fi + export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_FILE" --raw 2>/dev/null) || { + err "bw unlock failed" + exit 1 + } + log "bw unlocked" +} + +get_secrets() { + local item + item=$(bw get item "INVIDIOUS" 2>/dev/null) + POSTGRES_USER=$(echo "$item" | jq -r '.login.username // empty') + POSTGRES_PASSWORD=$(bw get password "INVIDIOUS" 2>/dev/null) + INVIDIOUS_COMPANION_KEY=$(echo "$item" | jq -r '.fields[] | select(.name=="SERVER_SECRET_KEY") | .value // empty') + HMAC_KEY=$(echo "$item" | jq -r '.fields[] | select(.name=="HMAC_KEY") | .value // empty') + + if [ -z "$POSTGRES_USER" ] || [ -z "$POSTGRES_PASSWORD" ]; then + err "INVIDIOUS: missing username or password" + exit 1 + fi + if [ -z "$INVIDIOUS_COMPANION_KEY" ]; then + err "INVIDIOUS: missing SERVER_SECRET_KEY field" + exit 1 + fi + if [ -z "$HMAC_KEY" ]; then + err "INVIDIOUS: missing HMAC_KEY field" + exit 1 + fi +} + +gen_env() { + local tmp + tmp=$(mktemp) + cat > "$tmp" << EOF +POSTGRES_USER=${POSTGRES_USER} +POSTGRES_PASSWORD=${POSTGRES_PASSWORD} +POSTGRES_DB=invidious +INVIDIOUS_COMPANION_KEY=${INVIDIOUS_COMPANION_KEY} +HMAC_KEY=${HMAC_KEY} +EOF + echo "$tmp" +} + +push_env_atomic() { + local tmp="$1" + < "$tmp" pct exec "$CT_ID" -- bash -c "cat > ${INVIDIOUS_PATH}/.env.tmp && chmod 600 ${INVIDIOUS_PATH}/.env.tmp && mv ${INVIDIOUS_PATH}/.env.tmp ${INVIDIOUS_PATH}/.env" + log ".env written (atomic), chmod 600" +} + +run_compose() { + pct exec "$CT_ID" -- bash -c "cd ${INVIDIOUS_PATH} && docker compose up -d --force-recreate" + log "Invidious started" +} + +main() { + log "deploy-invidious-credentials start (dry_run=$DRY_RUN)" + ensure_bw_unlocked + get_secrets + + if [ "$DRY_RUN" = true ]; then + log "DRY-RUN: would push .env and run compose" + log " POSTGRES_USER=$POSTGRES_USER" + log " POSTGRES_PASSWORD=***" + log " INVIDIOUS_COMPANION_KEY=***" + log " HMAC_KEY=***" + exit 0 + fi + + tmp=$(gen_env) + trap "rm -f $tmp" EXIT + push_env_atomic "$tmp" + run_compose + log "done" +} + +main diff --git a/scripts/deploy-nextcloud-credentials.sh b/scripts/deploy-nextcloud-credentials.sh new file mode 100644 index 0000000..b821b8c --- /dev/null +++ b/scripts/deploy-nextcloud-credentials.sh @@ -0,0 +1,125 @@ +#!/bin/bash +# deploy-nextcloud-credentials.sh — деплой кредов Nextcloud в CT 101 +# Секреты из Vaultwarden (объект NEXTCLOUD). Атомарная запись .env, обновление config.php через occ. +# +# Использование: +# /root/scripts/deploy-nextcloud-credentials.sh +# /root/scripts/deploy-nextcloud-credentials.sh --dry-run +# +# Ротация: сменил пароль/ключи в Vaultwarden → запустил скрипт → docker compose up -d --force-recreate +# +# Требования: bw, jq, /root/.bw-master (chmod 600) + +set -e + +CT_ID=101 +NEXTCLOUD_PATH="/opt/nextcloud" +CONFIG_PATH="/mnt/nextcloud-data/html/config/config.php" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BW_MASTER_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}" +DRY_RUN=false + +for arg in "$@"; do + case "$arg" in + --dry-run) DRY_RUN=true ;; + esac +done + +export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}" + +log() { echo "[$(date -Iseconds)] $*"; } +err() { echo "[$(date -Iseconds)] ERROR: $*" >&2; } + +ensure_bw_unlocked() { + local status + status=$(bw status 2>/dev/null | jq -r '.status' 2>/dev/null || echo "unknown") + if [ "$status" = "unlocked" ]; then + log "bw already unlocked, reusing session" + return 0 + fi + if [ ! -f "$BW_MASTER_FILE" ]; then + err "Missing $BW_MASTER_FILE" + exit 1 + fi + export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_FILE" --raw 2>/dev/null) || { + err "bw unlock failed" + exit 1 + } + log "bw unlocked" +} + +get_secrets() { + local item + item=$(bw get item "NEXTCLOUD" 2>/dev/null) + POSTGRES_PASSWORD=$(bw get password "NEXTCLOUD" 2>/dev/null) + NEXTCLOUD_TRUSTED_DOMAINS=$(echo "$item" | jq -r '.fields[] | select(.name=="NEXTCLOUD_TRUSTED_DOMAINS") | .value // empty') + DBPASSWORD=$(echo "$item" | jq -r '.fields[] | select(.name=="dbpassword") | .value // empty') + SECRET=$(echo "$item" | jq -r '.fields[] | select(.name=="secret") | .value // empty') + PASSWORDSALT=$(echo "$item" | jq -r '.fields[] | select(.name=="passwordsalt") | .value // empty') + INSTANCEID=$(echo "$item" | jq -r '.fields[] | select(.name=="instanceid") | .value // empty') + + if [ -z "$POSTGRES_PASSWORD" ]; then + err "NEXTCLOUD: missing password (POSTGRES_PASSWORD)" + exit 1 + fi + NEXTCLOUD_TRUSTED_DOMAINS="${NEXTCLOUD_TRUSTED_DOMAINS:-cloud.katykhin.ru 192.168.1.101}" +} + +gen_env() { + local tmp + tmp=$(mktemp) + cat > "$tmp" << EOF +POSTGRES_PASSWORD=${POSTGRES_PASSWORD} +NEXTCLOUD_TRUSTED_DOMAINS=${NEXTCLOUD_TRUSTED_DOMAINS} +EOF + echo "$tmp" +} + +push_env_atomic() { + local tmp="$1" + < "$tmp" pct exec "$CT_ID" -- bash -c "cat > ${NEXTCLOUD_PATH}/.env.tmp && chmod 600 ${NEXTCLOUD_PATH}/.env.tmp && mv ${NEXTCLOUD_PATH}/.env.tmp ${NEXTCLOUD_PATH}/.env" + log ".env written (atomic), chmod 600" +} + +push_compose() { + local compose_src="${SCRIPT_DIR}/nextcloud/docker-compose.yml" + if [ -f "$compose_src" ]; then + pct push "$CT_ID" "$compose_src" "${NEXTCLOUD_PATH}/docker-compose.yml" + log "docker-compose.yml pushed" + fi +} + +update_config_occ() { + [ -n "$DBPASSWORD" ] && pct exec "$CT_ID" -- docker exec nextcloud-nextcloud-1 php /var/www/html/occ config:system:set dbpassword --value="$DBPASSWORD" 2>/dev/null || true + [ -n "$SECRET" ] && pct exec "$CT_ID" -- docker exec nextcloud-nextcloud-1 php /var/www/html/occ config:system:set secret --value="$SECRET" 2>/dev/null || true + [ -n "$PASSWORDSALT" ] && pct exec "$CT_ID" -- docker exec nextcloud-nextcloud-1 php /var/www/html/occ config:system:set passwordsalt --value="$PASSWORDSALT" 2>/dev/null || true + [ -n "$INSTANCEID" ] && pct exec "$CT_ID" -- docker exec nextcloud-nextcloud-1 php /var/www/html/occ config:system:set instanceid --value="$INSTANCEID" 2>/dev/null || true + log "config.php updated via occ" +} + +run_compose() { + pct exec "$CT_ID" -- bash -c "cd ${NEXTCLOUD_PATH} && docker compose up -d --force-recreate" + log "Nextcloud started" +} + +main() { + log "deploy-nextcloud-credentials start (dry_run=$DRY_RUN)" + ensure_bw_unlocked + get_secrets + + if [ "$DRY_RUN" = true ]; then + log "DRY-RUN: would push .env, compose, update config, run compose" + log " POSTGRES_PASSWORD=***" + exit 0 + fi + + tmp=$(gen_env) + trap "rm -f $tmp" EXIT + push_env_atomic "$tmp" + push_compose + run_compose + update_config_occ + log "done" +} + +main diff --git a/scripts/deploy-paperless-credentials.sh b/scripts/deploy-paperless-credentials.sh new file mode 100644 index 0000000..0f69c74 --- /dev/null +++ b/scripts/deploy-paperless-credentials.sh @@ -0,0 +1,129 @@ +#!/bin/bash +# deploy-paperless-credentials.sh — деплой кредов Paperless в CT 104 +# Секреты из Vaultwarden (объект PAPERLESS). Атомарная запись docker-compose.env. +# +# Использование: +# /root/scripts/deploy-paperless-credentials.sh +# /root/scripts/deploy-paperless-credentials.sh --dry-run +# +# Ротация: сменил пароль/ключи в Vaultwarden → запустил скрипт → docker compose up -d --force-recreate +# +# Требования: bw, jq, /root/.bw-master (chmod 600) + +set -e + +CT_ID=104 +PAPERLESS_PATH="/opt/paperless" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BW_MASTER_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}" +DRY_RUN=false + +for arg in "$@"; do + case "$arg" in + --dry-run) DRY_RUN=true ;; + esac +done + +export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}" + +log() { echo "[$(date -Iseconds)] $*"; } +err() { echo "[$(date -Iseconds)] ERROR: $*" >&2; } + +ensure_bw_unlocked() { + local status + status=$(bw status 2>/dev/null | jq -r '.status' 2>/dev/null || echo "unknown") + if [ "$status" = "unlocked" ]; then + log "bw already unlocked, reusing session" + return 0 + fi + if [ ! -f "$BW_MASTER_FILE" ]; then + err "Missing $BW_MASTER_FILE" + exit 1 + fi + export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_FILE" --raw 2>/dev/null) || { + err "bw unlock failed" + exit 1 + } + log "bw unlocked" +} + +get_secrets() { + local item + item=$(bw get item "PAPERLESS" 2>/dev/null) + POSTGRES_PASSWORD=$(bw get password "PAPERLESS" 2>/dev/null) + PAPERLESS_URL=$(echo "$item" | jq -r '.fields[] | select(.name=="PAPERLESS_URL") | .value // empty') + PAPERLESS_SECRET_KEY=$(echo "$item" | jq -r '.fields[] | select(.name=="PAPERLESS_SECRET_KEY") | .value // empty') + PAPERLESS_TIME_ZONE=$(echo "$item" | jq -r '.fields[] | select(.name=="PAPERLESS_TIME_ZONE") | .value // empty') + PAPERLESS_OCR_LANGUAGE=$(echo "$item" | jq -r '.fields[] | select(.name=="PAPERLESS_OCR_LANGUAGE") | .value // empty') + PAPERLESS_OCR_LANGUAGES=$(echo "$item" | jq -r '.fields[] | select(.name=="PAPERLESS_OCR_LANGUAGES") | .value // empty') + + if [ -z "$POSTGRES_PASSWORD" ]; then + err "PAPERLESS: missing password (POSTGRES_PASSWORD)" + exit 1 + fi + if [ -z "$PAPERLESS_SECRET_KEY" ]; then + err "PAPERLESS: missing PAPERLESS_SECRET_KEY field" + exit 1 + fi + PAPERLESS_URL="${PAPERLESS_URL:-https://docs.katykhin.ru}" + PAPERLESS_TIME_ZONE="${PAPERLESS_TIME_ZONE:-Europe/Moscow}" + PAPERLESS_OCR_LANGUAGE="${PAPERLESS_OCR_LANGUAGE:-rus+eng}" + PAPERLESS_OCR_LANGUAGES="${PAPERLESS_OCR_LANGUAGES:-rus}" +} + +gen_env() { + local tmp + tmp=$(mktemp) + cat > "$tmp" << EOF +POSTGRES_PASSWORD=${POSTGRES_PASSWORD} +PAPERLESS_URL=${PAPERLESS_URL} +PAPERLESS_SECRET_KEY=${PAPERLESS_SECRET_KEY} +PAPERLESS_TIME_ZONE=${PAPERLESS_TIME_ZONE} +PAPERLESS_OCR_LANGUAGE=${PAPERLESS_OCR_LANGUAGE} +PAPERLESS_OCR_LANGUAGES=${PAPERLESS_OCR_LANGUAGES} +EOF + echo "$tmp" +} + +push_env_atomic() { + local tmp="$1" + < "$tmp" pct exec "$CT_ID" -- bash -c "cat > ${PAPERLESS_PATH}/docker-compose.env.tmp && chmod 600 ${PAPERLESS_PATH}/docker-compose.env.tmp && mv ${PAPERLESS_PATH}/docker-compose.env.tmp ${PAPERLESS_PATH}/docker-compose.env" + log "docker-compose.env written (atomic), chmod 600" +} + +push_compose() { + local compose_src="${SCRIPT_DIR}/paperless/docker-compose.yml" + if [ -f "$compose_src" ]; then + pct push "$CT_ID" "$compose_src" "${PAPERLESS_PATH}/docker-compose.yml" + log "docker-compose.yml pushed" + else + log "WARN: ${compose_src} not found, skipping compose push" + fi +} + +run_compose() { + pct exec "$CT_ID" -- bash -c "cd ${PAPERLESS_PATH} && docker compose up -d --force-recreate" + log "Paperless started" +} + +main() { + log "deploy-paperless-credentials start (dry_run=$DRY_RUN)" + ensure_bw_unlocked + get_secrets + + if [ "$DRY_RUN" = true ]; then + log "DRY-RUN: would push docker-compose.env and run compose" + log " POSTGRES_PASSWORD=***" + log " PAPERLESS_URL=$PAPERLESS_URL" + exit 0 + fi + + tmp=$(gen_env) + trap "rm -f $tmp" EXIT + push_env_atomic "$tmp" + push_compose + run_compose + log "done" +} + +main diff --git a/scripts/deploy-rag-credentials.sh b/scripts/deploy-rag-credentials.sh new file mode 100644 index 0000000..74291b3 --- /dev/null +++ b/scripts/deploy-rag-credentials.sh @@ -0,0 +1,130 @@ +#!/bin/bash +# deploy-rag-credentials.sh — деплой кредов RAG-service в CT 105 +# Секреты из Vaultwarden (объект RAG_SERVICE). Атомарная запись .env. +# +# Использование: +# /root/scripts/deploy-rag-credentials.sh +# /root/scripts/deploy-rag-credentials.sh --dry-run +# +# Ротация: сменил RAG_API_KEY в Vaultwarden → запустил скрипт → docker compose up -d --force-recreate +# +# Требования: bw, jq, /root/.bw-master (chmod 600) +# Vaultwarden: создать запись RAG_SERVICE с полем RAG_API_KEY (тип hidden). + +set -e + +CT_ID=105 +RAG_PATH="/home/rag-service" +BW_MASTER_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}" +DRY_RUN=false + +for arg in "$@"; do + case "$arg" in + --dry-run) DRY_RUN=true ;; + esac +done + +export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}" + +log() { echo "[$(date -Iseconds)] $*"; } +err() { echo "[$(date -Iseconds)] ERROR: $*" >&2; } + +ensure_bw_unlocked() { + local status + status=$(bw status 2>/dev/null | jq -r '.status' 2>/dev/null || echo "unknown") + if [ "$status" = "unlocked" ]; then + log "bw already unlocked, reusing session" + return 0 + fi + if [ ! -f "$BW_MASTER_FILE" ]; then + err "Missing $BW_MASTER_FILE" + exit 1 + fi + export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_FILE" --raw 2>/dev/null) || { + err "bw unlock failed" + exit 1 + } + log "bw unlocked" +} + +get_secrets() { + local item + item=$(bw get item "RAG_SERVICE" 2>/dev/null) || { + err "RAG_SERVICE not found in Vaultwarden. Create it: type Login, add custom field RAG_API_KEY (hidden)." + exit 1 + } + RAG_API_KEY=$(echo "$item" | jq -r '.fields[] | select(.name=="RAG_API_KEY") | .value // empty') + if [ -z "$RAG_API_KEY" ]; then + err "RAG_SERVICE: missing RAG_API_KEY field" + exit 1 + fi +} + +gen_env() { + local tmp + tmp=$(mktemp) + cat > "$tmp" << EOF +# RAG Service Configuration (generated from Vaultwarden) + +# Модель +RAG_MODEL=sentence-transformers/all-MiniLM-L12-v2 +RAG_CACHE_DIR=data/models + +# VectorStore +RAG_VECTORS_PATH=data/vectors/vectors.npz +RAG_MAX_EXAMPLES=10000 +RAG_SCORE_MULTIPLIER=5.0 + +# Батч-обработка +RAG_BATCH_SIZE=16 + +# Минимальная длина текста +RAG_MIN_TEXT_LENGTH=3 + +# API настройки +RAG_API_HOST=0.0.0.0 +RAG_API_PORT=8000 + +# Безопасность +RAG_API_KEY=${RAG_API_KEY} +RAG_ALLOW_NO_AUTH=false + +# Автосохранение векторов +RAG_AUTOSAVE_INTERVAL=600 + +# Логирование +LOG_LEVEL=INFO +EOF + echo "$tmp" +} + +push_env_atomic() { + local tmp="$1" + < "$tmp" pct exec "$CT_ID" -- bash -c "cat > ${RAG_PATH}/.env.tmp && chmod 600 ${RAG_PATH}/.env.tmp && mv ${RAG_PATH}/.env.tmp ${RAG_PATH}/.env" + log ".env written (atomic), chmod 600" +} + +run_compose() { + pct exec "$CT_ID" -- bash -c "cd ${RAG_PATH} && docker compose up -d --force-recreate" + log "RAG-service started" +} + +main() { + log "deploy-rag-credentials start (dry_run=$DRY_RUN)" + ensure_bw_unlocked + get_secrets + + if [ "$DRY_RUN" = true ]; then + log "DRY-RUN: would push .env and run compose" + log " RAG_API_KEY=***" + exit 0 + fi + + tmp=$(gen_env) + trap "rm -f $tmp" EXIT + push_env_atomic "$tmp" + run_compose + log "done" +} + +main diff --git a/scripts/deploy-ssh-keys-homelab.sh b/scripts/deploy-ssh-keys-homelab.sh new file mode 100644 index 0000000..22eeec4 --- /dev/null +++ b/scripts/deploy-ssh-keys-homelab.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# Deploy SSH public key to all LXC containers and VM 200 in homelab. +# Run from machine that can reach Proxmox (192.168.1.150). +# Usage: ./deploy-ssh-keys-homelab.sh [path-to-public-key] +# Default: ~/.ssh/id_rsa.pub or ~/.ssh/id_ed25519.pub + +set -e +PROXMOX="${PROXMOX:-root@192.168.1.150}" +KEY_FILE="${1:-$HOME/.ssh/id_rsa.pub}" +[ -f "$HOME/.ssh/id_ed25519.pub" ] && [ ! -f "$KEY_FILE" ] && KEY_FILE="$HOME/.ssh/id_ed25519.pub" + +if [ ! -f "$KEY_FILE" ]; then + echo "Usage: $0 [path-to-public-key]" + echo "No key found at $KEY_FILE" + exit 1 +fi + +CT_IDS="100 101 103 104 105 107 108 109" + +echo "Deploying key from $KEY_FILE to homelab hosts..." + +# Copy key to Proxmox temp, then deploy from there +TMP_KEY="/tmp/deploy-ssh-key-$$.pub" +scp -q "$KEY_FILE" "$PROXMOX:$TMP_KEY" +trap "ssh $PROXMOX 'rm -f $TMP_KEY'" EXIT + +# Proxmox host +echo "Proxmox (192.168.1.150)..." +ssh "$PROXMOX" "mkdir -p /root/.ssh && chmod 700 /root/.ssh && grep -qF \"\$(cat $TMP_KEY)\" /root/.ssh/authorized_keys 2>/dev/null || cat $TMP_KEY >> /root/.ssh/authorized_keys && chmod 600 /root/.ssh/authorized_keys" + +# LXC containers +for id in $CT_IDS; do + echo "CT $id (192.168.1.$id)..." + ssh "$PROXMOX" "pct exec $id -- bash -c 'mkdir -p /root/.ssh && chmod 700 /root/.ssh' && pct push $id $TMP_KEY /tmp/key.pub && pct exec $id -- bash -c 'grep -qF \"\$(cat /tmp/key.pub)\" /root/.ssh/authorized_keys 2>/dev/null || cat /tmp/key.pub >> /root/.ssh/authorized_keys && chmod 600 /root/.ssh/authorized_keys && rm /tmp/key.pub'" +done + +# VM 200 (admin user; root may be disabled) +echo "VM 200 (admin@192.168.1.200)..." +ssh "$PROXMOX" "scp -o StrictHostKeyChecking=accept-new $TMP_KEY admin@192.168.1.200:/tmp/key.pub && ssh admin@192.168.1.200 'mkdir -p /home/admin/.ssh /root/.ssh && chmod 700 /home/admin/.ssh /root/.ssh 2>/dev/null; grep -qF \"\$(cat /tmp/key.pub)\" /home/admin/.ssh/authorized_keys 2>/dev/null || cat /tmp/key.pub >> /home/admin/.ssh/authorized_keys; echo \"\$(cat /tmp/key.pub)\" | sudo tee -a /root/.ssh/authorized_keys >/dev/null; chmod 600 /home/admin/.ssh/authorized_keys /root/.ssh/authorized_keys 2>/dev/null; rm /tmp/key.pub'" + +echo "Done. Connect: ssh root@192.168.1.{100,101,103,104,105,107,108,109}, ssh admin@192.168.1.200" diff --git a/scripts/deploy-vpn-route-check.sh b/scripts/deploy-vpn-route-check.sh new file mode 100644 index 0000000..0546e16 --- /dev/null +++ b/scripts/deploy-vpn-route-check.sh @@ -0,0 +1,111 @@ +#!/bin/bash +# deploy-vpn-route-check.sh — идемпотентный деплой vpn-route-check на CT 100 +# Секреты берутся из Vaultwarden (объект localhost), .env генерируется на Proxmox и пушится в CT. +# +# Использование: +# /root/scripts/deploy-vpn-route-check.sh # деплой +# /root/scripts/deploy-vpn-route-check.sh --dry-run # только проверка, без записи и compose +# +# Требования: bw, jq, /root/.bw-master (chmod 600) + +set -e + +CT_ID=100 +CT_PATH="/opt/docker/vpn-route-check" +BW_MASTER_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}" +DRY_RUN=false + +for arg in "$@"; do + case "$arg" in + --dry-run) DRY_RUN=true ;; + esac +done + +export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}" + +log() { echo "[$(date -Iseconds)] $*"; } +err() { echo "[$(date -Iseconds)] ERROR: $*" >&2; } + +# --- 1. Разблокировка bw (reuse session если возможно) +ensure_bw_unlocked() { + local status + status=$(bw status 2>/dev/null | jq -r '.status' 2>/dev/null || echo "unknown") + if [ "$status" = "unlocked" ]; then + log "bw already unlocked, reusing session" + return 0 + fi + if [ ! -f "$BW_MASTER_FILE" ]; then + err "Missing $BW_MASTER_FILE" + exit 1 + fi + export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_FILE" --raw 2>/dev/null) || { + err "bw unlock failed" + exit 1 + } + log "bw unlocked" +} + +# --- 2. Получить секреты из Vaultwarden (localhost) +get_secrets() { + local host user pass + host=$(bw get item "localhost" 2>/dev/null | jq -r '.fields[] | select(.name=="ROUTER_TELNET_HOST") | .value // empty') + user=$(bw get username "localhost" 2>/dev/null) + pass=$(bw get password "localhost" 2>/dev/null) + + if [ -z "$user" ] || [ -z "$pass" ]; then + err "localhost: missing username or password in Vaultwarden" + exit 1 + fi + host="${host:-192.168.1.1}" + ROUTER_TELNET_HOST="$host" + ROUTER_TELNET_USER="$user" + ROUTER_TELNET_PASSWORD="$pass" +} + +# --- 3. Сгенерировать .env во временный файл +gen_env() { + local tmp + tmp=$(mktemp) + cat > "$tmp" << EOF +ROUTER_TELNET_HOST=${ROUTER_TELNET_HOST} +ROUTER_TELNET_USER=${ROUTER_TELNET_USER} +ROUTER_TELNET_PASSWORD=${ROUTER_TELNET_PASSWORD} +EOF + echo "$tmp" +} + +# --- 4. Атомарно записать .env в CT 100 +push_env_to_ct() { + local tmp="$1" + < "$tmp" pct exec "$CT_ID" -- bash -c "cat > ${CT_PATH}/.env.tmp && chmod 600 ${CT_PATH}/.env.tmp && mv ${CT_PATH}/.env.tmp ${CT_PATH}/.env" + log ".env written to CT $CT_ID (atomic)" +} + +# --- 5. docker compose up -d +run_compose() { + pct exec "$CT_ID" -- bash -c "cd ${CT_PATH} && docker compose up -d --force-recreate" + log "vpn-route-check started" +} + +# --- main +main() { + log "deploy-vpn-route-check start (dry_run=$DRY_RUN)" + ensure_bw_unlocked + get_secrets + + if [ "$DRY_RUN" = true ]; then + log "DRY-RUN: would push .env and run compose" + log " ROUTER_TELNET_HOST=$ROUTER_TELNET_HOST" + log " ROUTER_TELNET_USER=$ROUTER_TELNET_USER" + log " ROUTER_TELNET_PASSWORD=***" + exit 0 + fi + + tmp=$(gen_env) + trap "rm -f $tmp" EXIT + push_env_to_ct "$tmp" + run_compose + log "done" +} + +main diff --git a/scripts/deploy-wireguard-credentials.sh b/scripts/deploy-wireguard-credentials.sh new file mode 100644 index 0000000..2318134 --- /dev/null +++ b/scripts/deploy-wireguard-credentials.sh @@ -0,0 +1,95 @@ +#!/bin/bash +# deploy-wireguard-credentials.sh — деплой конфига WireGuard в CT 109 +# Секреты из Vaultwarden (объект LOCAL_VPN_SERVER_WG, поле wg0_conf — полный конфиг). +# +# Использование: +# /root/scripts/deploy-wireguard-credentials.sh +# /root/scripts/deploy-wireguard-credentials.sh --dry-run +# +# Перед первым запуском: создать в Vaultwarden запись LOCAL_VPN_SERVER_WG, +# добавить кастомное поле wg0_conf (hidden) с полным содержимым /etc/wireguard/wg0.conf. +# +# Ротация: сменил ключи в Vaultwarden → запустил скрипт → systemctl restart wg-quick@wg0 +# +# Требования: bw, jq, /root/.bw-master (chmod 600) + +set -e + +CT_ID=109 +WG_CONF_PATH="/etc/wireguard/wg0.conf" +BW_MASTER_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}" +DRY_RUN=false + +for arg in "$@"; do + case "$arg" in + --dry-run) DRY_RUN=true ;; + esac +done + +export PATH="/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}" + +log() { echo "[$(date -Iseconds)] $*"; } +err() { echo "[$(date -Iseconds)] ERROR: $*" >&2; } + +ensure_bw_unlocked() { + local status + status=$(bw status 2>/dev/null | jq -r '.status' 2>/dev/null || echo "unknown") + if [ "$status" = "unlocked" ]; then + log "bw already unlocked, reusing session" + return 0 + fi + if [ ! -f "$BW_MASTER_FILE" ]; then + err "Missing $BW_MASTER_FILE" + exit 1 + fi + export BW_SESSION=$(bw unlock --passwordfile "$BW_MASTER_FILE" --raw 2>/dev/null) || { + err "bw unlock failed" + exit 1 + } + log "bw unlocked" +} + +get_secrets() { + WG_CONF=$(bw get item "LOCAL_VPN_SERVER_WG" 2>/dev/null | jq -r '.fields[] | select(.name=="wg0_conf") | .value // empty') + if [ -z "$WG_CONF" ]; then + err "LOCAL_VPN_SERVER_WG not found or missing wg0_conf field. Create it in Vaultwarden, add field wg0_conf with full wg0.conf content." + exit 1 + fi + if ! echo "$WG_CONF" | grep -q '\[Interface\]'; then + err "wg0_conf: invalid format (expected [Interface] section)" + exit 1 + fi +} + +push_conf() { + local tmp + tmp=$(mktemp) + echo "$WG_CONF" > "$tmp" + pct push "$CT_ID" "$tmp" "${WG_CONF_PATH}.tmp" + rm -f "$tmp" + pct exec "$CT_ID" -- bash -c "chmod 600 ${WG_CONF_PATH}.tmp && mv ${WG_CONF_PATH}.tmp ${WG_CONF_PATH}" + log "wg0.conf written (atomic), chmod 600" +} + +restart_wg() { + pct exec "$CT_ID" -- systemctl restart wg-quick@wg0 + log "wg-quick@wg0 restarted" +} + +main() { + log "deploy-wireguard-credentials start (dry_run=$DRY_RUN)" + ensure_bw_unlocked + get_secrets + + if [ "$DRY_RUN" = true ]; then + log "DRY-RUN: would push wg0.conf and restart WireGuard" + log " wg0_conf: $(echo "$WG_CONF" | head -3)..." + exit 0 + fi + + push_conf + restart_wg + log "done" +} + +main diff --git a/scripts/gitea/docker-compose.yml b/scripts/gitea/docker-compose.yml new file mode 100644 index 0000000..4efd858 --- /dev/null +++ b/scripts/gitea/docker-compose.yml @@ -0,0 +1,74 @@ +# Шаблон для /opt/gitea/ на CT 103 +# Секреты в .env (генерируется deploy-gitea-credentials.sh из Vaultwarden). +# .env не коммитить. + +services: + db: + image: docker.io/library/postgres:16-alpine + restart: unless-stopped + env_file: .env + environment: + POSTGRES_USER: gitea + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: gitea + volumes: + - gitea-postgres:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U gitea"] + interval: 10s + timeout: 5s + retries: 5 + + server: + image: docker.gitea.com/gitea:1.25 + container_name: gitea + restart: unless-stopped + depends_on: + db: + condition: service_healthy + env_file: .env + environment: + USER_UID: 1000 + USER_GID: 1000 + GITEA__database__DB_TYPE: postgres + GITEA__database__HOST: db:5432 + GITEA__database__NAME: gitea + GITEA__database__USER: gitea + GITEA__database__PASSWD: ${POSTGRES_PASSWORD} + GITEA__server__DOMAIN: 192.168.1.103 + GITEA__server__ROOT_URL: http://192.168.1.103:3000/ + GITEA__server__SSH_PORT: 2222 + volumes: + - gitea-data:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + ports: + - "3000:3000" + - "2222:22" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3000/"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 30s + + runner: + image: docker.io/gitea/act_runner:latest + restart: unless-stopped + depends_on: + server: + condition: service_healthy + env_file: .env + environment: + GITEA_INSTANCE_URL: http://server:3000 + GITEA_RUNNER_REGISTRATION_TOKEN: ${GITEA_RUNNER_REGISTRATION_TOKEN} + GITEA_RUNNER_NAME: gitea-103-runner + GITEA_RUNNER_LABELS: docker:docker://alpine:latest + volumes: + - runner-data:/data + - /var/run/docker.sock:/var/run/docker.sock + +volumes: + gitea-data: + gitea-postgres: + runner-data: diff --git a/scripts/invidious/docker-compose.yml b/scripts/invidious/docker-compose.yml new file mode 100644 index 0000000..1518f3b --- /dev/null +++ b/scripts/invidious/docker-compose.yml @@ -0,0 +1,84 @@ +# Шаблон для /opt/invidious/docker-compose.yml на CT 107 +# Секреты в .env (генерируется deploy-invidious-credentials.sh из Vaultwarden). +# .env не коммитить. + +services: + invidious: + image: quay.io/invidious/invidious:latest + restart: unless-stopped + ports: + - "3000:3000" + env_file: .env + environment: + INVIDIOUS_CONFIG: | + db: + dbname: invidious + user: ${POSTGRES_USER} + password: ${POSTGRES_PASSWORD} + host: invidious-db + port: 5432 + check_tables: true + invidious_companion: + - private_url: "http://companion:8282/companion" + invidious_companion_key: "${INVIDIOUS_COMPANION_KEY}" + external_port: 443 + domain: "video.katykhin.ru" + https_only: true + use_pubsub_feeds: true + use_innertube_for_captions: true + hmac_key: "${HMAC_KEY}" + default_user_preferences: + default_home: Popular + dark_mode: "light" + player_style: "youtube" + vr_mode: false + automatic_instance_redirect: false + healthcheck: + test: wget -nv --tries=1 --spider http://127.0.0.1:3000/api/v1/stats || exit 1 + interval: 30s + timeout: 5s + retries: 2 + logging: + options: + max-size: "1G" + max-file: "4" + depends_on: + invidious-db: + condition: service_healthy + + companion: + image: quay.io/invidious/invidious-companion:latest + env_file: .env + environment: + SERVER_SECRET_KEY: ${INVIDIOUS_COMPANION_KEY} + restart: unless-stopped + logging: + options: + max-size: "1G" + max-file: "4" + cap_drop: + - ALL + read_only: true + volumes: + - companioncache:/var/tmp/youtubei.js:rw + security_opt: + - no-new-privileges:true + + invidious-db: + image: docker.io/library/postgres:14 + restart: unless-stopped + volumes: + - postgresdata:/var/lib/postgresql/data + - ./config/sql:/config/sql + - ./docker/init-invidious-db.sh:/docker-entrypoint-initdb.d/init-invidious-db.sh + env_file: .env + environment: + POSTGRES_DB: invidious + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] + +volumes: + postgresdata: + companioncache: diff --git a/scripts/nextcloud/docker-compose.yml b/scripts/nextcloud/docker-compose.yml new file mode 100644 index 0000000..3bc40f2 --- /dev/null +++ b/scripts/nextcloud/docker-compose.yml @@ -0,0 +1,52 @@ +# Шаблон для /opt/nextcloud/ на CT 101 +# Секреты в .env (генерируется deploy-nextcloud-credentials.sh из Vaultwarden). +# .env не коммитить. + +services: + db: + image: docker.io/library/postgres:16 + restart: unless-stopped + volumes: + - /mnt/nextcloud-data/pgdata:/var/lib/postgresql/data + env_file: .env + environment: + POSTGRES_DB: nextcloud + POSTGRES_USER: nextcloud + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U nextcloud"] + interval: 10s + timeout: 5s + retries: 5 + + redis: + image: docker.io/library/redis:7-alpine + restart: unless-stopped + command: redis-server --appendonly yes + + nextcloud: + image: docker.io/nextcloud:latest + restart: unless-stopped + depends_on: + db: + condition: service_healthy + redis: + condition: service_started + ports: + - "8080:80" + volumes: + - /mnt/nextcloud-data/html:/var/www/html + - /mnt/nextcloud-extra:/mnt/nextcloud-extra + - /opt/nextcloud/php-uploads.ini:/usr/local/etc/php/conf.d/zz-uploads.ini:ro + env_file: .env + environment: + APACHE_BODY_LIMIT: "0" + NEXTCLOUD_TRUSTED_DOMAINS: ${NEXTCLOUD_TRUSTED_DOMAINS} + OVERWRITEPROTOCOL: https + OVERWRITEHOST: cloud.katykhin.ru + OVERWRITECLIURL: https://cloud.katykhin.ru + REDIS_HOST: redis + POSTGRES_HOST: db + POSTGRES_DB: nextcloud + POSTGRES_USER: nextcloud + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} diff --git a/scripts/npm-add-proxy-vault.sh b/scripts/npm-add-proxy-vault.sh index 8762bb4..cb3d1a2 100644 --- a/scripts/npm-add-proxy-vault.sh +++ b/scripts/npm-add-proxy-vault.sh @@ -1,15 +1,17 @@ #!/bin/bash # Add vault.katykhin.ru → 192.168.1.103:8280 via NPM API + Access List (LAN + VPN only) -# Usage: NPM_EMAIL=j3tears100@gmail.com NPM_PASSWORD=xxx ./npm-add-proxy-vault.sh +# Usage: NPM_EMAIL=... NPM_PASSWORD=... ./npm-add-proxy-vault.sh +# NPM credentials: Vaultwarden, объект NPM_ADMIN (username=email, password) # Run from host that can reach NPM, or: ssh root@192.168.1.150 "pct exec 100 -- bash -s" < scripts/npm-add-proxy-vault.sh -# (then set NPM_URL=http://127.0.0.1:81 and NPM_EMAIL/NPM_PASSWORD in env or below) -# NPM credentials: see docs/containers/container-100.md +# (then set NPM_URL=http://127.0.0.1:81 and NPM_EMAIL/NPM_PASSWORD in env) set -e NPM_URL="${NPM_URL:-http://192.168.1.100:81}" API="$NPM_URL/api" -NPM_EMAIL="${NPM_EMAIL:-j3tears100@gmail.com}" -NPM_PASSWORD="${NPM_PASSWORD:-kqEUubVq02DJTS8}" +if [ -z "$NPM_EMAIL" ] || [ -z "$NPM_PASSWORD" ]; then + echo "Set NPM_EMAIL and NPM_PASSWORD (from Vaultwarden, объект NPM_ADMIN)" + exit 1 +fi echo "1. Getting token..." TOKEN=$(curl -s -X POST "$API/tokens" \ diff --git a/scripts/paperless/docker-compose.yml b/scripts/paperless/docker-compose.yml new file mode 100644 index 0000000..f7ff65a --- /dev/null +++ b/scripts/paperless/docker-compose.yml @@ -0,0 +1,41 @@ +# Шаблон для /opt/paperless/ на CT 104 +# Секреты в docker-compose.env (генерируется deploy-paperless-credentials.sh из Vaultwarden). +# docker-compose.env не коммитить. + +services: + broker: + image: docker.io/library/redis:8 + restart: unless-stopped + volumes: + - redisdata:/data + + db: + image: docker.io/library/postgres:18 + restart: unless-stopped + volumes: + - /mnt/paperless-data/pgdata:/var/lib/postgresql + env_file: docker-compose.env + environment: + POSTGRES_DB: paperless + POSTGRES_USER: paperless + + webserver: + image: ghcr.io/paperless-ngx/paperless-ngx:latest + restart: unless-stopped + depends_on: + - db + - broker + ports: + - "8000:8000" + volumes: + - /mnt/paperless-data/data:/usr/src/paperless/data + - /mnt/paperless-data/media:/usr/src/paperless/media + - ./export:/usr/src/paperless/export + - ./consume:/usr/src/paperless/consume + env_file: docker-compose.env + environment: + PAPERLESS_REDIS: redis://broker:6379 + PAPERLESS_DBHOST: db + +volumes: + redisdata: diff --git a/scripts/restore-one-vzdump-from-restic.sh b/scripts/restore-one-vzdump-from-restic.sh index 1e99b62..a1c778f 100644 --- a/scripts/restore-one-vzdump-from-restic.sh +++ b/scripts/restore-one-vzdump-from-restic.sh @@ -30,7 +30,7 @@ fi # Секреты из Vaultwarden (объект RESTIC) BW_MASTER_PASSWORD_FILE="${BW_MASTER_PASSWORD_FILE:-/root/.bw-master}" if [ ! -f "$BW_MASTER_PASSWORD_FILE" ]; then - echo "Нет файла с мастер-паролем Vaultwarden: $BW_MASTER_PASSWORD_FILE (chmod 600). См. docs/backup/proxmox-phase1-backup.md" + echo "Нет файла с мастер-паролем Vaultwarden: $BW_MASTER_PASSWORD_FILE (chmod 600). См. docs/vaultwarden-secrets.md" exit 1 fi if ! command -v bw >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then diff --git a/scripts/vpn-route-check/docker-compose.yml b/scripts/vpn-route-check/docker-compose.yml new file mode 100644 index 0000000..c202199 --- /dev/null +++ b/scripts/vpn-route-check/docker-compose.yml @@ -0,0 +1,16 @@ +# Шаблон для /opt/docker/vpn-route-check/docker-compose.yml на CT 100 +# Секреты в .env (генерируется deploy-vpn-route-check.sh из Vaultwarden). +# .env не коммитить. + +services: + vpn-route-check: + build: . + container_name: vpn-route-check + network_mode: host + env_file: .env + volumes: + - vpn-route-check-data:/data + restart: unless-stopped + +volumes: + vpn-route-check-data: