NordVPN
Install and configure NordVPN with Meshnet networking.
NordVPN must never coexist with the Wireguard VPN server on the same host: running both VPN stacks on one box conflicts over routing/firewall control. The play is therefore split in two:
NordVPN | Installtargetsall:!localhost:!wireguard: every managed host except thewireguardgroup gets NordVPN installed.NordVPN | Removetargetswireguard: any host in thewireguardgroup has NordVPN actively uninstalled, guaranteeing it is absent there.NordVPN | Cleanuptargetsall:!localhost: fully purges NordVPN from any managed host on demand, reusing the same removal steps asNordVPN | Remove.
The cluster CIDR and VPN CIDR (used for whitelisting) come from Vault; see Networks under the Vault data source.
Ansible hosts group: all:!localhost:!wireguard (install) / wireguard (remove) / all:!localhost (cleanup)
Tasks
NordVPN | Install (nordvpn, nordvpn_install)
- Removes the legacy, unsigned
/etc/apt/sources.list.d/nordvpn-app.listone-line repository file. - Downloads and dearmors the NordVPN signing key into
/usr/share/keyrings/nordvpn-keyring.gpg. - Adds the
nordvpndeb822 repository (stable/main,signed-bythe keyring above). - Installs the
nordvpnpackage and enables/startsnordvpnd.serviceandnordvpnd.socket. - Whitelists the cluster CIDR and VPN CIDR (
nordvpn whitelist add subnet) so cluster/VPN traffic bypasses the NordVPN tunnel. - Enables
net.ipv4.ip_forwardvia sysctl and allows UFW routing (ufw route allow in on nordlynx), so traffic can be forwarded through thenordlynxinterface. - Looks up the host's IP from Vault, resolves the network interface that carries it (
interface_by_ipfilter), and adds an*natblock to/etc/ufw/before.rules(inserted before the*filtertable) that masquerades traffic from the Meshnet range100.64.0.0/10out of that interface, then reloads UFW. - Disables the NordVPN app firewall (
nordvpn set firewall off), since UFW already manages host firewalling. - Disables the NordVPN app's real-time protection (
nordvpn set protection 0). - Checks account login status (
nordvpn account); the following meshnet steps are skipped entirely when the host isn't logged in. - If logged in, enables Meshnet (
nordvpn set meshnet on) and sets the device's Meshnet nickname to{{ ansible_facts['hostname'] }}-nord(nordvpn meshnet set nickname). - Refreshes the peer list (
nordvpn meshnet peer refresh), then reads it back (nordvpn meshnet peer list) and parses it with thenordvpn_meshnet_peer_hostnamesfilter, which returns every peer's Meshnet hostname (e.g.arpan.rec-andes.nord) excluding the local device's own entry. - For every discovered peer hostname, grants Meshnet permissions: fileshare, incoming traffic, local network access, and routing (
nordvpn meshnet peer <permission> allow <peer-hostname>). This runs for all peers on the account, not just other cluster nodes. - Each
nordvpnCLI step is idempotent: it inspects the command's stdout to tell an already-applied state (e.g. "already on the allowlist", "already set to", "already enabled", "already set for this device", "already allowed") from a real change, and from an actual failure.
NordVPN | Remove (nordvpn, nordvpn_remove)
- Stops and disables
nordvpnd.serviceandnordvpnd.socket; tolerates hosts where NordVPN was never installed (the units don't exist) by ignoring the module's "Could not find the requested service" error and failing on anything else. - Purges the
nordvpnpackage (apt ... purge: true), autoremoves any dependencies left unused, and autocleans the local.debpackage cache. - Deletes the repository/keyring files:
/etc/apt/sources.list.d/nordvpn-app.list,/usr/share/keyrings/nordvpn-keyring.asc,/etc/apt/sources.list.d/nordvpn.sources. - Removes the Meshnet NAT masquerade block from
/etc/ufw/before.rulesand reloads UFW.
NordVPN | Cleanup (dangerously_cleanup_nordvpn)
Destructive. Only runs when this exact tag is passed explicitly (it is intentionally excluded
from the plain nordvpn tag). Imports the same tasks/nordvpn/remove.yml steps as
NordVPN | Remove above, but targets all:!localhost, purging NordVPN from any managed host on
demand.
Tags
nordvpn: Run NordVPN install (non-wireguard hosts) and removal (wireguard hosts).nordvpn_install: Run NordVPN install only (non-wireguard hosts).nordvpn_remove: Run NordVPN removal only (wireguard hosts).dangerously_cleanup_nordvpn: Destructive. Fully purges NordVPN from any managed host (all:!localhost). Never included by the plainnordvpntag.
CI workflow
The workflow (nordvpn.yml) runs on any runners, split into two independent jobs:
nordvpn-install: runsplaybook.yml --tags "nordvpn_install".nordvpn-remove: runsplaybook.yml --tags "nordvpn_remove".
The two jobs target disjoint host groups (all:!localhost:!wireguard vs wireguard), so they run in parallel with no needs: dependency between them.
Deployment
uv sync --all-extras --all-packages --no-progress
uv --offline run --no-sync --no-progress ansible-galaxy install -r requirements.yml
# Run both install and remove
uv --offline run --no-sync --no-progress ansible-playbook playbook.yml --tags nordvpn
# Or run a single phase
uv --offline run --no-sync --no-progress ansible-playbook playbook.yml --tags nordvpn_install
uv --offline run --no-sync --no-progress ansible-playbook playbook.yml --tags nordvpn_remove