Files
home-network-k3s-impl/infra/ansible/playbooks/vpn.egress.gateway.yml
T
jakman85andClaude Opus 5 60e3f09c67 feat(vpn): prod egress-gateway group — k3s-prod-3, singleton, opt-in only
`[vpn_egress_gateway_prod]` is a SEPARATE inventory group with its own vaulted
credentials, not a second member of `[vpn_egress_gateway]`. That group is the
default target of playbooks/vpn.egress.gateway.yml, so adding a prod host to it
would mean a flagless `ansible-playbook playbooks/vpn.egress.gateway.yml`
silently reconfigures a prod node's kill-switch datapath. Prod is reached only by
asking for it, with `--list-hosts` first:

  -e vpn_egress_gateway_hosts=vpn_egress_gateway_prod --limit k3s-prod-3

Node 3 because node 1's SSD was replaced yesterday (least operational history)
and node 2 carries the §0b memattr history. Exactly one node, forever: Cilium OSS
picks "the first node in lexical ordering" among nodeSelector matches, so a
labelled k3s-prod-1 would silently steal the gateway role — and the public egress
IP — from k3s-prod-3, with no event and no alert.

vault.yml is a verbatim copy of the dev group's CIPHERTEXT: same PIA account per
the operator, and copying the encrypted blob means the plaintext was never
decrypted to disk, through a shell, or into a transcript. The two files are now
independent — rotating means re-encrypting both, which is the right behaviour for
two environments.

Also corrects a stale inventory note that claimed k3s-prod-3 has wlp1s0 up on
192.168.2.195. Re-verified before choosing it as the gateway, because a second
live uplink in a kill-switch datapath is not a footnote: there is no wl* netdev on
the node at all, no phy80211 device, the /etc/network/interfaces wlp1s0 stanza is
commented out, 192.168.2.195 is assigned nowhere, and the only IPv4 addresses are
lo and eno1 192.168.2.12. Nothing needed bringing down.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-12 17:38:46 -05:00

39 lines
2.4 KiB
YAML

---
# Stand up the node-level VPN egress tunnel on the designated egress-gateway node(s)
# (ADR-0015). Host infra — the split-tunnel OpenVPN service + policy routing + scoped
# nftables kill-switch. The CiliumEgressGatewayPolicy that steers vpn-class pods to this
# node's tun0 is authored separately as GitOps (platform/), against the same contract:
# * inventory group `vpn_egress_gateway` (this play's default target)
# * k8s node label homelab.keithjackson.dev/egress-gateway=true (set by the role)
# * host tunnel iface tun0 (split-tunnel; never the node default route)
#
# Secrets come from Ansible vault (inventory/group_vars/vpn_egress_gateway/vault.yml):
# vault_vpn_egress_gateway_pia_username, vault_vpn_egress_gateway_pia_password, vault_vpn_egress_gateway_pia_ovpn
# Run: ansible-playbook playbooks/vpn.egress.gateway.yml
#
# TARGET (R-ENV-02): the group is a var, defaulting to dev's `vpn_egress_gateway`. PROD has
# its own group as of 2026-09-12 — `vpn_egress_gateway_prod` (k3s-prod-3), with its own
# vaulted credentials in group_vars/vpn_egress_gateway_prod/:
#
# ansible-playbook playbooks/vpn.egress.gateway.yml \
# -e vpn_egress_gateway_hosts=vpn_egress_gateway_prod --limit k3s-prod-3 --list-hosts
#
# The var is `vpn_egress_gateway_hosts`, NOT the cluster-wide `k3s_hosts`. Prod is
# deliberately NOT a member of the default group, so a flagless run cannot reach it — and
# `--list-hosts` before every run is the habit that catches a mistyped flag, which in this
# repo has silently targeted all three dev nodes before.
#
# THE LABEL IS WHAT ARMS THE DATAPATH. The CiliumEgressGatewayPolicy is already reconciled
# on both clusters, so the instant a node carries
# `homelab.keithjackson.dev/egress-gateway=true` steering goes live. The role sequences
# kill-switch -> tunnel -> re-steer -> label, with the label LAST, on purpose. Never apply
# that label by hand, and never run this play with tags that skip the tunnel or kill-switch
# tasks. Exactly ONE node per cluster may carry it (Cilium OSS picks the lexically first
# match, so a labelled k3s-prod-1 would silently steal the role from k3s-prod-3).
- name: Configure the node-level VPN egress gateway
hosts: "{{ vpn_egress_gateway_hosts | default('vpn_egress_gateway') }}"
become: true
gather_facts: true # package module + service facts need os facts
roles:
- role: vpn_egress_gateway