TTR Networks

Intermediate · OpenVPN on Debian: users, routing and revocation · section 4 of 11

The first user

✓ Lab-tested 21 October 2026 on Debian 13.7, OpenVPN 2.6.14, easy-rsa 3.2.2, nftables 1.1.3

A user needs one file: their configuration, with the CA certificate, their own certificate and key, and the tls-crypt key all inlined, so there's nothing else to install. make-client.sh builds it from a template.

The template is short. The important line is remote-cert-tls server: the user's OpenVPN only accepts a server whose certificate is marked for server use. Without it, any other user's certificate (signed by the same CA) could impersonate the server.

Treat each .ovpn file like a password. It contains the user's private key. Deliver it over a secure channel, and if one leaks, revoke that user.

On a Linux laptop, the file is imported into NetworkManager or run with openvpn --config alice.ovpn. The lab's users run it directly. Two things to see when alice connects:

  • Her client gets the address from her ccd file (10.8.0.10, not the next one from the pool), the two pushed routes, and reports Initialization Sequence Completed.
  • The server's log shows the same from the other side: it read ccd/alice, assigned 10.8.0.10, and sent the PUSH_REPLY with the routes.
client-template.ovpn
client
dev tun
proto udp
remote 203.0.113.10 1194
resolv-retry infinite
nobind
persist-key
persist-tun
remote-cert-tls server
verb 3
  • Line 1: This is a client configuration.
  • Line 4: The server's public address and port.
  • Line 9: Only accept a server whose certificate is marked as a server certificate.
make-client.sh
#!/bin/sh
# make-client.sh NAME  ->  NAME.ovpn: the client template with the CA, the
# user's certificate and key, and the tls-crypt key inlined. One file per user.
set -e
name=$1; pki=~/easy-rsa/pki
{
  cat /etc/openvpn/client-template.ovpn
  echo "<ca>";        cat "$pki/ca.crt";                       echo "</ca>"
  echo "<cert>";      openssl x509 -in "$pki/issued/$name.crt"; echo "</cert>"
  echo "<key>";       cat "$pki/private/$name.key";            echo "</key>"
  echo "<tls-crypt>"; cat /etc/openvpn/server/tc.key;         echo "</tls-crypt>"
} > "$name.ovpn"
chmod 600 "$name.ovpn"
echo "wrote $name.ovpn"
  • Lines 7–11: Template, then the CA certificate, the user's certificate and key, and the tls-crypt key, each in its own inline block.
  • Line 13: The file holds a private key: owner-only.
Building the user files (real output)
root@vpn:~# ./make-client.sh alice
wrote alice.ovpn

root@vpn:~# ./make-client.sh bob
wrote bob.ovpn

root@vpn:~# ./make-client.sh branch
wrote branch.ovpn

root@vpn:~# grep -nE "^(client|remote|remote-cert-tls|<[a-z-]+>|</[a-z-]+>)" alice.ovpn; wc -l alice.ovpn
1:client
4:remote 203.0.113.10 1194
9:remote-cert-tls server
11:<ca>
32:</ca>
33:<cert>
54:</cert>
55:<key>
84:</key>
85:<tls-crypt>
107:</tls-crypt>
107 alice.ovpn
  • Lines 10–22: The structure of alice's 107-line file, without printing any key material: the settings, then four inline blocks.
alice connects: her log, then the server's (real output)
root@alice:~# openvpn --config alice.ovpn --daemon --log /var/log/openvpn.log && sleep 5 && tail -8 /var/log/openvpn.log
2026-10-21 05:18:05 net_iface_up: set tun0 up
2026-10-21 05:18:05 net_addr_v4_add: 10.8.0.10/24 dev tun0
2026-10-21 05:18:05 net_route_v4_add: 10.10.0.0/24 via 10.8.0.1 dev [NULL] table 0 metric -1
2026-10-21 05:18:05 net_route_v4_add: 10.20.0.0/24 via 10.8.0.1 dev [NULL] table 0 metric -1
2026-10-21 05:18:05 Initialization Sequence Completed
2026-10-21 05:18:05 Data Channel: cipher 'AES-256-GCM', peer-id: 0
2026-10-21 05:18:05 Timers: ping 10, ping-restart 60
2026-10-21 05:18:05 Protocol options: protocol-flags cc-exit tls-ekm dyn-tls-crypt

root@vpn:~# journalctl -u openvpn-server@server --no-pager -o cat | tail -8
alice/198.51.100.21:43892 MULTI_sva: pool returned IPv4=10.8.0.2, IPv6=(Not enabled)
alice/198.51.100.21:43892 OPTIONS IMPORT: reading client specific options from: ccd/alice
alice/198.51.100.21:43892 MULTI: Learn: 10.8.0.10 -> alice/198.51.100.21:43892
alice/198.51.100.21:43892 MULTI: primary virtual IP for alice/198.51.100.21:43892: 10.8.0.10
alice/198.51.100.21:43892 SENT CONTROL [alice]: 'PUSH_REPLY,route 10.10.0.0 255.255.255.0,route 10.20.0.0 255.255.255.0,route-gateway 10.8.0.1,topology subnet,ping 10,ping-restart 60,ifconfig 10.8.0.10 255.255.255.0,peer-id 0,cipher AES-256-GCM,protocol-flags cc-exit tls-ekm dyn-tls-crypt,tun-mtu 1500' (status=1)
alice/198.51.100.21:43892 Data Channel: cipher 'AES-256-GCM', peer-id: 0
alice/198.51.100.21:43892 Timers: ping 10, ping-restart 120
alice/198.51.100.21:43892 Protocol options: explicit-exit-notify 1, protocol-flags cc-exit tls-ekm dyn-tls-crypt
  • Line 3: Her fixed address from ccd/alice.
  • Lines 4–5: The two pushed routes, into the tunnel.
  • Line 6: Connected.
  • Line 7: The data channel is encrypted with AES-256-GCM.
  • Lines 12–15: The server first picks 10.8.0.2 from the pool, then reads ccd/alice and gives her 10.8.0.10 instead.
  • Line 16: Everything the server tells alice: routes, address, timers and cipher.