Files
infra/ansible/playbooks/openvpn-phone.yml
T
DmitryandClaude Sonnet 5 dfcee00232
lint / yamllint + ansible-lint + syntax-check (push) Canceled after 0s
fix(openvpn-phone): open FORWARD for road-warrior traffic into the LAN
The phone tunnel established fine (TLS peer-fingerprint OK) but no traffic
reached the LAN: ru-vps has policy FORWARD DROP and only per-service game
ACCEPTs. The site tunnel never needed a FORWARD rule because it carries
ru-vps's own traffic, not forwarded packets; the road-warrior instance is
the first forwarded path.

Add a `ufw route allow` (tun2 -> tun0, 10.80.0.0/30 -> 192.168.1.0/24). ufw
route rules survive `ufw reload`, unlike a raw `iptables -I FORWARD` which
would sit before the ufw chains and be flushed on reload. Return traffic is
covered by the global RELATED,ESTABLISHED accept in ufw-before-forward.

Verified on ru-vps: `ufw status` shows "192.168.1.0/24 on tun0 ALLOW FWD
10.80.0.0/30 on tun2"; make openvpn-phone idempotent.

Same gap exists in openvpn-laptop.yml (tun1) — still not deployed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KbuZrUoevfBgCpf5DCF4DG
2026-09-03 09:50:01 +03:00

