CLI Reference
Every action in openvpn-install.sh’s interactive menu is also available
as a flag, so clients can be added/revoked/listed from automation, not just
a terminal prompt. Every output example on this page is real, captured from
a live deployment – IPs and MAC addresses have been swapped for
documentation-reserved placeholders (203.0.113.x, aa:bb:cc:dd:ee:xx);
everything else, including client names and counts, is unmodified.
openvpn-install.sh
Section titled “openvpn-install.sh”sudo bash openvpn-install.sh --add-user NAME MAC # e.g. --add-user alice aa:bb:cc:dd:ee:ffsudo bash openvpn-install.sh --revoke-user NAMEsudo bash openvpn-install.sh --list-users # valid clients + db registration statussudo bash openvpn-install.sh --list-revoked-users # revoked clients, when, stale db entriessudo bash openvpn-install.sh --macs NAME # every MAC address registered for one clientsudo bash openvpn-install.sh --add-mac NAME MAC # register an extra device MAC for an existing clientsudo bash openvpn-install.sh --remove-mac NAME MAC # remove one MAC registration (client keeps its cert)sudo bash openvpn-install.sh --show-ovpn NAME # print an existing client's .ovpn config to stdoutsudo bash openvpn-install.sh --purge-revoked NAME # permanently delete a revoked client's leftover PKI/.ovpn filessudo bash openvpn-install.sh --restore NAME MAC # reissue a brand-new cert under a revoked client's namesudo bash openvpn-install.sh --check-certs # cross-check PKI certs vs openvpn_db.txtsudo bash openvpn-install.sh --lint-mac-db # validate openvpn_db.txt formatting/healthsudo bash openvpn-install.sh --help--check-certs and --lint-mac-db exit 0 when clean and 1 when they
find a problem, so they’re monitoring/CI-friendly.
--list-users
Section titled “--list-users”sudo bash openvpn-install.sh --list-usersNAME IN_DB MACSasif yes 1umair yes 2umer yes 1saad yes 1ahsan yes 1waqas yes 1majid yes 1talha yes 3ghayoor yes 4aliyan yes 1headoffice yes 1waleed yes 2--list-revoked-users
Section titled “--list-revoked-users”sudo bash openvpn-install.sh --list-revoked-usersNAME REVOKED_AT STALE_DB_ENTRY FILES_PRESENThabib 2026-03-26 19:46:27 UTC no no (already purged)Ghayoor 2026-03-27 11:03:13 UTC no no (already purged)test 2026-08-13 06:42:43 UTC no no (already purged)# (trimmed -- a real deployment accumulates one row per revocation, including# any test/throwaway clients created along the way)--macs NAME
Section titled “--macs NAME”sudo bash openvpn-install.sh --macs asif1 MAC address(es) registered for 'asif': - aa:bb:cc:dd:ee:ff--check-certs
Section titled “--check-certs”sudo bash openvpn-install.sh --check-certsOK -- every valid cert has a matching db entry and vice versa.--lint-mac-db
Section titled “--lint-mac-db”sudo bash openvpn-install.sh --lint-mac-dbOK -- /etc/openvpn/server/openvpn_db.txt is well-formed (19 entries).JSON output
Section titled “JSON output”Add --json to --list-users, --list-revoked-users, --macs,
--check-certs, or --lint-mac-db to get structured JSON instead of a
table. Argument order doesn’t matter. It’s rejected with a clear error on
every other command:
sudo bash openvpn-install.sh --list-users --json[{"name":"asif","in_db":true,"mac_count":1},{"name":"umair","in_db":true,"mac_count":2},{"name":"umer","in_db":true,"mac_count":1},{"name":"saad","in_db":true,"mac_count":1},{"name":"ahsan","in_db":true,"mac_count":1},{"name":"waqas","in_db":true,"mac_count":1},{"name":"majid","in_db":true,"mac_count":1},{"name":"talha","in_db":true,"mac_count":3},{"name":"ghayoor","in_db":true,"mac_count":4},{"name":"aliyan","in_db":true,"mac_count":1},{"name":"headoffice","in_db":true,"mac_count":1},{"name":"waleed","in_db":true,"mac_count":2}]sudo bash openvpn-install.sh --macs asif --json{"name":"asif","count":1,"macs":["aa:bb:cc:dd:ee:ff"]}sudo bash openvpn-install.sh --check-certs --json{"clean":true,"orphan_pki":[],"orphan_db":[]}sudo bash openvpn-install.sh --lint-mac-db --json{"clean":true,"entries":19,"trailing_newline_ok":true,"issues":[]}sudo bash openvpn-install.sh --add-user alice aa:bb:cc:dd:ee:ff --json--json option is not allowed with this command.Per-client restriction flags
Section titled “Per-client restriction flags”A subset of restrictions (see Device & Access Restrictions for the full picture) also has CLI equivalents:
sudo bash openvpn-install.sh --set-country alice PK # restrict alice to country code PK, or ANY to clearsudo bash openvpn-install.sh --set-os alice windows,linux # restrict alice's allowed OS, or ANY to clearsudo bash openvpn-install.sh --set-bandwidth alice 5 # 5 GB/month quota for alice, or ANY to clearsudo bash openvpn-install.sh --get-policy alice # show alice's current policysudo bash openvpn-install.sh --get-policy # show every client's policyCity/ASN/IP restrictions have no CLI equivalent yet – a known, documented
gap, not an oversight. Set those from the web admin’s “Manage Restrictions”
dialog, or by editing client_policy.json’s allowed_cities/
allowed_asns/allowed_ips directly.
--get-policy
Section titled “--get-policy”sudo bash openvpn-install.sh --get-policy asifPolicy for asif: {}An empty {} means no restriction of any kind is set for that client –
only the MAC-binding check applies. A client with restrictions set shows
them inline:
sudo bash openvpn-install.sh --get-policyAll client policies: {"testuser": {"allowed_os": ["linux", "mac"], "bandwidth_weekly_gb": 1.0, "country": "AG"}}country (singular) here is the pre-multi-country field format –
policy_store.py reads it as a one-item allowed_countries list
automatically, no manual migration needed (see Device & Access
Restrictions).
vpn-status.py
Section titled “vpn-status.py”python3 vpn-status.py # who's connected right nowpython3 vpn-status.py --all-clients # every known client: online / offline / revoked, last-seenpython3 vpn-status.py --rejected-connections # last 20 MAC-mismatch rejections (--rejected-connections N for a different count)python3 vpn-status.py --json # any of the above as JSONNeither openvpn-install.sh nor vpn-status.py needs to be run as root –
both escalate internally via sudo for the handful of files that require
it (the live status log, the PKI index), so a regular sudo-capable user
account is enough.
Who’s connected right now
Section titled “Who’s connected right now”python3 vpn-status.pyNAME SOURCE_IP MAC OS RX TX CONNECTED_SINCE DURATION---------- ------------- ----------------- ----------------------------------- ------ ------- ------------------- ---------headoffice 203.0.113.10 aa:bb:cc:dd:ee:01 Microsoft_Windows_10_Pro_10.0.19045 13.8MB 125.2MB 2026-08-14 14:53:04 0d 1h 29m
Note: 'OS' = device platform reported by the client via push-peer-info, not an OSlogin username (OpenVPN doesn't expose that). 'n/a' means no matched connectionwith peer-info has been logged yet for this client.python3 vpn-status.py --json[ { "name": "headoffice", "source_ip": "203.0.113.10", "bytes_received": 14507323, "bytes_sent": 131280375, "connected_since": "2026-08-14 14:53:04", "connected_since_epoch": 1786719184, "duration_seconds": 5433, "mac": "aa:bb:cc:dd:ee:01", "os": "Microsoft_Windows_10_Pro_10.0.19045", "duration": "0d 1h 30m", "bytes_received_h": "13.8MB", "bytes_sent_h": "125.2MB" }]Every known client
Section titled “Every known client”python3 vpn-status.py --all-clientsNAME STATUS MAC OS LAST_SEEN---------- ------- ----------------- ----------------------------------- -------------------ahsan offline aa:bb:cc:dd:ee:02 Microsoft_Windows_11_Pro_10.0.22621 2026-08-14T07:21:55aliyan offline aa:bb:cc:dd:ee:03 linux 2026-08-13T12:52:52asif offline aa:bb:cc:dd:ee:ff linux 2026-08-13T19:21:37ghayoor offline aa:bb:cc:dd:ee:04 macOS 26.5.2 (25F84) 2026-07-24T14:07:52habib revoked aa:bb:cc:dd:ee:05 linux 2026-03-24T17:46:15headoffice online aa:bb:cc:dd:ee:01 Microsoft_Windows_10_Pro_10.0.19045 now (connected)majid offline aa:bb:cc:dd:ee:06 Microsoft_Windows_11_Pro_10.0.26200 2026-08-13T13:31:40saad offline aa:bb:cc:dd:ee:07 Microsoft_Windows_11_Pro_10.0.26200 2026-08-13T09:42:35talha offline aa:bb:cc:dd:ee:08 macOS 15.6.1 (24G90) 2026-08-07T08:25:28umair offline aa:bb:cc:dd:ee:09 linux 2026-08-13T13:10:11umer offline aa:bb:cc:dd:ee:0a linux 2026-06-16T12:08:30waleed offline aa:bb:cc:dd:ee:0b n/a neverwaqas offline aa:bb:cc:dd:ee:0c Microsoft_Windows_11_Pro_10.0.26200 2026-08-05T09:20:23
Note: 'OS' = device platform reported by the client via push-peer-info, not an OSlogin username (OpenVPN doesn't expose that). 'n/a' means no matched connectionwith peer-info has been logged yet for this client.Rejected connection attempts
Section titled “Rejected connection attempts”python3 vpn-status.py --rejected-connections 5TIMESTAMP CLAIMED_NAME REASON MAC_PRESENTED MAC_REGISTERED OS SOURCE_IP:PORT TOTAL_ATTEMPTS------------------- ------------ ------------ ----------------- ----------------- ----- ------------------- --------------2026-08-13T15:14:35 asif mac_mismatch 00:00:00:00:00:00 aa:bb:cc:dd:ee:ff linux 203.0.113.20:54500 432026-08-13T15:14:32 asif mac_mismatch 00:00:00:00:00:00 aa:bb:cc:dd:ee:ff linux 203.0.113.20:58410 432026-08-13T15:14:30 asif mac_mismatch 00:00:00:00:00:00 aa:bb:cc:dd:ee:ff linux 203.0.113.20:34625 432026-08-13T15:14:28 asif mac_mismatch 00:00:00:00:00:00 aa:bb:cc:dd:ee:ff linux 203.0.113.20:59273 432026-08-13T15:14:26 asif mac_mismatch 00:00:00:00:00:00 aa:bb:cc:dd:ee:ff linux 203.0.113.20:55873 43
These are connections whose certificate CN + device MAC pair did not matchopenvpn_db.txt -- rejected before reaching the VPN. Repeated entries for thesame name/IP are usually a client-side device/network issue (wrong adapter,stale registration) rather than an attack, but worth a look if unexpected.Shell completion
Section titled “Shell completion”Bash tab-completion for both tools’ flags (and, for --revoke-user, live
client names when the PKI index happens to be readable by the completing
user):
source completions/openvpn-install-completion.bashsource completions/vpn-status-completion.bashAdd those two lines to ~/.bashrc to make it permanent, or copy the files
into /etc/bash_completion.d/ for a system-wide install. Only the scripts’
own names are bound – not bare sudo/bash/python3 – so tab-completion
for every other command invoked that way is unaffected.
Configuration
Section titled “Configuration”Copy vpn-tools.conf.example to /etc/openvpn/vpn-tools.conf and uncomment
only what you want to change. Both tools fall back to sensible defaults if
the file doesn’t exist at all, so a fresh clone works out of the box.
Notably, where generated .ovpn files get delivered is auto-detected,
not hardcoded to any particular distro’s default account name:
openvpn-install.sh uses whoever actually ran sudo to invoke it
($SUDO_USER), falling back to the first regular human account on the box,
then root. Set OVPN_OUTPUT_DIR/OVPN_OUTPUT_OWNER explicitly in the
config only if you want delivery to go somewhere else.