Получение и распостранение сертификатов с VPS на Proxmox, Home Assistant и пр.

1. Получение сертификата Let’s Encrypt на VPS

1.1. Установка Certbot

Certbot — официальный клиент Let’s Encrypt. Установим его на VPS с публичным IP:

sudo apt update && sudo apt install certbot -y

1.2. Выпуск сертификата

Проще всего использовать HTTP-валидацию (если домен привязан к VPS):

sudo certbot certonly --standalone -d example.com

Если домен не привязан к VPS, подойдёт DNS-валидация (например, через Cloudflare API):

sudo certbot certonly --dns-cloudflare --dns-cloudflare-credentials ~/.secrets/cloudflare.ini -d example.com

Сертификаты сохранятся в /etc/letsencrypt/live/example.com/.

Автоматическое продление

Certbot по умолчанию настроен на автоматическое продление сертификатов Let’s Encrypt.

Как работает автоматическое продление?

  1. Срок действия сертификата Let’s Encrypt — 90 дней, но Certbot обычно пытается продлевать его каждые 60 дней (или чаще).
  2. Автоматическое обновление реализовано через systemd/cron, в зависимости от системы.
    • В большинстве случаев Certbot добавляет задание в cron (или systemd timer), которое запускает certbot renew.
    • Проверить наличие задания можно командой: sudo crontab -l | grep certbot или для systemd: systemctl list-timers | grep certbot
  3. Команда certbot renew проверяет срок действия сертификатов и продлевает только те, которые истекают в течение 30 дней.

Как убедиться, что продление работает?

Вручную запустить пробное обновление (без реального продления):

sudo certbot renew --dry-run

Если тест проходит успешно, значит автоматическое обновление настроено правильно.

Проверить логи последнего обновления

sudo grep certbot /var/log/syslog

или

sudo journalctl -u certbot

Что делать, если автоматическое продление не настроено?

Если cron или systemd timer не добавлены, можно вручную создать задание (например, в cron):

sudo crontab -e

Добавить строку (запуск каждые 12 часов + логирование):

0 */12 * * * /usr/bin/certbot renew --quiet --post-hook "systemctl reload nginx"

(Замените nginx на ваш веб-сервер, если используете Apache или другой.)

Важно!

  • Убедитесь, что веб-сервер перезагружается после обновления (используйте --post-hook).
  • Если сервер недоступен на 80/443 портах в момент обновления, продление может не сработать.

Если всё настроено правильно, Certbot будет автоматически продлевать сертификаты без вашего вмешательства. 🚀

2. Распространение сертификата на другие серверы

2.1. Ручное копирование (разовый способ)

Сертификаты можно скопировать вручную через scp:

