Загрузить файлы в «/»
This commit is contained in:
+311
@@ -0,0 +1,311 @@
|
|||||||
|
### ASSISTANT
|
||||||
|
# 🔄 SSL Sync: Nginx Proxy Manager → Home Assistant
|
||||||
|
|
||||||
|
> 📖 **Описание**
|
||||||
|
> Автоматическая синхронизация Let's Encrypt сертификатов с сервера **Nginx Proxy Manager** в **Home Assistant** через SSH и cron.
|
||||||
|
> Оптимизировано для работы через туннель Keenetic, включает проверку целостности, дедупликацию (перезапуск только при реальном изменении), ротацию лога и безопасный перезапуск HA.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌐 Архитектура решения
|
||||||
|
| Компонент | IP / Путь | Роль |
|
||||||
|
|-----------|-----------|------|
|
||||||
|
| **Nginx Proxy Manager** | `192.168.1.2` | Хост-сервер, хранит сертификаты |
|
||||||
|
| **Home Assistant** | `192.168.31.10` | VM-клиент, принимает сертификаты в `/ssl/` |
|
||||||
|
| **Сеть** | Туннель Keenetic | Обеспечивает маршрутизацию между подсетями `192.168.1.0/24` и `192.168.31.0/24` |
|
||||||
|
| **Цель** | `/opt/npm/letsencrypt/live/npm-44/` → `root@192.168.31.10:/ssl/` | Ежедневная авто-синхронизация |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Требования
|
||||||
|
- Доступ к **хосту NPM** по SSH (скрипт выполняется в базовой ОС, а не в контейнере)
|
||||||
|
- SSH-доступ к HA от пользователя `root` (включен в HA OS или через Add-on `SSH & Web Terminal`)
|
||||||
|
- Установленные `openssh-client`, `cron`, `bash`, `openssl` на хосте NPM
|
||||||
|
- В `configuration.yaml` HA указана папка `/ssl` для сертификатов
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠 Пошаговая настройка
|
||||||
|
|
||||||
|
### 1. 🔑 Настройка беспарольного SSH
|
||||||
|
|
||||||
|
Выполните на **хосте NPM** (`192.168.1.2`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Генерация ключа (если отсутствует)
|
||||||
|
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_npm_ha -N "" -C "npm-to-ha"
|
||||||
|
|
||||||
|
# 2. Копирование публичного ключа на HA
|
||||||
|
ssh-copy-id -i ~/.ssh/id_ed25519_npm_ha.pub root@192.168.31.10
|
||||||
|
|
||||||
|
# 3. Фиксируем fingerprint хоста (вместо StrictHostKeyChecking=no)
|
||||||
|
ssh-keyscan -H 192.168.31.10 >> ~/.ssh/known_hosts
|
||||||
|
|
||||||
|
# 4. Проверка подключения
|
||||||
|
ssh -i ~/.ssh/id_ed25519_npm_ha root@192.168.31.10 echo "✅ SSH OK"
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ **Почему шаг 3 важен:** `StrictHostKeyChecking=no` отключает защиту от MITM-атак даже внутри LAN. Однократный `ssh-keyscan` безопаснее и не требует интерактивного подтверждения при каждом запуске.
|
||||||
|
|
||||||
|
> ⚠️ **HA OS**: Если используется Home Assistant OS, убедитесь, что SSH включён через `Settings → Add-ons → SSH & Web Terminal`, а ключ добавлен в поле `Authorized Keys`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 📜 Создание скрипта синхронизации
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo mkdir -p /opt/npm/scripts
|
||||||
|
sudo nano /opt/npm/scripts/sync_ssl_to_ha.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Вставьте следующий код:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# === КОНФИГУРАЦИЯ ===
|
||||||
|
FULLCHAIN="/opt/npm/letsencrypt/live/npm-44/fullchain.pem"
|
||||||
|
PRIVKEY="/opt/npm/letsencrypt/live/npm-44/privkey.pem"
|
||||||
|
HA_USER="root"
|
||||||
|
HA_IP="192.168.31.10"
|
||||||
|
DEST_DIR="/ssl"
|
||||||
|
SSH_KEY="/root/.ssh/id_ed25519_npm_ha"
|
||||||
|
LOG="/var/log/ha_ssl_sync.log"
|
||||||
|
MAX_LOG_LINES=500 # Ротация: хранить не более N строк лога
|
||||||
|
|
||||||
|
# Флаги для уведомлений (раскомментируйте и настройте при необходимости)
|
||||||
|
# HA_TOKEN="your_long_lived_token"
|
||||||
|
# HA_NOTIFY_URL="http://192.168.31.10:8123/api/services/notify/persistent_notification"
|
||||||
|
|
||||||
|
SSH_OPTS="-i $SSH_KEY -o StrictHostKeyChecking=yes -o ConnectTimeout=10 -o ServerAliveInterval=5 -o ServerAliveCountMax=3"
|
||||||
|
|
||||||
|
# === ФУНКЦИИ ===
|
||||||
|
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') | $1" >> "$LOG"; }
|
||||||
|
|
||||||
|
rotate_log() {
|
||||||
|
local lines
|
||||||
|
lines=$(wc -l < "$LOG" 2>/dev/null || echo 0)
|
||||||
|
if [[ $lines -gt $MAX_LOG_LINES ]]; then
|
||||||
|
tail -n "$MAX_LOG_LINES" "$LOG" > "${LOG}.tmp" && mv "${LOG}.tmp" "$LOG"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_cert() {
|
||||||
|
# Проверяем, что fullchain содержит валидный PEM-сертификат и не просрочен
|
||||||
|
if ! openssl x509 -in "$FULLCHAIN" -noout -checkend 0 2>/dev/null; then
|
||||||
|
log "❌ Сертификат невалиден или уже просрочен: $FULLCHAIN"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
# Проверяем соответствие ключа сертификату
|
||||||
|
local cert_mod privkey_mod
|
||||||
|
cert_mod=$(openssl x509 -noout -modulus -in "$FULLCHAIN" 2>/dev/null | md5sum)
|
||||||
|
privkey_mod=$(openssl rsa -noout -modulus -in "$PRIVKEY" 2>/dev/null | md5sum)
|
||||||
|
if [[ "$cert_mod" != "$privkey_mod" ]]; then
|
||||||
|
log "❌ Приватный ключ не соответствует сертификату!"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
cert_changed() {
|
||||||
|
# Получаем серийный номер текущего сертификата на HA и сравниваем с локальным
|
||||||
|
local local_serial ha_serial
|
||||||
|
local_serial=$(openssl x509 -noout -serial -in "$FULLCHAIN" 2>/dev/null | cut -d= -f2)
|
||||||
|
ha_serial=$(ssh $SSH_OPTS "$HA_USER@$HA_IP" \
|
||||||
|
"openssl x509 -noout -serial -in $DEST_DIR/fullchain.pem 2>/dev/null | cut -d= -f2" 2>/dev/null)
|
||||||
|
[[ "$local_serial" != "$ha_serial" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# notify_ha() {
|
||||||
|
# curl -s -X POST "$HA_NOTIFY_URL" \
|
||||||
|
# -H "Authorization: Bearer $HA_TOKEN" \
|
||||||
|
# -H "Content-Type: application/json" \
|
||||||
|
# -d "{\"message\": \"$1\", \"title\": \"SSL Sync\"}" > /dev/null
|
||||||
|
# }
|
||||||
|
|
||||||
|
# === ОСНОВНАЯ ЛОГИКА ===
|
||||||
|
rotate_log
|
||||||
|
log "=== Старт синхронизации ==="
|
||||||
|
|
||||||
|
# 1. Проверка исходных файлов
|
||||||
|
if [[ ! -f "$FULLCHAIN" || ! -f "$PRIVKEY" ]]; then
|
||||||
|
log "❌ Исходные сертификаты не найдены: $FULLCHAIN или $PRIVKEY"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Валидация сертификата перед копированием
|
||||||
|
if ! validate_cert; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 3. Проверка: изменился ли сертификат?
|
||||||
|
if ! cert_changed; then
|
||||||
|
log "✅ Сертификат не изменился — синхронизация не требуется."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "🆕 Обнаружен новый сертификат, начинаем синхронизацию..."
|
||||||
|
|
||||||
|
# 4. Копирование файлов
|
||||||
|
scp $SSH_OPTS "$FULLCHAIN" "$HA_USER@$HA_IP:$DEST_DIR/fullchain.pem" >> "$LOG" 2>&1
|
||||||
|
SCP1=$?
|
||||||
|
scp $SSH_OPTS "$PRIVKEY" "$HA_USER@$HA_IP:$DEST_DIR/privkey.pem" >> "$LOG" 2>&1
|
||||||
|
SCP2=$?
|
||||||
|
|
||||||
|
# 5. Перезапуск только при успешном копировании обоих файлов
|
||||||
|
if [[ $SCP1 -eq 0 && $SCP2 -eq 0 ]]; then
|
||||||
|
log "✅ Файлы успешно скопированы"
|
||||||
|
ssh $SSH_OPTS "$HA_USER@$HA_IP" "chmod 600 $DEST_DIR/privkey.pem && ha core restart" >> "$LOG" 2>&1
|
||||||
|
log "🔄 Команда рестарта HA отправлена"
|
||||||
|
# notify_ha "Сертификат обновлён и HA перезапущен"
|
||||||
|
else
|
||||||
|
log "❌ Ошибка копирования (fullchain=$SCP1, privkey=$SCP2). Рестарт отменён."
|
||||||
|
# notify_ha "⚠️ Ошибка синхронизации SSL — рестарт HA отменён"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 🔐 Настройка прав и логирования
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo chmod +x /opt/npm/scripts/sync_ssl_to_ha.sh
|
||||||
|
sudo touch /var/log/ha_ssl_sync.log
|
||||||
|
sudo chown root:root /opt/npm/scripts/sync_ssl_to_ha.sh
|
||||||
|
sudo chmod 700 /opt/npm/scripts/sync_ssl_to_ha.sh # только root, без лишних прав
|
||||||
|
```
|
||||||
|
|
||||||
|
> ℹ️ `chmod 700` вместо `750` — скрипт содержит путь к SSH-ключу и должен быть доступен только владельцу.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. ⏰ Автоматизация через Cron
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo crontab -e
|
||||||
|
```
|
||||||
|
|
||||||
|
Добавьте строку:
|
||||||
|
```cron
|
||||||
|
# Ежедневно в 04:15 (через 15 мин после планового обновления Let's Encrypt в NPM)
|
||||||
|
15 4 * * * /bin/bash /opt/npm/scripts/sync_ssl_to_ha.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
> 💡 **Почему 04:15?** NPM запускает проверку обновления сертификатов в `04:00`. Задержка в 15 минут гарантирует, что свежие файлы уже записаны на диск.
|
||||||
|
|
||||||
|
> 💡 **Альтернатива:** если NPM поддерживает deploy-хуки (например через `post-renewal`), можно запускать скрипт напрямую по событию обновления, а не по расписанию — это надёжнее.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. ⚙️ Конфигурация Home Assistant
|
||||||
|
|
||||||
|
В `configuration.yaml` убедитесь, что указан путь к `/ssl`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
http:
|
||||||
|
ssl_certificate: /ssl/fullchain.pem
|
||||||
|
ssl_key: /ssl/privkey.pem
|
||||||
|
# server_port: 443 # Раскомментируйте, если HA слушает 443 напрямую
|
||||||
|
use_x_forwarded_for: true
|
||||||
|
trusted_proxies:
|
||||||
|
- 192.168.1.2
|
||||||
|
- 127.0.0.1
|
||||||
|
```
|
||||||
|
|
||||||
|
После первого изменения перезапустите HA вручную:
|
||||||
|
```bash
|
||||||
|
ssh root@192.168.31.10 "ha core restart"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Тестирование
|
||||||
|
|
||||||
|
1. **Ручной запуск:**
|
||||||
|
```bash
|
||||||
|
sudo /opt/npm/scripts/sync_ssl_to_ha.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Проверка лога:**
|
||||||
|
```bash
|
||||||
|
tail -n 20 /var/log/ha_ssl_sync.log
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Проверка срока действия сертификата на HA:**
|
||||||
|
```bash
|
||||||
|
ssh root@192.168.31.10 "openssl x509 -noout -dates -in /ssl/fullchain.pem"
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Валидация в HA:**
|
||||||
|
- Откройте `https://<ваш-домен-или-ip>:443`
|
||||||
|
- Убедитесь, что браузер не ругается на сертификат
|
||||||
|
- В `Настройки → Система → Журналы` не должно быть ошибок SSL
|
||||||
|
|
||||||
|
5. **Принудительный тест синхронизации** (временно удалить сертификат на HA):
|
||||||
|
```bash
|
||||||
|
ssh root@192.168.31.10 "rm /ssl/fullchain.pem"
|
||||||
|
sudo /opt/npm/scripts/sync_ssl_to_ha.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Устранение неполадок
|
||||||
|
|
||||||
|
| Симптом | Причина | Решение |
|
||||||
|
|--------|---------|---------|
|
||||||
|
| `Permission denied` при `scp` | Ключ не добавлен или неправильные права | Проверьте `~/.ssh/authorized_keys` на HA и `chmod 600 ~/.ssh/id_ed25519_npm_ha` |
|
||||||
|
| `Host key verification failed` | Fingerprint не добавлен | Выполните `ssh-keyscan -H 192.168.31.10 >> ~/.ssh/known_hosts` на хосте NPM |
|
||||||
|
| `❌ Приватный ключ не соответствует` | Рассинхронизация файлов в NPM | Проверьте целостность файлов в `/opt/npm/letsencrypt/live/npm-44/` |
|
||||||
|
| `ha: command not found` | CLI не в PATH или не установлен | Используйте `docker restart homeassistant` или `systemctl restart home-assistant@homeassistant` |
|
||||||
|
| HA не стартует после синхронизации | Неправильные права на `privkey.pem` | Скрипт уже делает `chmod 600`. Если ошибка — проверьте, не перезаписывает ли файл Supervisor |
|
||||||
|
| Cron не запускает скрипт | Отсутствие полных путей или переменных окружения | В cron всегда указывайте `/bin/bash /полный/путь/скрипт.sh` |
|
||||||
|
| Таймаут по SSH | Нестабильный туннель Keenetic | Параметры `ConnectTimeout=10`, `ServerAliveInterval=5`, `ServerAliveCountMax=3` уже заданы в `SSH_OPTS` |
|
||||||
|
| Лог растёт бесконечно | Нет ротации | Ротация встроена в скрипт (`MAX_LOG_LINES`). Также можно добавить `logrotate` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔒 Безопасность и рекомендации
|
||||||
|
|
||||||
|
- ✅ Ключ SSH хранится с правами `600`, скрипт — с правами `700`. Не передавайте их третьим лицам.
|
||||||
|
- ✅ `StrictHostKeyChecking=yes` — fingerprint HA зафиксирован в `known_hosts`, MITM-атаки невозможны.
|
||||||
|
- ✅ Скрипт проверяет валидность и соответствие ключа сертификату **до** копирования.
|
||||||
|
- ✅ Перезапуск HA происходит **только** при успешном копировании обоих файлов **и** только при реальном изменении сертификата.
|
||||||
|
- ✅ Встроенная ротация лога — файл не вырастет бесконечно.
|
||||||
|
- 🔔 Для мониторинга раскомментируйте блок `notify_ha` и добавьте `HA_TOKEN` — будете получать уведомления прямо в интерфейсе HA (или настройте Telegram через `notify.telegram`).
|
||||||
|
- 🔄 Раз в квартал проверяйте актуальность ключа SSH и ротацию `authorized_keys` на HA.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Структура файлов
|
||||||
|
|
||||||
|
```
|
||||||
|
/opt/npm/scripts/
|
||||||
|
└── sync_ssl_to_ha.sh # Основной скрипт
|
||||||
|
/root/.ssh/
|
||||||
|
├── id_ed25519_npm_ha # Приватный SSH-ключ (chmod 600)
|
||||||
|
├── id_ed25519_npm_ha.pub # Публичный ключ
|
||||||
|
└── known_hosts # Fingerprint HA (StrictHostKeyChecking=yes)
|
||||||
|
/var/log/
|
||||||
|
└── ha_ssl_sync.log # Лог операций (авторотация)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Сводка изменений относительно v1.0.0
|
||||||
|
|
||||||
|
| Что изменилось | Почему |
|
||||||
|
|----------------|--------|
|
||||||
|
| `StrictHostKeyChecking=no` → `yes` + `ssh-keyscan` | Защита от MITM даже во внутренней сети |
|
||||||
|
| Добавлена валидация сертификата (`openssl x509 -checkend`) | Не синхронизировать просроченный или повреждённый файл |
|
||||||
|
| Добавлена проверка соответствия ключа сертификату | Исключить ситуацию рассинхронизации файлов в NPM |
|
||||||
|
| Добавлена дедупликация по серийному номеру | HA не перезапускается, если сертификат не менялся |
|
||||||
|
| Добавлена ротация лога (`MAX_LOG_LINES`) | Лог не вырастет до бесконечности |
|
||||||
|
| `chmod 750` → `chmod 700` | Скрипт с путями к ключам должен быть доступен только `root` |
|
||||||
|
| `ServerAliveCountMax=3` добавлен в `SSH_OPTS` | Надёжнее при нестабильном туннеле Keenetic |
|
||||||
|
| Добавлена заготовка `notify_ha()` | Уведомления в HA при успехе и ошибке — раскомментировать и настроить |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
📝 *Документация подготовлена для размещения в Gitea/Wiki. Версия: `1.1.0` | Обновлено: `2026-04-12`*
|
||||||
|
🔗 *При изменении архитектуры сети или пути к сертификатам обновите переменные в шапке скрипта.*
|
||||||
Reference in New Issue
Block a user