One ru-vps housekeeping sweep (2026-09-02/03): - playbooks/ru-vps-base.yml (new): adopt the Caddy compose stack into Ansible (pinned image by digest, homelab-caddy.service), and manage the corosync-qnetd UFW rule - allow 5403/tcp from homelab_pve_egress_ip, drop the stale rule for the retired ZeroTier 10.122.62.0/24. The qdevice had gone silent because its only allowed path was the decommissioned ZeroTier network. - group_vars/all/main.yml: homelab_pve_egress_ip (the NATed home egress the PVE nodes reach corosync-qnetd from - a direct path that does not depend on the OpenVPN tunnel). Marked dynamic: a change silently re-breaks the qdevice. - playbooks/status.yml: CLUSTER QUORUM section (pvecm status per PVE node) so a repeat failure is visible. Also drops the memoir-bot unit list and moves the gitea offsite-restic unit to the gitea host (see the OpenTofu-migration commit). - playbooks/ru-vps-zerotier-decommission.yml (new): stop the zerotier container, disable ssh-zt22.service, remove the interface/9993/9001/10.122.62.0/24 UFW rules. Node identity and data are kept; removal is a separate step. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012uoq5AVK8mkBgg83Mq6o5V
184 lines
8.3 KiB
YAML
184 lines
8.3 KiB
YAML
---
|
||
# Вывод ZeroTier с ru-vps и чистка оставшихся от него правил UFW.
|
||
#
|
||
# ПОЧЕМУ ЭТО БЕЗОПАСНО
|
||
# Проверено 2026-09-02: ZT-интерфейса на хосте нет, маршрутов через него нет,
|
||
# на ZT-адресах никто не слушает. Контейнер `zerotier` работает, но подключён
|
||
# в никуда (потому и unhealthy). `ssh-zt22.service` в состоянии failed: его
|
||
# sshd настроен на ListenAddress 10.122.62.65, которого больше не существует.
|
||
#
|
||
# Из Ansible ZeroTier выведен ещё в июле 2026 — на хосте остался хвост.
|
||
#
|
||
# ЧТО НЕ УДАЛЯЕТСЯ
|
||
# Каталог /opt/services/ru-vps/zerotier, данные /opt/data/zerotier и файл
|
||
# /etc/ssh/sshd_config_zt22 остаются на месте. `docker compose down` идёт БЕЗ
|
||
# -v, то есть identity узла ZeroTier сохраняется. Удаление этих файлов —
|
||
# отдельный осознанный шаг оператора после того, как всё устоялось.
|
||
#
|
||
# make zerotier-decommission CONFIRM=1
|
||
|
||
- name: Decommission ZeroTier on ru-vps
|
||
hosts: ru-vps
|
||
gather_facts: false
|
||
|
||
vars:
|
||
zt_dir: /opt/services/ru-vps/zerotier
|
||
zt_interface: zt6q3dmi2d
|
||
zt_legacy_cidr: 10.122.62.0/24
|
||
# Правила UFW для сервисов, которые на хосте ничего не слушают (проверено
|
||
# через ss): squid не запущен, danted слушает 1081 а не 1080, на 993 и 7892
|
||
# слушателей нет. Это НЕ наследие ZeroTier, поэтому по умолчанию не трогаем.
|
||
zt_cleanup_unrelated_stale_rules: false
|
||
|
||
tasks:
|
||
# --- Предполётные проверки ---------------------------------------------
|
||
- name: List the network interfaces
|
||
ansible.builtin.command: ip -br addr show
|
||
register: zt_ifaces
|
||
changed_when: false
|
||
check_mode: false
|
||
|
||
- name: Read the current firewall rules
|
||
ansible.builtin.command: ufw status
|
||
register: zt_ufw
|
||
changed_when: false
|
||
check_mode: false
|
||
|
||
- name: Refuse to run while a ZeroTier interface is up
|
||
ansible.builtin.assert:
|
||
that:
|
||
- zt_interface not in zt_ifaces.stdout
|
||
fail_msg: >-
|
||
Интерфейс {{ zt_interface }} присутствует на хосте — значит ZeroTier
|
||
снова используется. Плейбук рассчитан на вывод мёртвого стека и
|
||
отказывается работать: сначала подтверди, что сеть больше не нужна.
|
||
|
||
# Управляющая сессия идёт по 3422/tcp. Если глобального разрешения на этот
|
||
# порт нет, чистка правил может оборвать доступ — лучше не начинать.
|
||
- name: Require the management SSH rule to survive the cleanup
|
||
ansible.builtin.assert:
|
||
that:
|
||
- "'3422/tcp' in zt_ufw.stdout"
|
||
fail_msg: >-
|
||
В UFW нет правила для 3422/tcp. Прерываюсь, чтобы не остаться без
|
||
управляющего доступа.
|
||
|
||
# --- Контейнер ----------------------------------------------------------
|
||
- name: Check whether the ZeroTier stack is present
|
||
ansible.builtin.stat:
|
||
path: "{{ zt_dir }}/docker-compose.yml"
|
||
register: zt_compose
|
||
|
||
- name: Stop and remove the ZeroTier stack
|
||
ansible.builtin.command:
|
||
cmd: /usr/bin/docker compose -f {{ zt_dir }}/docker-compose.yml down
|
||
chdir: "{{ zt_dir }}"
|
||
register: zt_down
|
||
when: zt_compose.stat.exists
|
||
changed_when: "'Removing' in zt_down.stderr or 'Stopping' in zt_down.stderr"
|
||
|
||
# --- sshd на ZeroTier ---------------------------------------------------
|
||
- name: Disable the ZeroTier-only sshd unit
|
||
ansible.builtin.systemd:
|
||
name: ssh-zt22.service
|
||
state: stopped
|
||
enabled: false
|
||
failed_when: false
|
||
|
||
# `stop` не снимает состояние failed, и юнит навсегда остался бы в секции
|
||
# FAILED SYSTEMD UNITS отчёта `make status`. homelab-pve-routes.service —
|
||
# фантом той же эпохи: файла юнита уже нет, а запись о падении осталась.
|
||
- name: List the units currently in a failed state
|
||
ansible.builtin.command: systemctl list-units --state=failed --no-legend --plain --no-pager
|
||
register: zt_failed
|
||
changed_when: false
|
||
check_mode: false
|
||
|
||
- name: Clear the leftover failed state of the ZeroTier-era units
|
||
ansible.builtin.command: "systemctl reset-failed {{ item }}"
|
||
loop:
|
||
- ssh-zt22.service
|
||
- homelab-pve-routes.service
|
||
# Без этого условия reset-failed рапортует changed на каждом прогоне:
|
||
# его код возврата нулевой и когда сбрасывать нечего.
|
||
when: item in zt_failed.stdout
|
||
register: zt_reset
|
||
changed_when: zt_reset.rc == 0
|
||
failed_when: false
|
||
|
||
# --- UFW ----------------------------------------------------------------
|
||
- name: Remove the ZeroTier interface rules
|
||
community.general.ufw:
|
||
rule: allow
|
||
direction: "{{ item }}"
|
||
interface: "{{ zt_interface }}"
|
||
delete: true
|
||
loop: [in, out]
|
||
|
||
- name: Remove the ZeroTier port rules
|
||
community.general.ufw:
|
||
rule: "{{ item.rule }}"
|
||
port: "{{ item.port }}"
|
||
proto: "{{ item.proto | default('any') }}"
|
||
interface: "{{ item.interface | default(omit) }}"
|
||
direction: "{{ 'in' if item.interface is defined else omit }}"
|
||
delete: true
|
||
loop:
|
||
- {rule: allow, port: "9993", proto: udp} # ZeroTier UDP
|
||
- {rule: deny, port: "9001"} # ZeroTier controller
|
||
- {rule: allow, port: "9001", interface: "zt+"} # он же, но с ZT-сетей
|
||
loop_control:
|
||
label: "{{ item.rule }} {{ item.port }}{{ '/' ~ item.proto if item.proto is defined else '' }}"
|
||
|
||
- name: Remove rules scoped to the retired ZeroTier subnet
|
||
community.general.ufw:
|
||
rule: allow
|
||
port: "{{ item }}"
|
||
proto: tcp
|
||
src: "{{ zt_legacy_cidr }}"
|
||
delete: true
|
||
loop: ["22", "3422"]
|
||
|
||
- name: Remove stale rules for services that no longer listen
|
||
community.general.ufw:
|
||
rule: allow
|
||
port: "{{ item.port }}"
|
||
proto: tcp
|
||
delete: true
|
||
loop:
|
||
- {port: "3128"} # squid: стек есть, контейнер не запущен
|
||
- {port: "1080"} # danted слушает 1081, не 1080
|
||
- {port: "993"} # слушателей нет
|
||
- {port: "7892"} # слушателей нет
|
||
loop_control:
|
||
label: "{{ item.port }}/tcp"
|
||
when: zt_cleanup_unrelated_stale_rules | bool
|
||
|
||
# --- Проверка -----------------------------------------------------------
|
||
- name: List the running containers after the cleanup
|
||
ansible.builtin.command: docker ps --format {{ '{{' }}.Names{{ '}}' }}
|
||
register: zt_after_ps
|
||
changed_when: false
|
||
check_mode: false
|
||
|
||
- name: Read the firewall after the cleanup
|
||
ansible.builtin.command: ufw status
|
||
register: zt_after_ufw
|
||
changed_when: false
|
||
check_mode: false
|
||
|
||
- name: Confirm ZeroTier is gone and management access survived
|
||
ansible.builtin.assert:
|
||
that:
|
||
- "'zerotier' not in zt_after_ps.stdout_lines"
|
||
- zt_interface not in zt_after_ufw.stdout
|
||
- "'3422/tcp' in zt_after_ufw.stdout"
|
||
success_msg: ZeroTier выведен, правило для 3422/tcp на месте.
|
||
# В --check контейнер не останавливается и правила не удаляются, поэтому
|
||
# проверка результата заведомо не прошла бы. Diff'ы задач выше достоверны.
|
||
when: not ansible_check_mode
|
||
|
||
- name: Show the resulting firewall
|
||
ansible.builtin.debug:
|
||
var: zt_after_ufw.stdout_lines
|