scp -P SSH_PORT /etc/letsencrypt/live/example.com/* user@remote_server:/etc/ssl/proxmox/

Но это неудобно при частых обновлениях.

2.2. Автоматизация через Certbot Hooks

Certbot поддерживает хуки (--deploy-hook), которые выполняются после обновления сертификата.

Создадим скрипт /etc/letsencrypt/renewal-hooks/deploy/copy_certs.sh:

#!/bin/bash
DOMAIN="example.com"
REMOTE_USER="user"
REMOTE_SERVERS=("server1" "server2")
SSH_PORT="22"

for server in "${REMOTE_SERVERS[@]}"; do
  rsync -avz -e "ssh -p $SSH_PORT" \
    /etc/letsencrypt/live/$DOMAIN/privkey.pem \
    /etc/letsencrypt/live/$DOMAIN/fullchain.pem \
    $REMOTE_USER@$server:/etc/ssl/proxmox/
  
  ssh -p $SSH_PORT $REMOTE_USER@$server "systemctl reload nginx"
done

Даём права на выполнение:

chmod +x /etc/letsencrypt/renewal-hooks/deploy/copy_certs.sh

Теперь при каждом обновлении (certbot renew) сертификаты будут автоматически копироваться на указанные серверы.


3. Масштабируемое решение: Ansible

Если серверов много, ручное управление становится сложным. Ansible позволяет автоматизировать процесс централизованно.

3.1. Установка Ansible

На VPS:

sudo apt install ansible -y

3.2. Создание инвентаря (inventory.ini)

[proxmox]
proxmox1 ansible_host=IP_1 ansible_port=SSH_PORT ansible_user=user
proxmox2 ansible_host=IP_2 ansible_port=SSH_PORT ansible_user=user
[homeassistantos]
haos1 ansible_host=IP_1 ansible_port=SSH_PORT ansible_user=user
haos2 ansible_host=IP_2 ansible_port=SSH_PORT ansible_user=user

3.3. Playbook для развёртывания сертификатов (deploy_certs.yml)

Замените you.domain.com на свой

---
- name: Deploy Let's Encrypt certificates to remote hosts
  hosts: all
  become: true
  gather_facts: false
  vars:
    local_cert_dir: "/etc/letsencrypt/live/example.duckdns.org"
    group_proxmox: proxmox
    group_haos: homeassistantos

  tasks:

    - name: Set variables for Proxmox
      set_fact:
        cert_target_dir: "/etc/pve/local"
        cert_fullchain_name: "pveproxy-ssl.pem"
        cert_privkey_name: "pveproxy-ssl.key"
        cert_reload_command: "systemctl restart pveproxy"
      when: inventory_hostname in groups[group_proxmox]

    - name: Set variables for Home Assistant OS
      set_fact:
        cert_target_dir: "/ssl"
        cert_fullchain_name: "fullchain.pem"
        cert_privkey_name: "privkey.pem"
        cert_reload_command: "ha core restart"
      when: inventory_hostname in groups[group_haos]

    - name: Copy fullchain.pem to Proxmox
      copy:
        src: "{{ local_cert_dir }}/fullchain.pem"
        dest: "{{ cert_target_dir }}/{{ cert_fullchain_name }}"
        force: true
      when: inventory_hostname in groups[group_proxmox]

    - name: Copy privkey.pem to Proxmox
      copy:
        src: "{{ local_cert_dir }}/privkey.pem"
        dest: "{{ cert_target_dir }}/{{ cert_privkey_name }}"
        force: true
      when: inventory_hostname in groups[group_proxmox]

    - name: Restart Proxmox service
      shell: "{{ cert_reload_command }}"
      when: inventory_hostname in groups[group_proxmox]

    - name: Ensure HAOS SSL directory exists
      shell: "ssh -p {{ ansible_port }} root@localhost 'mkdir -p {{ cert_target_dir }}'"
      delegate_to: localhost
      when: inventory_hostname in groups[group_haos]

    - name: Copy fullchain.pem to HAOS
      shell: "scp -P {{ ansible_port }} {{ local_cert_dir }}/fullchain.pem root@localhost:{{ cert_target_dir }}/{{ cert_fullchain_name }}"
      delegate_to: localhost
      when: inventory_hostname in groups[group_haos]

    - name: Copy privkey.pem to HAOS
      shell: "scp -P {{ ansible_port }} {{ local_cert_dir }}/privkey.pem root@localhost:{{ cert_target_dir }}/{{ cert_privkey_name }}"
      delegate_to: localhost
      when: inventory_hostname in groups[group_haos]

    - name: Restart Home Assistant
      shell: "ssh -p {{ ansible_port }} root@localhost '{{ cert_reload_command }}'"
      delegate_to: localhost
      when: inventory_hostname in groups[group_haos]

3.4. Запуск Playbook

ansible-playbook -i inventory.ini deploy_certs.yml

3.5. Интеграция с Certbot

Добавим вызов Ansible в хук Certbot (/etc/letsencrypt/renewal-hooks/deploy/ansible_deploy.sh):

#!/bin/bash
ansible-playbook -i /path/to/inventory.ini /path/to/deploy_certs.yml

Теперь при каждом обновлении сертификата (certbot renew) Ansible будет разворачивать его на всех серверах.


4. Альтернативные методы

4.1. Общее хранилище (NFS/S3)

Если серверы имеют доступ к одному хранилищу, можно настроить синхронизацию:

rclone copy /etc/letsencrypt/live/example.com/ s3:bucket/certs/

4.2. Использование ACME-клиента на каждой машине

Если синхронизация невозможна, можно настроить certbot или acme.sh на каждом сервере с DNS-валидацией.


Заключение

  1. Самый простой способ — certbot + deploy-hook с rsync/scp.
  2. Для масштабируемости — Ansible.
  3. Для распределённых систем — общее хранилище (S3/NFS).

Автоматизация экономит время и снижает риск ошибок. Настроив один раз, вы забудете о ручном управлении сертификатами!

Ещё варианты Playbook

Упрощённый Playbook для Proxmox (без валидации):

---
- name: Deploy SSL certificates
  hosts: proxmox
  become: yes
  vars:
    cert_domain: "you.domain.com"

  tasks:
    - name: Get Proxmox node name
      command: hostname
      register: proxmox_node
      changed_when: false

    - name: Install certificates (atomic method)
      shell: |
        install -m 644 -o root "/etc/letsencrypt/live/{{ cert_domain }}/fullchain.pem" "/tmp/fullchain.pem" &&
        install -m 600 -o root "/etc/letsencrypt/live/{{ cert_domain }}/privkey.pem" "/tmp/privkey.pem" &&
        cp -a "/tmp/fullchain.pem" "/etc/pve/nodes/{{ proxmox_node.stdout }}/pveproxy-ssl.pem" &&
        cp -a "/tmp/privkey.pem" "/etc/pve/nodes/{{ proxmox_node.stdout }}/pveproxy-ssl.key" &&
        chown root:www-data "/etc/pve/nodes/{{ proxmox_node.stdout }}/pveproxy-ssl.*"
      args:
        executable: /bin/bash

    - name: Restart PVE services
      systemctl:
        name: "{{ item }}"
        state: restarted
      loop:
        - pveproxy
        - pvedaemon

Универсальная версия для любых систем:

---
- name: Universal SSL Deploy
  hosts: all
  become: yes
  vars:
    cert_domain: "you.domain.com"
    cert_paths:  # Определяем пути для разных ОС
      proxmox:
        cert: "/etc/pve/nodes/{{ ansible_hostname }}/pveproxy-ssl.pem"
        key: "/etc/pve/nodes/{{ ansible_hostname }}/pveproxy-ssl.key"
        services: ["pveproxy", "pvedaemon"]
      debian:
        cert: "/etc/ssl/certs/{{ cert_domain }}.pem"
        key: "/etc/ssl/private/{{ cert_domain }}.key"
        services: ["nginx"]
      openwrt:
        cert: "/etc/uhttpd.crt"
        key: "/etc/uhttpd.key"
        services: ["uhttpd"]
      homeassistant:
        cert: "/ssl/fullchain.pem"  # Для HA в Docker
        key: "/ssl/privkey.pem"
        services: ["home-assistant"]

  tasks:
    - name: Deploy certificates
      block:
        - name: Copy certificate
          copy:
            src: "/etc/letsencrypt/live/{{ cert_domain }}/fullchain.pem"
            dest: "{{ cert_paths[ansible_os_family | lower].cert | default(cert_paths['debian'].cert) }}"
            mode: '0644'
            owner: root
            group: root

        - name: Copy private key
          copy:
            src: "/etc/letsencrypt/live/{{ cert_domain }}/privkey.pem"
            dest: "{{ cert_paths[ansible_os_family | lower].key | default(cert_paths['debian'].key) }}"
            mode: '0600'
            owner: root
            group: root
      when: "'proxmox' not in ansible_os_family | lower"

    - name: Special handling for Proxmox
      shell: |
        install -m 644 -o root "/etc/letsencrypt/live/{{ cert_domain }}/fullchain.pem" "/tmp/fullchain.pem" &&
        install -m 600 -o root "/etc/letsencrypt/live/{{ cert_domain }}/privkey.pem" "/tmp/privkey.pem" &&
        cp -a "/tmp/fullchain.pem" "{{ cert_paths['proxmox'].cert }}" &&
        cp -a "/tmp/privkey.pem" "{{ cert_paths['proxmox'].key }}" &&
        chown root:www-data "{{ cert_paths['proxmox'].cert }}" "{{ cert_paths['proxmox'].key }}"
      args:
        executable: /bin/bash
      when: "'proxmox' in ansible_os_family | lower"

    - name: Restart services
      systemd:
        name: "{{ item }}"
        state: restarted
      loop: "{{ cert_paths[ansible_os_family | lower].services | default(cert_paths['debian'].services) }}"