chore: реализована работа с самоподписными сертификатами

This commit is contained in:
protokey committed 2026-09-26 14:12:01 +04:00
1 parent 9bb3a2fc5d
commit 602199d510
23 files changed
+524 -108

No files matched your search

+13 -3
View File
@@ -2,6 +2,8 @@
#
# Порт ssh меняется в два шага, чтобы не потерять сервер:
# сначала sshd слушает старый и новый порт, после проверки входа по ключу на новом порту старый закрывается.
# Если обновление системы что-то поставило, в конце сервер перезагружается: подхватывает новое ядро и библиотеки,
# заодно проверяется, что ssh и nftables переживают перезагрузку. В контейнере перезагрузки нет.
- name: Prepare fresh server (password login)
hosts: target
@@ -9,12 +11,12 @@
vars:
ansible_password: "{{ login_password }}"
tasks:
- name: Require Debian 13
- name: Require Debian 12 or 13
ansible.builtin.assert:
that:
- ansible_facts.distribution == 'Debian'
- ansible_facts.distribution_major_version == '13'
fail_msg: "Supported only Debian 13, got {{ ansible_facts.distribution }} {{ ansible_facts.distribution_version }}"
- ansible_facts.distribution_major_version in ['12', '13']
fail_msg: "Supported only Debian 12 and 13, got {{ ansible_facts.distribution }} {{ ansible_facts.distribution_version }}"
- name: Create .ssh directory
ansible.builtin.file:
@@ -35,6 +37,7 @@
ansible.builtin.apt:
update_cache: true
upgrade: dist
register: system_upgrade
- ansible.builtin.import_tasks: tasks/firewall.yml
vars:
@@ -70,3 +73,10 @@
- name: Check hardened ssh
ansible.builtin.wait_for_connection:
timeout: 60
- name: Reboot after upgrade
ansible.builtin.reboot:
reboot_timeout: 600
when:
- system_upgrade is changed
- ansible_facts.virtualization_type not in ['docker', 'podman', 'container', 'containerd']
+17 -2
View File
@@ -33,10 +33,25 @@
ansible.builtin.command: sysctl --system
changed_when: false
- name: Remove DNS over TLS config
- name: Remove encrypted DNS config
ansible.builtin.file:
path: /etc/systemd/resolved.conf.d/00-serverus.conf
path: "{{ item }}"
state: absent
loop:
- /etc/systemd/resolved.conf.d/00-serverus.conf
- /etc/systemd/network/90-serverus-dns.netdev
- /etc/systemd/network/90-serverus-dns.network
- name: Remove DNS fallback interface
ansible.builtin.command: ip link delete serverus-dns
register: dns_link
changed_when: dns_link.rc == 0
failed_when: false
- name: Stop dnscrypt-proxy
ansible.builtin.command: systemctl disable --now dnscrypt-proxy.socket dnscrypt-proxy.service
changed_when: false
failed_when: false
# systemd-resolved остается: без него /etc/resolv.conf указывает в никуда
- name: Restart systemd-resolved
+86 -5
View File
@@ -1,10 +1,74 @@
# Системный резолвер: systemd-resolved, только DNS-over-TLS к Cloudflare.
# Строгий режим: без TLS запрос не уйдет, открытым текстом DNS не утекает. Резервных серверов нет.
- name: Install systemd-resolved
# Системный резолвер: systemd-resolved с двумя путями до Cloudflare, оба шифрованные.
#
# Основной - DNS-over-TLS (853), строгий режим: без TLS запрос не уйдет, открытым текстом DNS не утекает.
# Запасной - dnscrypt-proxy с DNS-over-HTTPS (443) на 127.0.2.1, если 853 режут. resolved спрашивает оба пути
# параллельно и берет первый успешный ответ. Режим TLS в resolved один на область, поэтому запасной путь живет
# на отдельном dummy интерфейсе serverus-dns со своими настройками. Резервных серверов в открытом виде нет.
# Адрес интерфейса из TEST-NET (192.0.2.0/24) с глобальной областью: интерфейс только с link-local адресом
# resolved для DNS не использует. Проверено: при закрытом 853 ответ приходит через DoH за доли секунды.
- name: Install DNS packages
ansible.builtin.apt:
name: systemd-resolved
name: [ systemd-resolved, dnscrypt-proxy, bind9-dnsutils ]
state: present
- name: Configure dnscrypt-proxy
ansible.builtin.copy:
dest: /etc/dnscrypt-proxy/dnscrypt-proxy.toml
mode: "0644"
content: |
# слушает сокет dnscrypt-proxy.socket (127.0.2.1:53), списки серверов не качает
listen_addresses = []
server_names = ['cloudflare-1', 'cloudflare-2']
ipv4_servers = true
ipv6_servers = false
doh_servers = true
dnscrypt_servers = false
require_dnssec = false
require_nolog = false
require_nofilter = false
cache = true
[static]
[static.'cloudflare-1']
stamp = 'sdns://AgMAAAAAAAAABzEuMS4xLjEAEmNsb3VkZmxhcmUtZG5zLmNvbQovZG5zLXF1ZXJ5'
[static.'cloudflare-2']
stamp = 'sdns://AgMAAAAAAAAABzEuMC4wLjEAEmNsb3VkZmxhcmUtZG5zLmNvbQovZG5zLXF1ZXJ5'
register: dnscrypt_config
- name: Restart dnscrypt-proxy
ansible.builtin.systemd_service:
name: dnscrypt-proxy
enabled: true
state: restarted
when: dnscrypt_config is changed
- name: Create dummy interface for DNS fallback
ansible.builtin.copy:
dest: /etc/systemd/network/90-serverus-dns.netdev
mode: "0644"
content: |
[NetDev]
Name=serverus-dns
Kind=dummy
- name: Route fallback DNS through dnscrypt-proxy
ansible.builtin.copy:
dest: /etc/systemd/network/90-serverus-dns.network
mode: "0644"
content: |
[Match]
Name=serverus-dns
[Network]
Address=192.0.2.53/32
LinkLocalAddressing=no
ConfigureWithoutCarrier=yes
DNS=127.0.2.1
DNSOverTLS=no
DNSSEC=no
Domains=~.
DNSDefaultRoute=yes
- name: Create resolved drop-in directory
ansible.builtin.file:
path: /etc/systemd/resolved.conf.d
@@ -21,6 +85,8 @@
FallbackDNS=
DNSOverTLS=yes
Domains=~.
LLMNR=no
MulticastDNS=no
- name: Use resolved stub
ansible.builtin.file:
@@ -29,16 +95,31 @@
state: link
force: true
# networkd управляет только serverus-dns: остальные интерфейсы без .network файлов он не трогает
- name: Start systemd-networkd
ansible.builtin.systemd_service:
name: systemd-networkd
enabled: true
state: restarted
- name: Restart systemd-resolved
ansible.builtin.systemd_service:
name: systemd-resolved
enabled: true
state: restarted
- name: Check DNS over TLS
- name: Check DNS
ansible.builtin.command: resolvectl query deb.debian.org
register: dns_check
retries: 5
delay: 3
until: dns_check.rc == 0
changed_when: false
- name: Check DNS over HTTPS fallback
ansible.builtin.command: dig +short +time=5 @127.0.2.1 deb.debian.org
register: doh_check
retries: 5
delay: 3
until: doh_check.rc == 0 and doh_check.stdout != ''
changed_when: false
+1 -1
View File
@@ -10,7 +10,7 @@
no_log: true
- name: Read panel address
ansible.builtin.command: "{{ xui_bin }} setting -show true"
ansible.builtin.command: "{{ xui_bin }} setting -show"
register: xui_panel_settings
changed_when: false
no_log: true
+67 -41
View File
@@ -1,57 +1,83 @@
# vars: cert_host - домен или ip, на который выпускается сертификат
# vars: cert_host - домен или ip, на который выпускается сертификат;
# cert_fullchain, cert_private_key (optional) - готовый сертификат, например от своего CA
#
# Сертификат Let's Encrypt через acme.sh в режиме standalone: проверка HTTP-01 на 80 порту, он должен быть открыт.
# На ip выдается только короткий сертификат (~6 дней), acme.sh продлевает его по cron и перезапускает панель.
# Ставит сертификат панели и подписке. Если выпустить не удалось, падает, а не оставляет панель на http.
# Без готового сертификата - Let's Encrypt через acme.sh в режиме standalone: проверка HTTP-01 на 80 порту,
# он должен быть открыт. На ip выдается только короткий сертификат (~6 дней), acme.sh продлевает его по cron
# и перезапускает панель. Ставит сертификат панели. Если сертификата нет, падает, а не оставляет панель на http.
- name: Set certificate paths
ansible.builtin.set_fact:
cert_is_ip: "{{ cert_host is match('^[0-9.]+$') or ':' in cert_host }}"
cert_dir: "/root/cert/{{ cert_host }}"
cert_given: "{{ cert_fullchain | default('') != '' }}"
acme: /root/.acme.sh/acme.sh
- name: Install acme.sh dependencies
ansible.builtin.apt:
name: [ curl, socat, cron ]
state: present
- name: Download acme.sh
ansible.builtin.get_url:
url: https://get.acme.sh
dest: /root/acme-install.sh
mode: "0700"
when: not acme is file
- name: Install acme.sh
ansible.builtin.shell: sh /root/acme-install.sh
args:
chdir: /root
creates: "{{ acme }}"
- name: Use Let's Encrypt
ansible.builtin.command: "{{ acme }} --set-default-ca --server letsencrypt"
changed_when: false
# код 2 - действующий сертификат уже есть, продлевать рано
- name: Issue certificate
ansible.builtin.command: >-
{{ acme }} --issue -d {{ cert_host }} --standalone --httpport 80 --server letsencrypt
{{ '--certificate-profile shortlived --days 6' if cert_is_ip else '' }}
register: cert_issue
changed_when: cert_issue.rc == 0
failed_when: cert_issue.rc not in [0, 2]
- name: Create certificate directory
ansible.builtin.file:
path: "{{ cert_dir }}"
state: directory
mode: "0700"
- name: Install certificate
ansible.builtin.command: >-
{{ acme }} --installcert -d {{ cert_host }}
--key-file {{ cert_dir }}/privkey.pem --fullchain-file {{ cert_dir }}/fullchain.pem
--reloadcmd "systemctl restart x-ui"
changed_when: true
- name: Upload certificate
when: cert_given
ansible.builtin.copy:
dest: "{{ cert_dir }}/{{ item.name }}"
content: "{{ item.content }}"
mode: "{{ item.mode }}"
loop:
- { name: fullchain.pem, content: "{{ cert_fullchain | default('') }}", mode: "0644" }
- { name: privkey.pem, content: "{{ cert_private_key | default('') }}", mode: "0600" }
loop_control:
label: "{{ item.name }}"
no_log: true
# иначе cron acme.sh перезапишет загруженный сертификат своим при продлении
- name: Stop Let's Encrypt renewal
when: cert_given and acme is file
ansible.builtin.command: "{{ acme }} --remove -d {{ cert_host }}"
register: acme_remove
changed_when: acme_remove.rc == 0
failed_when: false
- name: Issue Let's Encrypt certificate
when: not cert_given
block:
- name: Install acme.sh dependencies
ansible.builtin.apt:
name: [ curl, socat, cron ]
state: present
- name: Download acme.sh
ansible.builtin.get_url:
url: https://get.acme.sh
dest: /root/acme-install.sh
mode: "0700"
when: not acme is file
- name: Install acme.sh
ansible.builtin.shell: sh /root/acme-install.sh
args:
chdir: /root
creates: "{{ acme }}"
- name: Use Let's Encrypt
ansible.builtin.command: "{{ acme }} --set-default-ca --server letsencrypt"
changed_when: false
# код 2 - действующий сертификат уже есть, продлевать рано
- name: Issue certificate
ansible.builtin.command: >-
{{ acme }} --issue -d {{ cert_host }} --standalone --httpport 80 --server letsencrypt
{{ '--certificate-profile shortlived --days 6' if cert_is_ip else '' }}
register: cert_issue
changed_when: cert_issue.rc == 0
failed_when: cert_issue.rc not in [0, 2]
- name: Install certificate
ansible.builtin.command: >-
{{ acme }} --installcert -d {{ cert_host }}
--key-file {{ cert_dir }}/privkey.pem --fullchain-file {{ cert_dir }}/fullchain.pem
--reloadcmd "systemctl restart x-ui"
changed_when: true
- name: Check certificate
ansible.builtin.command: "openssl x509 -in {{ cert_dir }}/fullchain.pem -noout -checkend 3600 -ext subjectAltName"
+5 -4
View File
@@ -1,6 +1,7 @@
# extravars: username, password, panel_port, web_base_path, sub_port, sub_path, domain (optional), ssl (optional),
# version (optional)
# extravars: username, password, panel_port, web_base_path, sub_port, sub_path, version, domain (optional),
# ssl (optional), cert_fullchain и cert_private_key (optional, готовый сертификат вместо Let's Encrypt)
#
# version - тег релиза 3x-ui: скрипт установки берется из того же тега, панель ставится этой версии.
# Ставит 3x-ui скриптом из README проекта в неинтерактивном режиме, SSL скрипта выключен: он при ошибке молча оставляет
# панель на http. Сертификат выпускает tasks/xui_cert.yml на домен, если он задан, иначе на ip. Нужен открытый 80 порт.
# ssl=false - без сертификата, для серверов без публичного адреса.
@@ -22,14 +23,14 @@
- name: Download install script
ansible.builtin.get_url:
url: https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh
url: "https://raw.githubusercontent.com/mhsanaei/3x-ui/{{ version }}/install.sh"
dest: /root/3x-ui-install.sh
mode: "0700"
force: true
- name: Run install script
ansible.builtin.command:
cmd: "bash /root/3x-ui-install.sh {{ version | default('') }}"
cmd: "bash /root/3x-ui-install.sh {{ version }}"
creates: "{{ xui_bin }}"
environment:
XUI_NONINTERACTIVE: "1"
+2 -1
View File
@@ -122,7 +122,8 @@
- name: Return inbound
vars:
stream: "{{ inbound.streamSettings | from_json }}"
# список отдает строку JSON, а ответ на добавление - уже объект
stream: "{{ inbound.streamSettings if inbound.streamSettings is mapping else inbound.streamSettings | from_json }}"
ansible.builtin.set_stats:
aggregate: false
data: