--- - name: Create Hermes AI LXC on cloud-pc hosts: cloud-pc gather_facts: false vars: hermes_ai_vmid: 147 hermes_ai_hostname: hermes-ai hermes_ai_ip: 192.168.1.31/24 hermes_ai_gateway: 192.168.1.1 hermes_ai_rootfs: data:24 hermes_ai_ostemplate: local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst hermes_ai_pubkey_file: ~/.ssh/id_ed25519_homelab.pub tasks: - name: Check if Hermes AI LXC exists ansible.builtin.command: "pct config {{ hermes_ai_vmid }}" register: hermes_ai_pct_config changed_when: false failed_when: false - name: Install Hermes AI LXC SSH public key on PVE host ansible.builtin.copy: dest: /tmp/hermes-ai-lxc.pub owner: root group: root mode: "0600" content: "{{ lookup('file', hermes_ai_pubkey_file) }}\n" when: hermes_ai_pct_config.rc != 0 - name: Create Hermes AI LXC ansible.builtin.command: >- pct create {{ hermes_ai_vmid }} {{ hermes_ai_ostemplate }} --hostname {{ hermes_ai_hostname }} --rootfs {{ hermes_ai_rootfs }} --cores 2 --memory 4096 --swap 512 --net0 name=eth0,bridge=vmbr0,gw={{ hermes_ai_gateway }},ip={{ hermes_ai_ip }},firewall=1 --nameserver 1.1.1.1 --unprivileged 1 --features nesting=1,keyctl=1 --onboot 1 --startup order=90 --cmode shell --ssh-public-keys /tmp/hermes-ai-lxc.pub when: hermes_ai_pct_config.rc != 0 - name: Start Hermes AI LXC ansible.builtin.command: "pct start {{ hermes_ai_vmid }}" register: hermes_ai_pct_start changed_when: hermes_ai_pct_start.rc == 0 failed_when: hermes_ai_pct_start.rc not in [0, 255] - name: Configure Hermes AI runtime hosts: cloud-pc gather_facts: false vars: hermes_ai_vmid: 147 handlers: - name: restart Hermes AI LXC ansible.builtin.command: "pct reboot {{ hermes_ai_vmid }}" changed_when: true tasks: - name: Read Hermes AI LXC configuration ansible.builtin.command: "pct config {{ hermes_ai_vmid }}" register: hermes_ai_lxc_config changed_when: false - name: Enable keyctl for Docker in Hermes AI LXC ansible.builtin.command: "pct set {{ hermes_ai_vmid }} --features nesting=1,keyctl=1" when: "'keyctl=1' not in hermes_ai_lxc_config.stdout" notify: restart Hermes AI LXC - name: Allow FUSE device in Hermes AI LXC config ansible.builtin.lineinfile: path: "/etc/pve/lxc/{{ hermes_ai_vmid }}.conf" line: "lxc.cgroup2.devices.allow: c 10:229 rwm" state: present notify: restart Hermes AI LXC - name: Bind mount FUSE device in Hermes AI LXC config ansible.builtin.lineinfile: path: "/etc/pve/lxc/{{ hermes_ai_vmid }}.conf" line: "lxc.mount.entry: /dev/fuse dev/fuse none bind,create=file" state: present notify: restart Hermes AI LXC - name: Check TUN device on PVE host ansible.builtin.stat: path: /dev/net/tun register: hermes_ai_tun_device - name: Require TUN device on PVE host ansible.builtin.assert: that: - hermes_ai_tun_device.stat.exists - hermes_ai_tun_device.stat.ischr fail_msg: /dev/net/tun must exist on cloud-pc before enabling the Hermes AI transparent proxy. - name: Allow TUN device in Hermes AI LXC config ansible.builtin.lineinfile: path: "/etc/pve/lxc/{{ hermes_ai_vmid }}.conf" line: "lxc.cgroup2.devices.allow: c 10:200 rwm" state: present notify: restart Hermes AI LXC - name: Bind mount TUN device in Hermes AI LXC config ansible.builtin.lineinfile: path: "/etc/pve/lxc/{{ hermes_ai_vmid }}.conf" line: "lxc.mount.entry: /dev/net/tun dev/net/tun none bind,create=file" state: present notify: restart Hermes AI LXC - name: Apply pending Hermes AI LXC configuration ansible.builtin.meta: flush_handlers - name: Wait for Hermes AI SSH through ru-vps ansible.builtin.wait_for_connection: timeout: 120 delegate_to: hermes-ai vars: ansible_become: false - name: Install Hermes AI base runtime hosts: hermes-ai gather_facts: false vars: ansible_become: false hermes_ai_root: /opt/hermes-ai hermes_ai_state: /srv/hermes-ai hermes_ai_proxy_dir: /opt/hermes-ai/tun-proxy hermes_ai_proxy_image: metacubex/mihomo@sha256:e6acd921addecfd59a8e2d38203f88356d635b54de6c0673db0e015139989312 hermes_ai_proxy_name: hermes-ai-tun-proxy tasks: - name: Install Hermes AI runtime packages ansible.builtin.apt: name: - ca-certificates - curl - docker-compose - docker.io - fuse-overlayfs - git - ufw state: present update_cache: true - name: Configure Docker storage driver for unprivileged LXC ansible.builtin.copy: dest: /etc/docker/daemon.json owner: root group: root mode: "0644" content: | { "storage-driver": "fuse-overlayfs" } register: hermes_ai_docker_config - name: Enable Docker service ansible.builtin.systemd: name: docker enabled: true state: "{{ 'restarted' if hermes_ai_docker_config.changed else 'started' }}" - name: Allow SSH only from the HomeLab LAN community.general.ufw: rule: allow port: "22" proto: tcp src: "{{ homelab_lan_cidr }}" - name: Allow SSH from the OpenVPN network community.general.ufw: rule: allow port: "22" proto: tcp src: "{{ openvpn_network_cidr }}" - name: Enable restrictive Hermes AI firewall community.general.ufw: state: enabled policy: deny direction: incoming - name: Create Hermes AI application directories ansible.builtin.file: path: "{{ item }}" state: directory owner: root group: root mode: "0750" loop: - "{{ hermes_ai_root }}" - "{{ hermes_ai_state }}" - "{{ hermes_ai_proxy_dir }}" - name: Configure Hermes AI transparent proxy ansible.builtin.copy: dest: "{{ hermes_ai_proxy_dir }}/config.yaml" owner: root group: root mode: "0640" content: | ipv6: false tun: enable: true stack: system device: hermes-tun auto-route: true auto-redirect: true auto-detect-interface: true route-exclude-address: - 192.168.1.27/32 dns-hijack: - any:53 dns: enable: true enhanced-mode: redir-host nameserver: - https://cloudflare-dns.com/dns-query sniffer: enable: true force-dns-mapping: true parse-pure-ip: true proxies: - name: mihomo-lan type: socks5 server: 192.168.1.27 port: 7891 proxy-groups: - name: PROXY type: select proxies: - mihomo-lan rules: - IP-CIDR,127.0.0.0/8,DIRECT,no-resolve - IP-CIDR,10.0.0.0/8,DIRECT,no-resolve - IP-CIDR,172.16.0.0/12,DIRECT,no-resolve - IP-CIDR,192.168.0.0/16,DIRECT,no-resolve - IP-CIDR,169.254.0.0/16,DIRECT,no-resolve - MATCH,PROXY register: hermes_ai_proxy_config - name: Wait for Mihomo SOCKS5 upstream ansible.builtin.wait_for: host: 192.168.1.27 port: 7891 timeout: 15 - name: Validate Hermes AI transparent proxy config ansible.builtin.command: >- docker run --rm --network none -v {{ hermes_ai_proxy_dir }}:/root/.config/mihomo:ro {{ hermes_ai_proxy_image }} -t -d /root/.config/mihomo changed_when: false - name: Install Hermes AI transparent proxy service ansible.builtin.copy: dest: /etc/systemd/system/hermes-ai-tun-proxy.service owner: root group: root mode: "0644" content: | [Unit] Description=Hermes AI transparent Mihomo proxy After=docker.service network-online.target Requires=docker.service [Service] Restart=always RestartSec=10 ExecStartPre=-/usr/bin/docker rm -f {{ hermes_ai_proxy_name }} ExecStart=/usr/bin/docker run --rm \ --name {{ hermes_ai_proxy_name }} \ --network host \ --cap-drop ALL \ --cap-add NET_ADMIN \ --security-opt no-new-privileges \ --device /dev/net/tun \ -v {{ hermes_ai_proxy_dir }}:/root/.config/mihomo:ro \ {{ hermes_ai_proxy_image }} ExecStop=/usr/bin/docker stop {{ hermes_ai_proxy_name }} [Install] WantedBy=multi-user.target register: hermes_ai_proxy_unit - name: Reload systemd when transparent proxy service changes ansible.builtin.systemd: daemon_reload: true when: hermes_ai_proxy_unit.changed - name: Enable Hermes AI transparent proxy ansible.builtin.systemd: name: hermes-ai-tun-proxy enabled: true state: "{{ 'restarted' if hermes_ai_proxy_config.changed or hermes_ai_proxy_unit.changed else 'started' }}" - name: Check Hermes AI transparent proxy service ansible.builtin.command: systemctl is-active hermes-ai-tun-proxy register: hermes_ai_proxy_status changed_when: false failed_when: hermes_ai_proxy_status.stdout != 'active' - name: Check Hermes AI external HTTPS access ansible.builtin.command: curl --fail --silent --show-error --max-time 20 https://api.ipify.org changed_when: false no_log: true - name: Document Hermes AI runtime layout ansible.builtin.copy: dest: "{{ hermes_ai_root }}/README.md" owner: root group: root mode: "0644" content: | # Hermes AI runtime Deploy the Hermes application here. Keep runtime state and the real `.env` file in /srv/hermes-ai. Do not commit Telegram or LLM tokens. Docker is installed with fuse-overlayfs for this unprivileged LXC. Hermes is a Telegram bot and needs no published Docker ports. Do not use `ports:` or `-p` without adding an explicit firewall policy: Docker port publishing can bypass UFW. The hermes-ai-tun-proxy service routes external traffic through Mihomo at 192.168.1.27:7890. LAN traffic remains direct.