nordility
nordility is a standalone extraction of the nordility.py tooling from citegres, repackaged as a small Python project that is explicitly focused on automating NordVPN actions.
It keeps the original workflow:
- connect to NordVPN
- disconnect from NordVPN
- rotate to a new country/server group
It also adds a usable CLI, packaging metadata, and a small test suite.
Contributor Docs
- Architecture blueprint:
docs/contributor-architecture-blueprint.md - Diagram source:
docs/diagrams/repo-architecture.puml - Draw.io source:
docs/diagrams/repo-architecture.drawio - Rendered diagram targets:
docs/diagrams/repo-architecture.puml.pngdocs/diagrams/repo-architecture.puml.svg
Features
- Platform-agnostic VPN automation through two interchangeable backends: the
original
NordVPN.exeflow on Windows, and thenordvpnterminal CLI on Linux and macOS - Backend auto-detection, so the same commands work unchanged on each platform
- Fast and full country pools for randomized server rotation
- A NordVPN/WireGuard watch service that keeps private WireGuard access working after external NordVPN reconnects or rotates
- A local web control surface for private Caddy/mTLS access from Safari
- No third-party runtime dependencies
- Compatibility helpers that preserve the original function names
Platform support
nordility began as a Windows-first tool wrapping NordVPN.exe. It is now
platform-agnostic: the package installs and the CLI runs on Linux, macOS and
Windows, verified on each by CI on every push.
What limits a given command is rarely nordility itself. It drives whichever
NordVPN executable is present, so support follows two separate questions: does
this package run here, and is there a NordVPN binary for it to drive?
| Feature | Linux | macOS | Windows |
|---|---|---|---|
Package installs, CLI runs, --help works |
yes | yes | yes |
NordVPN.exe backend |
— | — | yes |
nordvpn CLI backend |
yes | if present | — |
web over TCP (status only) |
yes | yes | yes |
web --unix-socket (privileged actions) |
yes | yes | no |
login --token via KeePass |
yes | no | no |
watch-wireguard and its systemd units |
yes | no | no |
Backend selection is by executable path, not by OS: a path ending in .exe
selects the Windows backend, anything else selects the CLI backend. So macOS
resolves to the CLI backend and works wherever a nordvpn binary is on PATH
— NordVPN ships that CLI for Linux, so on macOS this depends on what you have
installed rather than on anything here.
The rest is the host OS having a facility or not. web --unix-socket needs
AF_UNIX, which Windows does not provide; run web without it for the
status-only TCP interface. watch-wireguard repairs Linux policy-routing rules
and installs systemd units, so it is inherently Linux-only. login --token
guards on sys.platform for the PTY and core-dump hardening it relies on.
Where a feature is unavailable the CLI reports it and exits, rather than failing obscurely.
Install
For the CLI and Python API:
python -m pip install nordility
On Linux, the optional KeePass-backed login also needs auto-pass, currently
installed from its source repository:
python -m pip install 'git+https://github.com/casonk/auto-pass.git'
The NordVPN application or nordvpn command must be installed separately.
The package wheel contains the Python CLI and API. The systemd installers and
tracked config examples below are repository files; clone the repository to
use them.
For development from a checkout:
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e .
Configure
By default, nordility uses:
- Windows backend:
C:/Program Files/NordVPN/NordVPN.exe - CLI backend:
nordvpn
You can override the executable with either environment variable. On Linux or macOS:
export NORDILITY_EXECUTABLE="/usr/bin/nordvpn"
export NORDILITY_BACKEND="cli"
On Windows:
$env:NORDILITY_EXECUTABLE = "C:/Program Files/NordVPN/NordVPN.exe"
$env:NORDILITY_BACKEND = "windows"
Neither is usually needed: auto infers the backend from the executable path.
The accepted backends are:
autowindowscli
auto infers windows for .exe executables and cli otherwise.
For auto-pass-backed login defaults in a repository checkout, copy
config/auto-pass.example.ini to
config/auto-pass.ini. The CLI will use that file as the default
--keepass-profile and --keepass-entry source for login,
connect --auto-login, and change --auto-login. With a wheel installation,
pass those options explicitly and configure auto-pass through its documented
environment variables. The config file and sibling-checkout discovery are
conveniences for the repository deployment, not prerequisites for the core
package.
Literal --token input is intentionally unsupported because it exposes the
credential in the caller's process arguments. On Linux, KeePass-resolved
tokens pass over a private stdin pipe to nordility.token_login; that helper
validates the root-owned official CLI, disables core dumps/process dumpability,
and launches nordvpn login --token in a PTY with no positional credential.
It verifies consent is disabled, waits for NordVPN 5.2's exact prompt and
disabled ECHO/ECHONL, and only then reads, forwards, and wipes the token
while discarding all child output. Prompt drift fails closed; there is no
credential-bearing fallback. The token is also redacted from caller errors and
results.
Usage
nordility connect
nordility login
nordility disconnect
nordility change --speed fast
nordility change --group United_States
nordility watch-wireguard --once
nordility web --host 127.0.0.1 --port 5300 # status only; actions disabled
nordility list-groups --speed full
If you do not install the package, you can still run it from the repo root:
PYTHONPATH=src python -m nordility change --speed fast
Python API
from nordility import change_vpn_server, connect_vpn_server, disconnect_vpn_server
print(connect_vpn_server())
print(change_vpn_server(speed="fast"))
print(disconnect_vpn_server())
For more control, use NordVPNClient directly.
NordVPN/WireGuard Watch Service
NordVPN reconnects and server rotations can flush Linux policy-routing rules.
If this host is also serving a private WireGuard tunnel, that can route phone
handshake replies through nordlynx instead of the real gateway.
Run one repair pass:
PYTHONPATH=src python -m nordility --backend cli watch-wireguard --once
Install the resident systemd watcher:
sudo ./scripts/install_wireguard_watch_service.sh
sudo systemctl status nordility-wireguard-watch.service --no-pager
The watcher detects NordVPN status/NordLynx endpoint changes and routing drift,
starts wg0 via wg-quick@wg0.service if /etc/wireguard/wg0.conf exists
but the interface is down, then derives a config-owned interface allowlist
before it reads or resets peer endpoints. It reapplies the user-managed
WireGuard socket fwmark plus:
ip rule add fwmark 51820 lookup main priority 100 protocol 196
It only changes WireGuard interfaces backed by /etc/wireguard/<iface>.conf,
and it rejects the daemon-managed nordlynx name even if a confusingly named
config file exists. A repair is reported as successful only when the interface
fwmark mutation succeeds and the exact priority-100, fwmark-51820,
lookup-main, protocol-196 rule is already present or is added successfully.
The numeric protocol tag is Nordility's ownership marker, so rollback deletes
only the rule Nordility created. Rules at another priority, for another mark,
table, or protocol, or with a partial mark mask do not satisfy that check.
Before installing the global rule, Nordility inventories every active
WireGuard fwmark and refuses a collision on any interface outside the explicit
mutation allowlist. A newly added rule is rolled back if no target interface
can be marked.
Interface startup is systemd-only. Nordility does not fall back to raw
wg-quick up, because doing so would bypass security drop-ins attached to the
wg-quick@<interface>.service activation boundary.
Private Web Control
The privileged web control surface is served only over a root-created Unix
socket intended for wiring-harness Caddy/mTLS:
sudo ./scripts/install_web_service.sh
sudo systemctl status nordility-web.service --no-pager
Default backend:
- Local upstream:
unix//run/nordility/web.sock(root:caddy, mode0660) - Private Caddy URL:
https://nordility.clockwork.internal
TCP mode is deliberately status-only: POST /api/action returns 403 even on
loopback. Unix-socket actions additionally require the exact configured HTTPS
Origin, matching Host, and application/json; this prevents loopback
bypass and browser CSRF from replacing Caddy/mTLS as the authorization boundary.
The page exposes power on/off, fast/full rotation, and built-in country
selection. Installed root services deliberately do not accept --auto-login
or KeePass options: authenticate the official NordVPN client separately before
starting them. After each VPN action the web service runs the same WireGuard
repair path used by the watcher so wg0 remains available for phone access.
All three systemd installers stage an explicit Python-module allowlist at
/opt/nordility with root ownership and non-writable modes. Root services never
execute Python from the user-writable Git checkout.
After adding nordility.clockwork.internal to the local wiring-harness
service registry, refresh the shared certificate SANs and Caddy config:
[[services]]
name = "nordility"
description = "NordVPN outbound control surface"
owner_repo = "./util-repos/nordility"
hostname = "nordility.clockwork.internal"
access_mode = "shared-mtls"
ingress = "wiring-harness-caddy"
unix_socket = "/run/nordility/web.sock"
cd ../wiring-harness
WH_WG_IP=10.99.0.1 bash scripts/setup-mtls.sh --refresh-server
sudo python3 scripts/setup_caddy.py --provision
Tests
PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=src python3 -m unittest discover -s tests -v
bash tests/test_token_login_podman.sh
The container test runs the Linux-only helper against a deliberately delayed no-echo prompt and checks that accepted and rejected credentials do not escape through process arguments, the environment, or helper output.
Metadata
Release files for nordility 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| nordility-0.1.0.tar.gz | 51.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| nordility-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 85.2 kB
Release files / nordility-0.1.0.tar.gz
| Download URL | nordility-0.1.0.tar.gz |
|---|---|
| Size | 51.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6f9f843a15ebb26ba1642ef46a092b57884bdccfc6ac3749246ef4ff107b3756
|
|
BLAKE2b-256 checksum How to use checksums |
664449079b62cd4ede510be8dbf718b6edbbbcfcd309448ab5a31fb2620f90a7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.
Transparency logRelease files / nordility-0.1.0-py3-none-any.whl
| Download URL | nordility-0.1.0-py3-none-any.whl |
|---|---|
| Size | 33.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ba0d48b0b7f3ce2fc5bd3db398a280afdde9eb6618df539d985521f81be754cc
|
|
BLAKE2b-256 checksum How to use checksums |
5367acd4863c5a0729eec3f0faf1412bb3685909a1c41d29372e9a5cb9cdfc25
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.
Transparency log