Skip to content

Device & Access Restrictions

Every client connection is checked against a registered name=mac allowlist (openvpn_db.txt) in addition to normal certificate authentication, via a client-connect script. --add-user registers each client this way; a client-connect script (openvpn-mac-addr-check.py, already wired into server.conf) checks the connecting certificate’s CN against the device’s MAC address (via IV_HWADDR, which requires push-peer-info – already set in every generated .ovpn) on every connection attempt, and rejects anything that doesn’t match.

This adds a device-binding layer on top of normal certificate authentication: a stolen or copied .ovpn file alone isn’t enough to connect from an unregistered device.

On top of MAC binding, each client can optionally be restricted by:

Restriction How it’s checked
Allowed countries One or more ISO 3166-1 countries (or unrestricted); verified via GeoIP against the connecting IP.
Allowed cities One or more city names, verified via GeoIP (GeoLite2-City).
Allowed networks (ASN) One or more autonomous system numbers, verified via GeoIP (GeoLite2-ASN) – identifies the connecting ISP/network rather than a physical location.
Allowed IP addresses One or more single IPs or CIDR ranges, checked directly against the connecting IP – no GeoIP database needed.
Allowed device OS A subset of windows / linux / mac, matched against OpenVPN’s own IV_PLAT. Leaving this empty means unrestricted – it does not mean “block everything.”
Monthly bandwidth quota A soft cutoff in GB, checked only at connection time. A session that goes over quota mid-connection is not killed – it just can’t reconnect once the quota is used up. Resets on the 1st of each calendar month, server-local time.

Every restriction is entirely optional per client, and all are orthogonal to the MAC-binding check (which always applies regardless). Country / city / ASN / IP are collectively “Location & Network Restrictions” – the exact same restriction types a linked portal user’s own Login Restrictions offer, and creating or editing a user with Login Restrictions enabled automatically syncs those values onto their linked VPN profile.

The VPN Clients page’s Manage Restrictions dialog exposes every setting above per client, grouped into “Device & Access Policy” (OS, bandwidth) and “Location & Network Restrictions” (country, city, ASN, IP) – plus a best-effort “used this month” indicator when a quota is set.

Manage Restrictions dialog on the VPN Clients page

The country list is a static, self-contained dataset (no external API call at runtime); city and ASN use cascading “pick a country, search its real GeoIP values” pickers, so you can only select a value that actually exists in the database.

Enforcement happens on the OpenVPN host itself, in scripts under host-scripts/ (installed to /etc/openvpn/server/):

File (deployed to /etc/openvpn/server/) server.conf directive
openvpn-mac-addr-check.py client-connect /etc/openvpn/server/openvpn-mac-addr-check.py
openvpn-client-disconnect.py client-disconnect /etc/openvpn/server/openvpn-client-disconnect.py
policy_lib.py imported by both scripts above – no server.conf entry needed

Automated (recommended): the Python installer handles all of this itself. A fresh install (app/cli/openvpn_admin.py install, or the web admin’s OpenVPN Install page) deploys and wires everything above automatically, before OpenVPN’s first start. An already-installed server can be brought up to date with:

Terminal window
sudo python3 app/cli/openvpn_admin.py install-host-scripts # stage files + server.conf change only
sudo python3 app/cli/openvpn_admin.py install-host-scripts --restart # ...and restart the service now

Both forms are idempotent. Without --restart, the server.conf change is staged but not yet active – restart at a chosen maintenance window, since it drops every currently-connected client.

The connect script checks, in order, once identity is established by the MAC check: OS → country → city → ASN → IP address → bandwidth quota. Each rejection is logged with a machine-readable reason (mac_mismatch, os_not_allowed, country_not_allowed, country_lookup_failed, and so on), visible via vpn-status.py --rejected-connections and the web admin’s Diagnostics page.

Two JSON files under /etc/openvpn/server/policy/ (paths configurable via vpn-tools.conf’s CLIENT_POLICY_FILE/CLIENT_USAGE_FILE):

  • client_policy.json – admin-configured restrictions, keyed by client name:

    {
    "alice": {"allowed_countries": ["PK", "AE"], "allowed_cities": ["Karachi"], "allowed_os": ["windows", "linux"], "bandwidth_monthly_gb": 5},
    "bob": {"allowed_asns": ["AS15169"], "allowed_ips": ["203.0.113.5", "10.0.0.0/24"], "bandwidth_monthly_gb": 10}
    }

    A client absent from this file, or present with an empty object, is fully unrestricted (only the MAC check applies).

  • client_usage.json – monthly bandwidth usage, keyed by client name, written only by openvpn-client-disconnect.py:

    {"alice": {"period_start": "2026-08-01", "bytes_used": 1073741824}}

Both files use an atomic write-to-tmp-then-rename pattern with flock-based locking, so the app, the CLI, and the connect/disconnect scripts can all touch them concurrently without corruption.

Country, city, and ASN restrictions need the matching MaxMind GeoLite2 database on the OpenVPN host, which requires a free MaxMind account.

  1. Sign up at maxmind.com/en/geolite2/signup and generate a license key.

  2. Add it to /etc/openvpn/vpn-tools.conf:

    MAXMIND_LICENSE_KEY=your_key_here
  3. Run sudo bash geoip-update.sh once to fetch the database(s) – defaults to /etc/openvpn/server/GeoLite2-Country.mmdb / -City.mmdb / -ASN.mmdb.

  4. Install the weekly refresh timer:

    Terminal window
    cp systemd/openvpn-geoip-update.{service,timer} /etc/systemd/system/
    systemctl enable --now openvpn-geoip-update.timer

    Safe to install before steps 1-2 – the update script detects a missing license key and exits cleanly instead of erroring.

  5. Install the pure-Python GeoIP reader on the OpenVPN host:

    Terminal window
    python3 -m pip install geoip2
  6. Pick countries/cities/ASNs/IPs per client from the Manage Restrictions dialog on the VPN Clients page – no deployment-wide setting to configure, every client is independent.

IP address restriction needs none of this – it’s a pure ipaddress stdlib comparison against the connecting IP, no MaxMind database involved.