Skip to content

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.

Terminal window
sudo bash openvpn-install.sh --add-user NAME MAC # e.g. --add-user alice aa:bb:cc:dd:ee:ff
sudo bash openvpn-install.sh --revoke-user NAME
sudo bash openvpn-install.sh --list-users # valid clients + db registration status
sudo bash openvpn-install.sh --list-revoked-users # revoked clients, when, stale db entries
sudo bash openvpn-install.sh --macs NAME # every MAC address registered for one client
sudo bash openvpn-install.sh --add-mac NAME MAC # register an extra device MAC for an existing client
sudo 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 stdout
sudo bash openvpn-install.sh --purge-revoked NAME # permanently delete a revoked client's leftover PKI/.ovpn files
sudo bash openvpn-install.sh --restore NAME MAC # reissue a brand-new cert under a revoked client's name
sudo bash openvpn-install.sh --check-certs # cross-check PKI certs vs openvpn_db.txt
sudo bash openvpn-install.sh --lint-mac-db # validate openvpn_db.txt formatting/health
sudo 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.

Command
sudo bash openvpn-install.sh --list-users
Output
NAME IN_DB MACS
asif yes 1
umair yes 2
umer yes 1
saad yes 1
ahsan yes 1
waqas yes 1
majid yes 1
talha yes 3
ghayoor yes 4
aliyan yes 1
headoffice yes 1
waleed yes 2
Command
sudo bash openvpn-install.sh --list-revoked-users
Output
NAME REVOKED_AT STALE_DB_ENTRY FILES_PRESENT
habib 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)
Command
sudo bash openvpn-install.sh --macs asif
Output
1 MAC address(es) registered for 'asif':
- aa:bb:cc:dd:ee:ff
Command
sudo bash openvpn-install.sh --check-certs
Output
OK -- every valid cert has a matching db entry and vice versa.
Command
sudo bash openvpn-install.sh --lint-mac-db
Output
OK -- /etc/openvpn/server/openvpn_db.txt is well-formed (19 entries).

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:

Command
sudo bash openvpn-install.sh --list-users --json
Output
[{"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}]
Command
sudo bash openvpn-install.sh --macs asif --json
Output
{"name":"asif","count":1,"macs":["aa:bb:cc:dd:ee:ff"]}
Command
sudo bash openvpn-install.sh --check-certs --json
Output
{"clean":true,"orphan_pki":[],"orphan_db":[]}
Command
sudo bash openvpn-install.sh --lint-mac-db --json
Output
{"clean":true,"entries":19,"trailing_newline_ok":true,"issues":[]}
Command
sudo bash openvpn-install.sh --add-user alice aa:bb:cc:dd:ee:ff --json
Output
--json option is not allowed with this command.

A subset of restrictions (see Device & Access Restrictions for the full picture) also has CLI equivalents:

Terminal window
sudo bash openvpn-install.sh --set-country alice PK # restrict alice to country code PK, or ANY to clear
sudo bash openvpn-install.sh --set-os alice windows,linux # restrict alice's allowed OS, or ANY to clear
sudo bash openvpn-install.sh --set-bandwidth alice 5 # 5 GB/month quota for alice, or ANY to clear
sudo bash openvpn-install.sh --get-policy alice # show alice's current policy
sudo bash openvpn-install.sh --get-policy # show every client's policy

City/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.

Command
sudo bash openvpn-install.sh --get-policy asif
Output
Policy 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:

Command
sudo bash openvpn-install.sh --get-policy
Output
All 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).

Terminal window
python3 vpn-status.py # who's connected right now
python3 vpn-status.py --all-clients # every known client: online / offline / revoked, last-seen
python3 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 JSON

Neither 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.

Command
python3 vpn-status.py
Output
NAME 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 OS
login username (OpenVPN doesn't expose that). 'n/a' means no matched connection
with peer-info has been logged yet for this client.
Command
python3 vpn-status.py --json
Output
[
{
"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"
}
]
Command
python3 vpn-status.py --all-clients
Output
NAME STATUS MAC OS LAST_SEEN
---------- ------- ----------------- ----------------------------------- -------------------
ahsan offline aa:bb:cc:dd:ee:02 Microsoft_Windows_11_Pro_10.0.22621 2026-08-14T07:21:55
aliyan offline aa:bb:cc:dd:ee:03 linux 2026-08-13T12:52:52
asif offline aa:bb:cc:dd:ee:ff linux 2026-08-13T19:21:37
ghayoor offline aa:bb:cc:dd:ee:04 macOS 26.5.2 (25F84) 2026-07-24T14:07:52
habib revoked aa:bb:cc:dd:ee:05 linux 2026-03-24T17:46:15
headoffice 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:40
saad offline aa:bb:cc:dd:ee:07 Microsoft_Windows_11_Pro_10.0.26200 2026-08-13T09:42:35
talha offline aa:bb:cc:dd:ee:08 macOS 15.6.1 (24G90) 2026-08-07T08:25:28
umair offline aa:bb:cc:dd:ee:09 linux 2026-08-13T13:10:11
umer offline aa:bb:cc:dd:ee:0a linux 2026-06-16T12:08:30
waleed offline aa:bb:cc:dd:ee:0b n/a never
waqas 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 OS
login username (OpenVPN doesn't expose that). 'n/a' means no matched connection
with peer-info has been logged yet for this client.
Command
python3 vpn-status.py --rejected-connections 5
Output
TIMESTAMP 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 43
2026-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 43
2026-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 43
2026-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 43
2026-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 match
openvpn_db.txt -- rejected before reaching the VPN. Repeated entries for the
same name/IP are usually a client-side device/network issue (wrong adapter,
stale registration) rather than an attack, but worth a look if unexpected.

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):

Terminal window
source completions/openvpn-install-completion.bash
source completions/vpn-status-completion.bash

Add 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.

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.