2026-04-12 00:32:34 +03:00
2026-04-12 00:32:34 +03:00

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):

# 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. 📜 Создание скрипта синхронизации

sudo mkdir -p /opt/npm/scripts
sudo nano /opt/npm/scripts/sync_ssl_to_ha.sh

Вставьте следующий код:

#!/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. 🔐 Настройка прав и логирования

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

sudo crontab -e

Добавьте строку:

# Ежедневно в 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:

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 вручную:

ssh root@192.168.31.10 "ha core restart"

🧪 Тестирование

  1. Ручной запуск:

    sudo /opt/npm/scripts/sync_ssl_to_ha.sh
    
  2. Проверка лога:

    tail -n 20 /var/log/ha_ssl_sync.log
    
  3. Проверка срока действия сертификата на HA:

    ssh root@192.168.31.10 "openssl x509 -noout -dates -in /ssl/fullchain.pem"
    
  4. Валидация в HA:

    • Откройте https://<ваш-домен-или-ip>:443
    • Убедитесь, что браузер не ругается на сертификат
    • В Настройки → Система → Журналы не должно быть ошибок SSL
  5. Принудительный тест синхронизации (временно удалить сертификат на HA):

    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=noyes + ssh-keyscan Защита от MITM даже во внутренней сети
Добавлена валидация сертификата (openssl x509 -checkend) Не синхронизировать просроченный или повреждённый файл
Добавлена проверка соответствия ключа сертификату Исключить ситуацию рассинхронизации файлов в NPM
Добавлена дедупликация по серийному номеру HA не перезапускается, если сертификат не менялся
Добавлена ротация лога (MAX_LOG_LINES) Лог не вырастет до бесконечности
chmod 750chmod 700 Скрипт с путями к ключам должен быть доступен только root
ServerAliveCountMax=3 добавлен в SSH_OPTS Надёжнее при нестабильном туннеле Keenetic
Добавлена заготовка notify_ha() Уведомления в HA при успехе и ошибке — раскомментировать и настроить

📝 Документация подготовлена для размещения в Gitea/Wiki. Версия: 1.1.0 | Обновлено: 2026-04-12
🔗 При изменении архитектуры сети или пути к сертификатам обновите переменные в шапке скрипта.

S
Description
No description provided
Readme
36 KiB