diff --git a/infra/ansible/inventory/group_vars/vpn_egress_gateway_prod/vars.yml b/infra/ansible/inventory/group_vars/vpn_egress_gateway_prod/vars.yml new file mode 100644 index 0000000..19e2ed4 --- /dev/null +++ b/infra/ansible/inventory/group_vars/vpn_egress_gateway_prod/vars.yml @@ -0,0 +1,22 @@ +--- +# Non-secret group_vars for the PROD egress gateway (k3s-prod-3). Credentials live in the +# vaulted vault.yml beside this file. + +# PROD DIALS ITS OWN PIA SERVER — and it has to. +# +# The vaulted .ovpn pins ONE server, 158.173.241.112. Measured 2026-09-12: with dev +# (k3s-dev-3) and prod (k3s-prod-3) both dialling it, PIA handed BOTH sessions the SAME +# tunnel address — `Server-10779-2a ... ifconfig 10.72.192.7` in both journals. Each +# reconnect steals that address from the other; the loser then sees no data for 60s, hits +# `--ping-restart`, reconnects and steals it back. Both tunnels flapped on a ~2-minute cycle +# (dev 22:57 / 22:59 / 23:01 UTC, prod interleaved a minute off) and neither environment had +# reliable vpn-class egress. This is NOT the account's connection limit — that has room. +# +# 158.173.241.135 is another member of the same PIA Stockholm LB pool: `sweden.privacy.network` +# resolved from k3s-prod-3 to 158.173.241.{34,55,112,135,239}. Any server in the pool accepts +# the same credentials and the same inline CA, so only the address differs. Dev keeps the +# .ovpn's pinned .112 untouched, which is why the override lives HERE and not in the role. +# +# A public server address is not a credential, so this is plain group_vars, not vault. +# If prod is ever moved to its own PIA account, this can go away again. +vpn_egress_gateway_pia_remote: "158.173.241.135 1198" diff --git a/infra/ansible/roles/vpn_egress_gateway/defaults/main.yml b/infra/ansible/roles/vpn_egress_gateway/defaults/main.yml index 359d1ea..817ad04 100644 --- a/infra/ansible/roles/vpn_egress_gateway/defaults/main.yml +++ b/infra/ansible/roles/vpn_egress_gateway/defaults/main.yml @@ -20,6 +20,23 @@ vpn_egress_gateway_pia_password: "{{ vault_vpn_egress_gateway_pia_password }}" # The FULL text of the PIA .ovpn (inline /, cipher/auth, remote line). vpn_egress_gateway_pia_ovpn: "{{ vault_vpn_egress_gateway_pia_ovpn }}" +# OVERRIDE THE .ovpn's PINNED `remote` — " ", empty to honour the .ovpn. +# +# WHY THIS EXISTS (measured 2026-09-12): the operator's .ovpn pins ONE PIA server IP. Point +# two gateways at the same vaulted .ovpn and both dial the SAME server, and PIA hands BOTH +# THE SAME tunnel address — dev and prod were both `Server-10779-2a` +# (158.173.241.112:1198) with `ifconfig 10.72.192.7`. Each reconnect then steals that +# address from the other, the loser sees no data for 60s, hits `--ping-restart`, reconnects +# and steals it back: both tunnels flapped on a ~2-minute cycle and neither was reliably +# up. It is NOT a connection-LIMIT problem (the account has room) — it is one pinned server. +# +# So each environment's gateway needs its own server. The PIA Stockholm LB pool +# (`sweden.privacy.network`) resolved to 158.173.241.{34,55,112,135,239}; any PIA server in +# the pool accepts the same credentials and CA, so this is a per-environment knob rather +# than a second vaulted .ovpn. NOT a secret — a public server address — so it belongs in +# group_vars//vars.yml, not the vault. +vpn_egress_gateway_pia_remote: "" + # --- on-disk layout ------------------------------------------------------------- vpn_egress_gateway_config_dir: /etc/openvpn/client vpn_egress_gateway_scripts_dir: /etc/openvpn/scripts diff --git a/infra/ansible/roles/vpn_egress_gateway/tasks/main.yml b/infra/ansible/roles/vpn_egress_gateway/tasks/main.yml index 401a13f..f9a7761 100644 --- a/infra/ansible/roles/vpn_egress_gateway/tasks/main.yml +++ b/infra/ansible/roles/vpn_egress_gateway/tasks/main.yml @@ -28,9 +28,18 @@ - "{{ vpn_egress_gateway_config_dir }}" - "{{ vpn_egress_gateway_scripts_dir }}" +# `vpn_egress_gateway_pia_remote`, when set, rewrites ONLY the `remote ` line so +# this environment's gateway dials its own PIA server. Two gateways sharing the .ovpn's single +# pinned server are handed the SAME tunnel address and evict each other every ~2 minutes — see +# the variable's comment in defaults/main.yml. The regex is anchored to a line that STARTS with +# `remote` followed by whitespace, so `remote-cert-tls` / `remote-random` are untouched. - name: Deploy the operator's proven PIA .ovpn (from Ansible vault) ansible.builtin.copy: - content: "{{ vpn_egress_gateway_pia_ovpn }}" + content: >- + {{ vpn_egress_gateway_pia_ovpn if (vpn_egress_gateway_pia_remote | length == 0) + else (vpn_egress_gateway_pia_ovpn + | regex_replace('(?m)^[ \t]*remote[ \t]+\S+.*$', + 'remote ' ~ vpn_egress_gateway_pia_remote)) }} dest: "{{ vpn_egress_gateway_ovpn_path }}" owner: root group: root