Commit the accumulated infrastructure work that was living only in the working tree: monitoring stack, emergency access/bot, gyro allocator, grimmory, adguard, backup audit and the OpenCode agent definitions. Also ignore Python bytecode, local archives and Nix/direnv artifacts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GTocXkGUUazHdKKd3r9k71
281 lines
8.5 KiB
YAML
281 lines
8.5 KiB
YAML
---
|
|
- name: Harden live Mihomo on ru-vps
|
|
hosts: ru-vps
|
|
gather_facts: false
|
|
vars:
|
|
ru_vps_mihomo_harden_confirm: false
|
|
mihomo_root: /opt/services/ru-vps/mihomo
|
|
mihomo_config_path: "{{ mihomo_root }}/config/config.yaml"
|
|
mihomo_compose_path: "{{ mihomo_root }}/docker-compose.yml"
|
|
mihomo_backup_dir: /var/backups/ru-vps-mihomo
|
|
mihomo_state_dir: /var/lib/ru-vps-mihomo
|
|
mihomo_state_path: "{{ mihomo_state_dir }}/auth.json"
|
|
mihomo_harden_script_path: /usr/local/sbin/ru-vps-mihomo-harden
|
|
mihomo_harden_script_src: "{{ playbook_dir }}/../files/ru-vps-mihomo-harden.py"
|
|
mihomo_backup_path: "{{ mihomo_backup_dir }}/config.yaml.{{ lookup('pipe', 'date -u +%Y%m%d%H%M%S') }}"
|
|
pre_tasks:
|
|
- name: Require explicit Mihomo hardening confirmation
|
|
ansible.builtin.assert:
|
|
that:
|
|
- ru_vps_mihomo_harden_confirm | bool
|
|
fail_msg: Run with -e ru_vps_mihomo_harden_confirm=true.
|
|
|
|
- name: Preflight Mihomo config file exists
|
|
ansible.builtin.stat:
|
|
path: "{{ mihomo_config_path }}"
|
|
register: mihomo_config_stat
|
|
|
|
- name: Preflight Mihomo compose file exists
|
|
ansible.builtin.stat:
|
|
path: "{{ mihomo_compose_path }}"
|
|
register: mihomo_compose_stat
|
|
|
|
- name: Preflight Mihomo state file exists
|
|
ansible.builtin.stat:
|
|
path: "{{ mihomo_state_path }}"
|
|
register: mihomo_state_stat
|
|
|
|
- name: Refuse to run without live Mihomo config and compose files
|
|
ansible.builtin.assert:
|
|
that:
|
|
- mihomo_config_stat.stat.exists
|
|
- mihomo_compose_stat.stat.exists
|
|
fail_msg: Live Mihomo config or compose file is missing.
|
|
|
|
- name: Preflight Mihomo container is running
|
|
ansible.builtin.command:
|
|
argv:
|
|
- docker
|
|
- inspect
|
|
- -f
|
|
- '{{ "{{" }}.State.Running{{ "}}" }}'
|
|
- mihomo
|
|
register: mihomo_container_state
|
|
changed_when: false
|
|
failed_when: mihomo_container_state.stdout.strip() != 'true'
|
|
|
|
- name: Preflight PyYAML is available on the target
|
|
ansible.builtin.command:
|
|
argv:
|
|
- python3
|
|
- -c
|
|
- import yaml
|
|
changed_when: false
|
|
|
|
- name: Preflight current Mihomo config validates
|
|
ansible.builtin.command:
|
|
argv:
|
|
- docker
|
|
- compose
|
|
- -f
|
|
- "{{ mihomo_compose_path }}"
|
|
- exec
|
|
- -T
|
|
- mihomo
|
|
- /mihomo
|
|
- -t
|
|
- -d
|
|
- /root/.config/mihomo
|
|
changed_when: false
|
|
|
|
- name: Ensure Mihomo backup directory exists
|
|
ansible.builtin.file:
|
|
path: "{{ mihomo_backup_dir }}"
|
|
state: directory
|
|
owner: root
|
|
group: root
|
|
mode: "0700"
|
|
|
|
- name: Install root-only Mihomo hardening helper
|
|
ansible.builtin.copy:
|
|
src: "{{ mihomo_harden_script_src }}"
|
|
dest: "{{ mihomo_harden_script_path }}"
|
|
owner: root
|
|
group: root
|
|
mode: "0700"
|
|
|
|
- name: Create fresh backup of the live Mihomo config after validation
|
|
ansible.builtin.copy:
|
|
src: "{{ mihomo_config_path }}"
|
|
dest: "{{ mihomo_backup_path }}"
|
|
remote_src: true
|
|
owner: root
|
|
group: root
|
|
mode: "0600"
|
|
|
|
tasks:
|
|
- block:
|
|
- name: Apply Mihomo hardening in place
|
|
ansible.builtin.command:
|
|
argv:
|
|
- "{{ mihomo_harden_script_path }}"
|
|
- apply
|
|
- --config
|
|
- "{{ mihomo_config_path }}"
|
|
- --state
|
|
- "{{ mihomo_state_path }}"
|
|
register: mihomo_harden_apply
|
|
changed_when: (mihomo_harden_apply.stdout | from_json).changed
|
|
no_log: true
|
|
|
|
- name: Validate hardened Mihomo config in the running container
|
|
ansible.builtin.command:
|
|
argv:
|
|
- docker
|
|
- compose
|
|
- -f
|
|
- "{{ mihomo_compose_path }}"
|
|
- exec
|
|
- -T
|
|
- mihomo
|
|
- /mihomo
|
|
- -t
|
|
- -d
|
|
- /root/.config/mihomo
|
|
changed_when: false
|
|
|
|
- name: Recreate Mihomo container after config hardening
|
|
ansible.builtin.command:
|
|
argv:
|
|
- docker
|
|
- compose
|
|
- -f
|
|
- "{{ mihomo_compose_path }}"
|
|
- up
|
|
- -d
|
|
- --force-recreate
|
|
- mihomo
|
|
changed_when: true
|
|
|
|
- name: Wait for hardened Mihomo listeners on loopback
|
|
ansible.builtin.wait_for:
|
|
host: 127.0.0.1
|
|
port: "{{ item }}"
|
|
state: started
|
|
timeout: 30
|
|
loop:
|
|
- 7890
|
|
- 7891
|
|
|
|
- name: Run authenticated Mihomo SOCKS probe to Telegram
|
|
ansible.builtin.command:
|
|
argv:
|
|
- "{{ mihomo_harden_script_path }}"
|
|
- probe
|
|
- --config
|
|
- "{{ mihomo_config_path }}"
|
|
- --state
|
|
- "{{ mihomo_state_path }}"
|
|
register: mihomo_harden_probe
|
|
changed_when: false
|
|
no_log: true
|
|
|
|
- name: Check anonymous SOCKS access fails
|
|
ansible.builtin.command:
|
|
argv:
|
|
- curl
|
|
- --silent
|
|
- --show-error
|
|
- --connect-timeout
|
|
- "5"
|
|
- --max-time
|
|
- "15"
|
|
- --proxy
|
|
- socks5h://127.0.0.1:7891
|
|
- https://api.telegram.org
|
|
- --output
|
|
- /dev/null
|
|
register: mihomo_anon_probe
|
|
changed_when: false
|
|
failed_when: false
|
|
|
|
- name: Remove public Mihomo UFW rules after successful hardening
|
|
community.general.ufw:
|
|
rule: allow
|
|
port: "{{ item.port }}"
|
|
proto: "{{ item.proto }}"
|
|
delete: true
|
|
loop:
|
|
- { port: 7890, proto: tcp }
|
|
- { port: 7890, proto: udp }
|
|
- { port: 7891, proto: tcp }
|
|
- { port: 7891, proto: udp }
|
|
|
|
- name: Verify Mihomo listeners are loopback only
|
|
ansible.builtin.command:
|
|
argv:
|
|
- ss
|
|
- -H
|
|
- -ltnp
|
|
- '( sport = :7890 or sport = :7891 )'
|
|
register: mihomo_ss
|
|
changed_when: false
|
|
|
|
- name: Confirm hardened Mihomo is bound to loopback only and anonymous access fails
|
|
ansible.builtin.assert:
|
|
that:
|
|
- mihomo_ss.stdout is search('127\\.0\\.0\\.1:7890')
|
|
- mihomo_ss.stdout is search('127\\.0\\.0\\.1:7891')
|
|
- mihomo_ss.stdout is not search('0\\.0\\.0\\.0:7890|:::7890|0\\.0\\.0\\.0:7891|:::7891')
|
|
- mihomo_anon_probe.rc != 0
|
|
fail_msg: Hardened Mihomo must listen on loopback only and reject anonymous SOCKS access.
|
|
|
|
rescue:
|
|
- name: Restore the live Mihomo config from backup
|
|
ansible.builtin.copy:
|
|
src: "{{ mihomo_backup_path }}"
|
|
dest: "{{ mihomo_config_path }}"
|
|
remote_src: true
|
|
owner: root
|
|
group: root
|
|
mode: "0640"
|
|
|
|
- name: Recreate Mihomo container after rollback
|
|
ansible.builtin.command:
|
|
argv:
|
|
- docker
|
|
- compose
|
|
- -f
|
|
- "{{ mihomo_compose_path }}"
|
|
- up
|
|
- -d
|
|
- --force-recreate
|
|
- mihomo
|
|
changed_when: true
|
|
|
|
- name: Remove newly created Mihomo credentials state after rollback
|
|
ansible.builtin.file:
|
|
path: "{{ mihomo_state_path }}"
|
|
state: absent
|
|
when: not mihomo_state_stat.stat.exists
|
|
|
|
- name: Wait for restored Mihomo listeners on loopback
|
|
ansible.builtin.wait_for:
|
|
host: 127.0.0.1
|
|
port: "{{ item }}"
|
|
state: started
|
|
timeout: 30
|
|
loop:
|
|
- 7890
|
|
- 7891
|
|
|
|
- name: Validate restored Mihomo config in the running container
|
|
ansible.builtin.command:
|
|
argv:
|
|
- docker
|
|
- compose
|
|
- -f
|
|
- "{{ mihomo_compose_path }}"
|
|
- exec
|
|
- -T
|
|
- mihomo
|
|
- /mihomo
|
|
- -t
|
|
- -d
|
|
- /root/.config/mihomo
|
|
changed_when: false
|
|
|
|
- name: Fail Mihomo hardening after restoring the backup
|
|
ansible.builtin.fail:
|
|
msg: Mihomo hardening failed and the live config was restored from backup.
|