diff --git a/ansible/playbooks/openvpn-phone.yml b/ansible/playbooks/openvpn-phone.yml index 0f502d1..c43e0a7 100644 --- a/ansible/playbooks/openvpn-phone.yml +++ b/ansible/playbooks/openvpn-phone.yml @@ -2,21 +2,27 @@ # ============================================================================ # Road-warrior OpenVPN профиль для телефона (Android). # -# Отдельный point-to-point static-key инстанс на ru-vps (порт 9444/tcp, tun2, +# Отдельный TLS point-to-point инстанс на ru-vps (порт 9444/tcp, tun2, # сеть 10.80.0.0/30), параллельный тоннелю site-to-site (tun0, homelab-openvpn) # и профилю ноутбука (tun1, openvpn-laptop.yml). Клиент подключается один за -# раз — static key не мультиплексируется. +# раз. +# +# КРИПТО: peer-fingerprint, БЕЗ CA/PKI. +# На ru-vps генерятся две самоподписанные EC-пары (server.* и client.*), +# каждая сторона пинит другую по SHA256-отпечатку. Это «quick setup» +# из man-страницы OpenVPN 2.6+. Прежняя схема со static key (--secret) +# отброшена: OpenVPN 2.7 в Android-клиенте запускать её отказывается +# ("No tls-client or tls-server option"), а 2.8 уберёт совсем. # # КЛИЕНТ -# Приложение «OpenVPN for Android» (Arne Schwabe, F-Droid/Play). Официальный -# OpenVPN Connect static-key конфиги НЕ поддерживает — на Android нужен -# именно этот клиент. +# Android — «OpenVPN for Android» (Arne Schwabe) или OpenVPN Connect: +# TLS-конфиг понимают оба. Профиль самодостаточный (cert+key внутри). # # ЧТО ДЕЛАЕТ -# Ставит openvpn на ru-vps, генерит static key (один раз), открывает +# Ставит openvpn на ru-vps, генерит EC-пары (один раз), открывает # 9444/tcp в UFW, поднимает systemd-юнит homelab-openvpn-phone, NAT'ит # 10.80.0.0/30 -> LAN через tun0, кладёт готовый профиль в -# ansible/generated/ada-phone.ovpn (mode 0600, содержит секрет). +# ansible/generated/ada-phone.ovpn (mode 0600, содержит приватный ключ). # # ЗАПУСК # make openvpn-phone @@ -29,7 +35,10 @@ phone_profile_name: ada-phone phone_openvpn_dir: /etc/openvpn/homelab-phone phone_openvpn_config: /etc/openvpn/homelab-phone/server.conf - phone_openvpn_key: /etc/openvpn/homelab-phone/static.key + phone_openvpn_server_cert: /etc/openvpn/homelab-phone/server.crt + phone_openvpn_server_key: /etc/openvpn/homelab-phone/server.key + phone_openvpn_client_cert: /etc/openvpn/homelab-phone/client.crt + phone_openvpn_client_key: /etc/openvpn/homelab-phone/client.key phone_openvpn_service: homelab-openvpn-phone phone_openvpn_port: 9444 phone_openvpn_interface: tun2 @@ -38,6 +47,7 @@ phone_openvpn_server_ip: 10.80.0.1 phone_openvpn_client_ip: 10.80.0.2 phone_openvpn_remote: 157.22.231.198 + phone_openvpn_data_ciphers: "AES-256-GCM:AES-128-GCM" phone_openvpn_client_config_local: "{{ playbook_dir }}/../generated/{{ phone_profile_name }}.ovpn" phone_openvpn_routes: "{{ homelab_vpn_client_routes }}" @@ -47,6 +57,7 @@ name: - openvpn - iptables + - openssl state: present update_cache: true @@ -58,12 +69,101 @@ group: root mode: "0700" - - name: Generate phone static key if missing - ansible.builtin.command: "openvpn --genkey secret {{ phone_openvpn_key }}" + - name: Remove the obsolete static key (crypto model changed to TLS) + ansible.builtin.file: + path: "{{ phone_openvpn_dir }}/static.key" + state: absent + + - name: Generate the server EC keypair if missing + ansible.builtin.command: + argv: + - openssl + - req + - -x509 + - -newkey + - ec + - -pkeyopt + - ec_paramgen_curve:prime256v1 + - -keyout + - "{{ phone_openvpn_server_key }}" + - -out + - "{{ phone_openvpn_server_cert }}" + - -nodes + - -sha256 + - -days + - "3650" + - -subj + - /CN=homelab-phone-server args: - creates: "{{ phone_openvpn_key }}" + creates: "{{ phone_openvpn_server_cert }}" no_log: true + - name: Generate the client EC keypair if missing + ansible.builtin.command: + argv: + - openssl + - req + - -x509 + - -newkey + - ec + - -pkeyopt + - ec_paramgen_curve:prime256v1 + - -keyout + - "{{ phone_openvpn_client_key }}" + - -out + - "{{ phone_openvpn_client_cert }}" + - -nodes + - -sha256 + - -days + - "3650" + - -subj + - /CN=homelab-phone-client + args: + creates: "{{ phone_openvpn_client_cert }}" + no_log: true + + - name: Enforce key permissions + ansible.builtin.file: + path: "{{ item }}" + owner: root + group: root + mode: "0600" + loop: + - "{{ phone_openvpn_server_key }}" + - "{{ phone_openvpn_client_key }}" + + - name: Read the server certificate fingerprint + ansible.builtin.command: + argv: + - openssl + - x509 + - -in + - "{{ phone_openvpn_server_cert }}" + - -noout + - -fingerprint + - -sha256 + register: phone_openvpn_server_fp_raw + changed_when: false + + - name: Read the client certificate fingerprint + ansible.builtin.command: + argv: + - openssl + - x509 + - -in + - "{{ phone_openvpn_client_cert }}" + - -noout + - -fingerprint + - -sha256 + register: phone_openvpn_client_fp_raw + changed_when: false + + - name: Extract the bare colon-separated fingerprints + # openssl печатает "sha256 Fingerprint=AB:CD:...", отпечаток без '='. + ansible.builtin.set_fact: + phone_openvpn_server_fp: "{{ phone_openvpn_server_fp_raw.stdout | regex_replace('^.*=', '') | trim }}" + phone_openvpn_client_fp: "{{ phone_openvpn_client_fp_raw.stdout | regex_replace('^.*=', '') | trim }}" + - name: Enable IPv4 forwarding on ru-vps ansible.posix.sysctl: name: net.ipv4.ip_forward @@ -85,16 +185,21 @@ mode: "0600" content: | dev {{ phone_openvpn_interface }} + dev-type tun ifconfig {{ phone_openvpn_server_ip }} {{ phone_openvpn_client_ip }} - secret {{ phone_openvpn_key }} - cipher AES-256-CBC - auth SHA256 proto tcp-server port {{ phone_openvpn_port }} + tls-server + dh none + cert {{ phone_openvpn_server_cert }} + key {{ phone_openvpn_server_key }} + peer-fingerprint {{ phone_openvpn_client_fp }} + data-ciphers {{ phone_openvpn_data_ciphers }} + auth SHA256 persist-key persist-tun - verb 3 keepalive 10 60 + verb 3 notify: restart phone openvpn - name: Render phone OpenVPN up script @@ -156,10 +261,16 @@ state: started enabled: true - - name: Read phone static key for client profile + - name: Read the client certificate for the profile ansible.builtin.slurp: - src: "{{ phone_openvpn_key }}" - register: phone_openvpn_static_key_raw + src: "{{ phone_openvpn_client_cert }}" + register: phone_openvpn_client_cert_raw + no_log: true + + - name: Read the client key for the profile + ansible.builtin.slurp: + src: "{{ phone_openvpn_client_key }}" + register: phone_openvpn_client_key_raw no_log: true - name: Ensure local generated directory exists @@ -177,12 +288,14 @@ dest: "{{ phone_openvpn_client_config_local }}" mode: "0600" content: | - client dev tun + dev-type tun proto tcp-client remote {{ phone_openvpn_remote }} {{ phone_openvpn_port }} ifconfig {{ phone_openvpn_client_ip }} {{ phone_openvpn_server_ip }} - cipher AES-256-CBC + tls-client + peer-fingerprint {{ phone_openvpn_server_fp }} + data-ciphers {{ phone_openvpn_data_ciphers }} auth SHA256 persist-key persist-tun @@ -192,9 +305,12 @@ {% for route in phone_openvpn_routes %} route {{ route.network }} {{ route.netmask }} {% endfor %} - - {{ phone_openvpn_static_key_raw.content | b64decode }} - + + {{ phone_openvpn_client_cert_raw.content | b64decode | trim }} + + + {{ phone_openvpn_client_key_raw.content | b64decode | trim }} + delegate_to: localhost vars: ansible_connection: local diff --git a/docs/ai/plan.md b/docs/ai/plan.md index c3aeb43..2e3c5bf 100644 --- a/docs/ai/plan.md +++ b/docs/ai/plan.md @@ -5,23 +5,33 @@ ### Road-warrior OpenVPN для телефона Выполнено 2026-09-03. `playbooks/openvpn-phone.yml` (`make openvpn-phone`) -поднимает на ru-vps отдельный point-to-point static-key инстанс: -порт 9444/tcp, `tun2`, сеть 10.80.0.0/30, юнит `homelab-openvpn-phone`, -NAT `10.80.0.0/30 -> LAN` через `tun0`. Профиль (с секретом) — -`ansible/generated/ada-phone.ovpn` (gitignored). Клиент — «OpenVPN for -Android» (Arne Schwabe); официальный OpenVPN Connect static key не умеет. +поднимает на ru-vps отдельный TLS point-to-point инстанс: порт 9444/tcp, +`tun2`, сеть 10.80.0.0/30, юнит `homelab-openvpn-phone`, NAT +`10.80.0.0/30 -> LAN` через `tun0`. Профиль (с приватным ключом) — +`ansible/generated/ada-phone.ovpn` (gitignored). -Прогон идемпотентен (третий заход `changed=0`). Проверено: сервис active, -`tun2` up, UFW 9444/tcp, NAT-правило на месте, с ru-vps `192.168.1.30:8082` -достижим. Маршруты клиента вынесены в `homelab_vpn_client_routes` -(`group_vars/all/main.yml`) — общий список с `openvpn-laptop.yml`, не весь -`/24` (домашняя сеть телефона тоже 192.168.1.0/24). +Крипто: **peer-fingerprint, без CA/PKI**. На ru-vps генерятся две +самоподписанные EC-пары (`server.*`, `client.*`), стороны пинят друг друга +по SHA256-отпечатку (`peer-fingerprint`), `data-ciphers AES-256-GCM`. Первая +попытка была на static key (`--secret`) — Android-клиент несёт OpenVPN 2.7 и +запускать её отказался («No tls-client or tls-server option»; 2.8 уберёт +совсем). Клиент на Android — «OpenVPN for Android» (Arne Schwabe) или +OpenVPN Connect: TLS-конфиг понимают оба. + +Прогон идемпотентен (`changed=0` на повторе). Проверено: сервис active, +OpenVPN 2.6.19 слушает 9444, `tun2` up, `Using certificate fingerprint to +verify peer`, UFW 9444/tcp, NAT-правило на месте, с ru-vps +`192.168.1.30:8082` достижим. Маршруты клиента вынесены в +`homelab_vpn_client_routes` (`group_vars/all/main.yml`) — общий список с +`openvpn-laptop.yml`, не весь `/24` (домашняя сеть телефона тоже +192.168.1.0/24). Побочно: в `openvpn-laptop.yml` и `openvpn-phone.yml` локальная запись профиля переведена с `become: false` на `vars: {ansible_connection: local, ansible_become: false}` — ключ `become: false` на `delegate_to: localhost` не подавлял унаследованный `ansible_become: true` (sudo password required). -`openvpn-laptop.yml` на живом ru-vps по-прежнему не применялся. +`openvpn-laptop.yml` по-прежнему на static key и на живом ru-vps не +применялся — при реальном использовании ему нужен тот же перевод на TLS. ### Дашборд-обзор инфраструктуры (Homepage)