Skip to main content

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 | Install targets all:!localhost:!wireguard: every managed host except the wireguard group gets NordVPN installed.
  • NordVPN | Remove targets wireguard: any host in the wireguard group has NordVPN actively uninstalled, guaranteeing it is absent there.
  • NordVPN | Cleanup targets all:!localhost: fully purges NordVPN from any managed host on demand, reusing the same removal steps as NordVPN | 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.list one-line repository file.
  • Downloads and dearmors the NordVPN signing key into /usr/share/keyrings/nordvpn-keyring.gpg.
  • Adds the nordvpn deb822 repository (stable/main, signed-by the keyring above).
  • Installs the nordvpn package and enables/starts nordvpnd.service and nordvpnd.socket.
  • Whitelists the cluster CIDR and VPN CIDR (nordvpn whitelist add subnet) so cluster/VPN traffic bypasses the NordVPN tunnel.
  • Enables net.ipv4.ip_forward via sysctl and allows UFW routing (ufw route allow in on nordlynx), so traffic can be forwarded through the nordlynx interface.
  • Looks up the host's IP from Vault, resolves the network interface that carries it (interface_by_ip filter), and adds an *nat block to /etc/ufw/before.rules (inserted before the *filter table) that masquerades traffic from the Meshnet range 100.64.0.0/10 out 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 the nordvpn_meshnet_peer_hostnames filter, 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 nordvpn CLI 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.service and nordvpnd.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 nordvpn package (apt ... purge: true), autoremoves any dependencies left unused, and autocleans the local .deb package 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.rules and 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 plain nordvpn tag.

CI workflow​

The workflow (nordvpn.yml) runs on any runners, split into two independent jobs:

  • nordvpn-install: runs playbook.yml --tags "nordvpn_install".
  • nordvpn-remove: runs playbook.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