====== DNS, TLS и рецепт деплоя ====== Как ''*.melnoff.com'' раскрывается в интернет и как добавить новый сервис. ===== DNS ===== * **Зона:** ''melnoff.com'' на **Cloudflare** (NS: ''erin.ns.cloudflare.com'', ''west.ns.cloudflare.com''). * **Режим:** все публичные сабдомены — **DNS-only** (grey cloud). Cloudflare proxy выключен. * **Все записи** — A на ''95.165.74.182'' (публичный IP домашнего интернет-канала). * **Что важно:** включение Cloudflare proxy (orange cloud) **сломает** текущую схему ACME — у нас webroot http-01, требующий публично доступного :80. Если хотите включать proxy — сначала переключить acme.sh на DNS-01 challenge через Cloudflare API. ===== TLS ===== * **CA:** Let's Encrypt. ZeroSSL (default в свежих acme.sh) ушёл за EAB/email — обходим явным ''--server letsencrypt''. * **Утилита:** acme.sh, бинарь ''/usr/lib/acme/client/acme.sh'', ACME home ''/etc/acme/''. * **Тип ключа:** ECDSA-256 (''--keylength ec-256''). * **Challenge:** http-01 webroot, корень ''/var/run/acme/challenge'', обработчик — ''/etc/nginx/conf.d/acme80.conf'' (один общий вhost listen 80, в server_name перечислены все домены). * **Auto-renewal:** через UCI-секцию ''acme'' (''/etc/config/acme''), демон ''/etc/init.d/acme''. ===== Pattern: один сервис — один файл ''.conf'' ===== Все vhost'ы построены по одному шаблону. Пример (''bw.conf''): server { listen 443 ssl; listen [::]:443 ssl; listen 8080; server_name bw.melnoff.com; ssl_certificate /etc/acme/bw.melnoff.com_ecc/fullchain.cer; ssl_certificate_key /etc/acme/bw.melnoff.com_ecc/bw.melnoff.com.key; ssl_session_cache shared:SSL:32k; ssl_session_timeout 64m; location / { proxy_pass http://172.16.10.1:83; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; proxy_read_timeout 300s; } } * **Backend всегда** ''http://172.16.10.1:'' — это IP LXC 107 в VLAN 2. Не ''192.168.0.6''! * **listen 8080** — параллельный non-TLS листен для внутреннего использования. ===== Deploy recipe — добавить новый ''.melnoff.com'' ===== ==== 0. Выбрать порт ==== Свободные на LXC 107 (на 2026-04-27): ''8084''+, ''84-99''. Занятые: 83 (vw), 3000 (gitea), 3001 (grafana), 8000+9443 (portainer), 8082 (zabbix-web), 8083 (dokuwiki), 10051 (zabbix-server), 2244 (gitea-ssh). ==== 1. Cloudflare DNS ==== Добавить A-запись ''.melnoff.com'' → ''95.165.74.182'', **Proxy: DNS-only**. Дождаться пропагации: dig @1.1.1.1 +short .melnoff.com A ==== 2. Контейнер на LXC 107 ==== ssh root@192.168.0.6 pct exec 107 -- /bin/sh mkdir -p /opt/ cat > /opt//docker-compose.yml <: image: restart: unless-stopped ports: - ":80" volumes: - /opt//data:/... environment: TZ: Europe/Moscow YAML cd /opt/ && docker compose up -d ==== 3. ACME challenge handler ==== ssh root@192.168.0.1 # бэкап cp /etc/nginx/conf.d/acme80.conf /etc/nginx/conf.d/acme80.conf.bak.$(date +%s) # добавить домен в server_name sed -i 's/\(server_name [^;]*\)/\1 .melnoff.com/' /etc/nginx/conf.d/acme80.conf # reload /etc/init.d/nginx reload ==== 4. Self-test ACME path (полезно перед issue) ==== mkdir -p /var/run/acme/challenge/.well-known/acme-challenge echo HELLO > /var/run/acme/challenge/.well-known/acme-challenge/test curl -H "Host: .melnoff.com" http://127.0.0.1/.well-known/acme-challenge/test # должен вернуть HTTP 200 и 'HELLO' rm /var/run/acme/challenge/.well-known/acme-challenge/test ==== 5. Issue cert ==== /usr/lib/acme/client/acme.sh --home /etc/acme \ --issue --server letsencrypt \ --webroot /var/run/acme/challenge \ -d .melnoff.com \ --keylength ec-256 ==== 6. UCI запись для auto-renewal ==== uci set acme.=cert uci set acme..enabled=1 uci set acme..staging=0 uci set acme..validation_method=webroot uci set acme..key_type=ec256 uci set acme..domains=.melnoff.com uci commit acme ==== 7. vhost ==== cat > /etc/nginx/conf.d/.conf <.melnoff.com; ssl_certificate /etc/acme/.melnoff.com_ecc/fullchain.cer; ssl_certificate_key /etc/acme/.melnoff.com_ecc/.melnoff.com.key; ssl_session_cache shared:SSL:32k; ssl_session_timeout 64m; location / { proxy_pass http://172.16.10.1:; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; proxy_read_timeout 300s; } } NGINX /etc/init.d/nginx reload ==== 8. Проверка ==== curl -sI https://.melnoff.com/ ===== Известные грабли ===== * **bash в Alpine LXC отсутствует.** ''pct exec 107 -- bash ...'' даст ошибку. Использовать ''/bin/sh''. * **curl в Alpine LXC по умолчанию не установлен.** Поставить: ''apk add --no-cache curl''. * **''nginx -t'' без флагов** не работает на OpenWrt nginx-package (он использует ''/etc/nginx/uci.conf''). Используйте ''/etc/init.d/nginx reload''. Для инспекции — ''nginx -T''. * **acme.sh по умолчанию хочет ZeroSSL** и требует EAB/email. Всегда передавайте ''--server letsencrypt''. * **ash (busybox) не любит круглые скобки** в неэкранированном ''echo''. Берите в кавычки. * **vhost не должен ссылаться на отсутствующий cert** — иначе ''nginx reload'' упадёт. Порядок: acme80 update → reload → issue cert → vhost → reload again. * **При включении Cloudflare proxy** webroot http-01 challenge сломается — нужно перейти на DNS-01.