diff --git a/OPERATIONS_RUNBOOK.md b/VPS/OPERATIONS_RUNBOOK.md similarity index 100% rename from OPERATIONS_RUNBOOK.md rename to VPS/OPERATIONS_RUNBOOK.md diff --git a/VPS/PROJECT_STATE.md b/VPS/PROJECT_STATE.md new file mode 100644 index 0000000..3ab0665 --- /dev/null +++ b/VPS/PROJECT_STATE.md @@ -0,0 +1,65 @@ +# VPS nao-kvn.ru — состояние проекта (компактно) + +Стартовый контекст для нового чата. Только текущее состояние. Секреты — в файлах на VPS, не здесь. + +## Доступ +- VPS: `ssh root@45.91.8.169` — только ed25519-ключ (на Manjaro, с passphrase), пароль отключён. +- Стек: `/opt/docker/core/` (`docker-compose.yml`, `caddy/Caddyfile`). +- Сервисы: https://vault.nao-kvn.ru, https://portainer.nao-kvn.ru, https://status.nao-kvn.ru. ТЗ VPS: MSK. + +## Цель +Личный VPS: Portainer (управление Docker), Uptime Kuma (мониторинг доступности сервисов по трём площадкам с белыми IP — ДК «Арктика», Архангельскстат Нарьян-Мар, дом), Vaultwarden (пароли). Всё защищено и с проверенным многокопийным бэкапом. + +## Серверы и роли +- **VPS** `45.91.8.169` (`box-938039`): Ubuntu 24.04 LTS, KVM, 1 vCPU / 961 MiB RAM / 20 ГБ, swap 2 ГБ, MSK, тариф 400₽. Несёт Docker-стек, WireGuard-клиент, бэкапы. RAM в обрез (~0.5/0.96 ГБ занято). +- **Дом — Keenetic Hopper KN-3811**: белый IP `91.122.209.x`, WireGuard-сервер, SMB-сервер (USB-диск, шара `TOSHIBA EXT`, юзер `only_vps`), LAN `172.16.0.0/24` (SMB-хост `172.16.0.1`). + +## Архитектура +Один Compose-стек. **Caddy — единственный публичный вход (80/443)**, авто-HTTPS Let's Encrypt. Остальные контейнеры портов на хост не публикуют — только в сети `core_proxy`, Caddy проксирует по имени контейнера. Portainer и Uptime Kuma — за Caddy basic-auth; Vaultwarden публичен (своя авторизация). SMB-офсайт — через WG-туннель VPS→Keenetic (порт 445 в интернет не выставлен). + +## Сервисы (стек /opt/docker/core, сеть core_proxy, restart: unless-stopped) +| Сервис | Образ | Порт | Доступ | +|---|---|---|---| +| caddy | caddy:2 | 80/443 | proxy + TLS | +| portainer | portainer/portainer-ce:lts | 9000 | portainer.nao-kvn.ru, basic-auth | +| uptime-kuma | louislam/uptime-kuma:1 | 3001 | status.nao-kvn.ru, basic-auth | +| vaultwarden | vaultwarden/server:latest | 80 | vault.nao-kvn.ru, публичный | + +Vaultwarden: `DOMAIN=https://vault.nao-kvn.ru`, `SIGNUPS_ALLOWED=false`, `ADMIN_TOKEN` задан. Portainer-админ: `administrator`. Volumes: `core_{caddy_data,caddy_config,portainer_data,uptime_data,vaultwarden_data}`. + +## Безопасность +- HTTPS везде; наружу только 80/443 (Caddy). +- Caddy basic-auth (логин `admin`, bcrypt) перед Portainer и Uptime Kuma (сниппет `(protected)`). +- Vaultwarden: регистрация закрыта. +- UFW: входящие закрыты, разрешён только SSH (22, v4+v6); 80/443 ведёт Docker. fail2ban на SSH (бан через UFW). +- SSH: только ключ, пароль отключён, root `prohibit-password` (`/etc/ssh/sshd_config.d/00-hardening.conf`). +- Docker-логи лимитированы (10 МБ × 3). SMB доступен только через приватный WG. + +## DNS / TLS +- Домен `nao-kvn.ru`. A-записи → `45.91.8.169`: `portainer`, `status`, `vault`. +- Caddy авто-выпускает/продлевает Let's Encrypt (TLS-ALPN-01) для трёх поддоменов; сертификаты в `core_caddy_data`. + +## Бэкап (Vaultwarden) +- Скрипт `/usr/local/bin/vw-backup.sh`: консистентный снимок БД `sqlite3 .backup` + `rsa_key*`/`config`/`attachments`/`sends` → `tar.gz`. +- Три копии: (1) локально `/opt/backups/vaultwarden` (14 шт.); (2) Google Drive `gdrive:vaultwarden-backups/` — обязательная, 90 дн; (3) домашний SMB `homesmb:TOSHIBA EXT/vaultwarden-backups/` — best-effort (не валит бэкап при недоступности дома), 90 дн. +- **Восстановление проверено фактически** (integrity ok, аккаунт + записи + `rsa_key` на месте). +- Расписание: systemd `vw-backup.timer`, ежедневно 03:30 MSK, Persistent. +- rclone-remotes: `gdrive` (scope drive.file), `homesmb` (smb, 172.16.0.1, only_vps). +- WG: `wg-quick@wg0` enabled, VPS `10.0.0.5`, Keenetic `10.0.0.1`, маршрут на `172.16.0.0/24`. + +## Мониторинг +Uptime Kuma развёрнут (`status.nao-kvn.ru`, basic-auth + свой логин, админ создан). **Мониторы и уведомления ещё НЕ настроены** — главная функциональная недоделка. Telegram отвергнут (массовые блокировки в РФ с весны 2026); планируется ntfy (self-hosted) или email. + +## Пакеты +Docker CE 29.6.0 / containerd 2.2.5 / compose; sqlite3; rclone (с SMB-бэкендом); wireguard-tools; smbclient; fail2ban; ufw. + +## Открытые задачи +1. **Уведомления**: поднять ntfy (self-hosted, `ntfy.nao-kvn.ru` через Caddy) или email в Uptime Kuma. +2. **Мониторы** по трём площадкам: ping шлюзов + TCP/HTTP ключевых сервисов; группы «Арктика / Архангельскстат / Дом»; повесить уведомления. +3. **Перенести пароли** из бумажного блокнота в Vaultwarden (бэкап проверен — безопасно), затем уничтожить блокнот. + +## Известные риски +RAM в обрез (апгрейд до 2 ГБ при расширении); мониторинг не введён в строй (нет оповещений при сбое); `ADMIN_TOKEN` открытым текстом в compose; SMB-копия зависит от доступности дома; один VPS — единая точка отказа по аптайму. + +## Принципы работы +Продакшен-аккуратность: для рискованных изменений сначала проверка, потом применение, с путём отката; по одному шагу за раз с подтверждением; в multi-step процедурах шаги не объединять. diff --git a/TODO.md b/VPS/TODO.md similarity index 100% rename from TODO.md rename to VPS/TODO.md diff --git a/VPS/backups/README.md b/VPS/backups/README.md new file mode 100644 index 0000000..293541a --- /dev/null +++ b/VPS/backups/README.md @@ -0,0 +1,8 @@ +# Бэкапы Vaultwarden +Эта папка — только описание; сами архивы здесь не хранятся. + +- **Что:** консистентный снимок БД Vaultwarden (`sqlite3 .backup`) + `rsa_key*`/`config.json`/`attachments`/`sends` → `tar.gz`. +- **Где:** локально на VPS `/opt/backups/vaultwarden/` (последние 14); Google Drive `gdrive:vaultwarden-backups/`; домашний SMB `homesmb:TOSHIBA EXT/vaultwarden-backups/`. +- **Расписание:** systemd `vw-backup.timer`, ежедневно 03:30 MSK (Persistent). +- **Скрипт:** `../scripts/vw-backup.sh` (= `/usr/local/bin/vw-backup.sh`). +- **Восстановление:** см. `../OPERATIONS_RUNBOOK.md`. Статус — ПРОВЕРЕНО фактически. diff --git a/VPS/configs/Caddyfile b/VPS/configs/Caddyfile new file mode 100644 index 0000000..ee7f31c --- /dev/null +++ b/VPS/configs/Caddyfile @@ -0,0 +1,17 @@ +# /opt/docker/core/caddy/Caddyfile (bcrypt-хеш скрыт — реальный в файле на VPS) +(protected) { + basic_auth { + admin + } +} +portainer.nao-kvn.ru { + import protected + reverse_proxy portainer:9000 +} +status.nao-kvn.ru { + import protected + reverse_proxy uptime-kuma:3001 +} +vault.nao-kvn.ru { + reverse_proxy vaultwarden:80 +} diff --git a/VPS/configs/docker-compose.yml b/VPS/configs/docker-compose.yml new file mode 100644 index 0000000..35f9cfa --- /dev/null +++ b/VPS/configs/docker-compose.yml @@ -0,0 +1,38 @@ +# /opt/docker/core/docker-compose.yml (ADMIN_TOKEN скрыт — реальный в файле на VPS) +services: + caddy: + image: caddy:2 + restart: unless-stopped + ports: ["80:80", "443:443"] + volumes: + - ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro + - caddy_data:/data + - caddy_config:/config + networks: [proxy] + + portainer: + image: portainer/portainer-ce:lts + restart: unless-stopped + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - portainer_data:/data + networks: [proxy] + + uptime-kuma: + image: louislam/uptime-kuma:1 + restart: unless-stopped + volumes: [uptime_data:/app/data] + networks: [proxy] + + vaultwarden: + image: vaultwarden/server:latest + restart: unless-stopped + environment: + DOMAIN: "https://vault.nao-kvn.ru" + SIGNUPS_ALLOWED: "false" + ADMIN_TOKEN: "<задан в файле на VPS>" + volumes: [vaultwarden_data:/data] + networks: [proxy] + +volumes: { caddy_data: , caddy_config: , portainer_data: , uptime_data: , vaultwarden_data: } +networks: { proxy: } diff --git a/VPS/configs/docker-daemon.json b/VPS/configs/docker-daemon.json new file mode 100644 index 0000000..b523bd4 --- /dev/null +++ b/VPS/configs/docker-daemon.json @@ -0,0 +1 @@ +{ "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" } } diff --git a/VPS/configs/fail2ban-jail.local b/VPS/configs/fail2ban-jail.local new file mode 100644 index 0000000..ac96507 --- /dev/null +++ b/VPS/configs/fail2ban-jail.local @@ -0,0 +1,10 @@ +# /etc/fail2ban/jail.local +[DEFAULT] +banaction = ufw +bantime = 1h +findtime = 10m +maxretry = 5 + +[sshd] +enabled = true +backend = systemd diff --git a/VPS/configs/rclone-remotes.md b/VPS/configs/rclone-remotes.md new file mode 100644 index 0000000..2d8dada --- /dev/null +++ b/VPS/configs/rclone-remotes.md @@ -0,0 +1,15 @@ +# rclone remotes — /root/.config/rclone/rclone.conf +Секреты (токен Google Drive, obscured-пароль SMB) — в самом rclone.conf на VPS, здесь не приводятся. + +## gdrive (Google Drive) +- type: drive, scope: drive.file (видит только свои файлы) +- путь бэкапов: `gdrive:vaultwarden-backups/` +- (пере)авторизация: на машине с браузером `rclone config` (auto-config), затем скопировать + `~/.config/rclone/rclone.conf` на VPS в `/root/.config/rclone/`. + +## homesmb (домашний SMB через WG) +- type: smb, host: 172.16.0.1, user: only_vps, pass: зашифрован (--obscure) +- путь: `"homesmb:TOSHIBA EXT/vaultwarden-backups/"` (кавычки из-за пробела в имени шары) +- пересоздать: + `rclone config create homesmb smb host 172.16.0.1 user only_vps pass "<пароль>" --obscure` +- зависит от поднятого туннеля wg0. diff --git a/VPS/configs/sshd-00-hardening.conf b/VPS/configs/sshd-00-hardening.conf new file mode 100644 index 0000000..88fd733 --- /dev/null +++ b/VPS/configs/sshd-00-hardening.conf @@ -0,0 +1,6 @@ +# /etc/ssh/sshd_config.d/00-hardening.conf +# Префикс 00- намеренно: sshd берёт первое значение, перебивает cloud-init. +PasswordAuthentication no +KbdInteractiveAuthentication no +PermitRootLogin prohibit-password +PubkeyAuthentication yes diff --git a/VPS/configs/wg0.conf b/VPS/configs/wg0.conf new file mode 100644 index 0000000..c1545fc --- /dev/null +++ b/VPS/configs/wg0.conf @@ -0,0 +1,12 @@ +# /etc/wireguard/wg0.conf (приватный ключ и endpoint скрыты — реальные в файле на VPS) +[Interface] +PrivateKey = <в файле на VPS> +Address = 10.0.0.5/32 + +[Peer] +PublicKey = <публичный ключ WG-сервера Keenetic> +Endpoint = 91.122.209.x:<порт> +AllowedIPs = 10.0.0.0/24, 172.16.0.0/24 +PersistentKeepalive = 25 +# VPS в туннеле 10.0.0.5; Keenetic 10.0.0.1; домашняя LAN/SMB 172.16.0.0/24 (хост 172.16.0.1) +# systemd: wg-quick@wg0 enabled (автоподъём при загрузке) diff --git a/VPS/scripts/vw-backup.sh b/VPS/scripts/vw-backup.sh new file mode 100644 index 0000000..2a12285 --- /dev/null +++ b/VPS/scripts/vw-backup.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# = /usr/local/bin/vw-backup.sh (права 700). Запуск по systemd-таймеру vw-backup.timer (03:30 MSK). +set -euo pipefail + +VOL="/var/lib/docker/volumes/core_vaultwarden_data/_data" +DEST="/opt/backups/vaultwarden" +KEEP=14 +TS="$(date +%Y%m%d-%H%M%S)" +WORK="$(mktemp -d)" +trap 'rm -rf "$WORK"' EXIT + +mkdir -p "$DEST" + +sqlite3 "$VOL/db.sqlite3" ".timeout 10000" ".backup '$WORK/db.sqlite3'" + +cp -a "$VOL"/rsa_key* "$WORK"/ 2>/dev/null || true +cp -a "$VOL"/config.json "$WORK"/ 2>/dev/null || true +cp -a "$VOL"/attachments "$WORK"/ 2>/dev/null || true +cp -a "$VOL"/sends "$WORK"/ 2>/dev/null || true + +ARCHIVE="$DEST/vaultwarden-$TS.tar.gz" +tar -czf "$ARCHIVE" -C "$WORK" . + +ls -1t "$DEST"/vaultwarden-*.tar.gz | tail -n +$((KEEP+1)) | xargs -r rm -f + +# офсайт 1: Google Drive (обязательный) +rclone copy "$ARCHIVE" gdrive:vaultwarden-backups/ +rclone delete --min-age 90d gdrive:vaultwarden-backups/ 2>/dev/null || true + +# офсайт 2: домашний SMB через WG (best-effort) +if rclone copy "$ARCHIVE" "homesmb:TOSHIBA EXT/vaultwarden-backups/" 2>/dev/null; then + rclone delete --min-age 90d "homesmb:TOSHIBA EXT/vaultwarden-backups/" 2>/dev/null || true + SMB="ok" +else + SMB="недоступен" +fi + +echo "OK: $ARCHIVE ($(du -h "$ARCHIVE" | cut -f1)) -> локально + gdrive + smb:$SMB"