343 lines
12 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
# ============================================================================
# Road-warrior OpenVPN профиль для телефона (Android).
#
# Отдельный TLS point-to-point инстанс на ru-vps (порт 9444/tcp, tun2,
# сеть 10.80.0.0/30), параллельный тоннелю site-to-site (tun0, homelab-openvpn)
# и профилю ноутбука (tun1, openvpn-laptop.yml). Клиент подключается один за
# раз.
#
# КРИПТО: 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 уберёт совсем.
#
# КЛИЕНТ
# Android — «OpenVPN for Android» (Arne Schwabe) или OpenVPN Connect:
# TLS-конфиг понимают оба. Профиль самодостаточный (cert+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, содержит приватный ключ).
#
# ЗАПУСК
# make openvpn-phone
# ============================================================================
- name: Configure OpenVPN phone access through ru-vps
hosts: ru-vps
gather_facts: true
become: true
vars:
phone_profile_name: ada-phone
phone_openvpn_dir: /etc/openvpn/homelab-phone
phone_openvpn_config: /etc/openvpn/homelab-phone/server.conf
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
phone_openvpn_site_interface: tun0
phone_openvpn_network_cidr: 10.80.0.0/30
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 }}"
tasks:
- name: Install OpenVPN packages
ansible.builtin.apt:
name:
- openvpn
- iptables
- openssl
state: present
update_cache: true
- name: Ensure phone OpenVPN config directory exists
ansible.builtin.file:
path: "{{ phone_openvpn_dir }}"
state: directory
owner: root
group: root
mode: "0700"
- 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_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
value: "1"
state: present
reload: true
- name: Allow phone OpenVPN TCP in UFW
ansible.builtin.command: "ufw allow {{ phone_openvpn_port }}/tcp"
register: ufw_allow_phone_openvpn
changed_when: "'Rule added' in ufw_allow_phone_openvpn.stdout or 'Rules updated' in ufw_allow_phone_openvpn.stdout"
failed_when: false
# Road-warrior трафик tun2 -> tun0 ФОРВАРДИТСЯ через ru-vps, а policy
# FORWARD = DROP. Сайт-тоннель этого правила не требовал (он несёт
# собственный трафик ru-vps, не forward). ufw route allow переживает
# `ufw reload`, в отличие от сырого iptables -I FORWARD. Обратное
# направление покрыто глобальным RELATED,ESTABLISHED в ufw-before-forward.
- name: Allow forwarding phone VPN traffic into the LAN
community.general.ufw:
route: true
rule: allow
interface_in: "{{ phone_openvpn_interface }}"
interface_out: "{{ phone_openvpn_site_interface }}"
src: "{{ phone_openvpn_network_cidr }}"
dest: "{{ homelab_lan_cidr }}"
- name: Render phone OpenVPN server config
ansible.builtin.copy:
dest: "{{ phone_openvpn_config }}"
owner: root
group: root
mode: "0600"
content: |
dev {{ phone_openvpn_interface }}
dev-type tun
ifconfig {{ phone_openvpn_server_ip }} {{ phone_openvpn_client_ip }}
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
keepalive 10 60
verb 3
notify: restart phone openvpn
- name: Render phone OpenVPN up script
ansible.builtin.copy:
dest: "{{ phone_openvpn_dir }}/up.sh"
owner: root
group: root
mode: "0700"
content: |
#!/bin/sh
iptables -t nat -C POSTROUTING -s {{ phone_openvpn_network_cidr }} -d {{ homelab_lan_cidr }} -o {{ phone_openvpn_site_interface }} -j MASQUERADE 2>/dev/null || \
iptables -t nat -A POSTROUTING -s {{ phone_openvpn_network_cidr }} -d {{ homelab_lan_cidr }} -o {{ phone_openvpn_site_interface }} -j MASQUERADE
notify: restart phone openvpn
- name: Render phone OpenVPN down script
ansible.builtin.copy:
dest: "{{ phone_openvpn_dir }}/down.sh"
owner: root
group: root
mode: "0700"
content: |
#!/bin/sh
iptables -t nat -D POSTROUTING -s {{ phone_openvpn_network_cidr }} -d {{ homelab_lan_cidr }} -o {{ phone_openvpn_site_interface }} -j MASQUERADE 2>/dev/null || true
notify: restart phone openvpn
- name: Render phone OpenVPN systemd service
ansible.builtin.copy:
dest: "/etc/systemd/system/{{ phone_openvpn_service }}.service"
owner: root
group: root
mode: "0644"
content: |
[Unit]
Description=HomeLab OpenVPN phone access
After=network-online.target homelab-openvpn.service
Wants=network-online.target
Requires=homelab-openvpn.service
[Service]
Type=simple
ExecStartPre={{ phone_openvpn_dir }}/up.sh
ExecStart=/usr/sbin/openvpn --config {{ phone_openvpn_config }}
ExecStopPost={{ phone_openvpn_dir }}/down.sh
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
notify:
- reload systemd
- restart phone openvpn
- name: Flush handlers before starting phone OpenVPN
ansible.builtin.meta: flush_handlers
- name: Enable and start phone OpenVPN
ansible.builtin.systemd:
name: "{{ phone_openvpn_service }}"
state: started
enabled: true
- name: Read the client certificate for the profile
ansible.builtin.slurp:
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
ansible.builtin.file:
path: "{{ playbook_dir }}/../generated"
state: directory
mode: "0700"
delegate_to: localhost
vars:
ansible_connection: local
ansible_become: false
- name: Write local phone OpenVPN profile
ansible.builtin.copy:
dest: "{{ phone_openvpn_client_config_local }}"
mode: "0600"
content: |
dev tun
dev-type tun
proto tcp-client
remote {{ phone_openvpn_remote }} {{ phone_openvpn_port }}
ifconfig {{ phone_openvpn_client_ip }} {{ phone_openvpn_server_ip }}
tls-client
peer-fingerprint {{ phone_openvpn_server_fp }}
data-ciphers {{ phone_openvpn_data_ciphers }}
auth SHA256
persist-key
persist-tun
resolv-retry infinite
nobind
verb 3
{% for route in phone_openvpn_routes %}
route {{ route.network }} {{ route.netmask }}
{% endfor %}
<cert>
{{ phone_openvpn_client_cert_raw.content | b64decode | trim }}
</cert>
<key>
{{ phone_openvpn_client_key_raw.content | b64decode | trim }}
</key>
delegate_to: localhost
vars:
ansible_connection: local
ansible_become: false
no_log: true
handlers:
- name: reload systemd
ansible.builtin.systemd:
daemon_reload: true
- name: restart phone openvpn
ansible.builtin.systemd:
name: "{{ phone_openvpn_service }}"
state: restarted