The server
The server needs four things: its configuration file, IP forwarding, a firewall, and to be started as a systemd service. On Debian, the openvpn-server@ service reads /etc/openvpn/server/<name>.conf, so server.conf becomes openvpn-server@server.
A few lines in the configuration do most of the work:
server 10.8.0.0 255.255.255.0gives the VPN its own subnet. The server takes 10.8.0.1 and hands out addresses to users from the rest.topology subnetmakestun0behave like an ordinary /24, one address per user. It's the recommended setting for new servers; older configurations usenet30, which spends four addresses on every user.push "route …"tells each user's computer which networks to send through the tunnel. This is a split tunnel: only the office and branch LANs use the VPN, and everything else goes straight to the internet.client-config-dir ccdpoints at a directory with one file per user, for settings that apply to that user only.user nobody/group nogroup: after opening the tunnel and the port as root, OpenVPN drops its privileges. This matters later.
The firewall allows only what's needed: OpenVPN's UDP port, SSH and ping to the server itself; VPN users to the office LAN; the office to the branch LAN; and alice's full-tunnel traffic out to the internet, through NAT. Everything else, including traffic from one VPN user to another, is dropped and counted.
After systemctl enable --now, the service is running and survives reboots. systemctl status and journalctl -u openvpn-server@server are where to look first, every time.
# /etc/openvpn/server/server.conf (started by: systemctl enable --now openvpn-server@server)
port 1194
proto udp
dev tun
topology subnet
# The VPN's own subnet: the server is 10.8.0.1, clients get addresses from the rest.
server 10.8.0.0 255.255.255.0
# Certificates and keys (from easy-rsa), and the shared tls-crypt key.
ca ca.crt
cert server.crt
key server.key
dh none
tls-crypt tc.key
crl-verify crl.pem
# Per-user settings (fixed addresses, extra routes) live in one file per user.
client-config-dir ccd
# Split tunnel by default: only the office LAN goes through the VPN.
push "route 10.10.0.0 255.255.255.0"
# A branch office's LAN sits behind the user "branch" (see ccd/branch).
route 10.20.0.0 255.255.255.0
push "route 10.20.0.0 255.255.255.0"
keepalive 10 60
user nobody
group nogroup
persist-key
persist-tun
status /run/openvpn-server/status-server.log 10
verb 3
explicit-exit-notify 1- Lines 2–5: UDP port 1194 and a routed TUN interface, with one ordinary /24 for the VPN.
- Line 8: The VPN subnet. The server is 10.8.0.1.
- Lines 11–16: The server's identity, the tls-crypt key, and the CRL.
dh noneuses modern elliptic-curve key exchange, so there's no Diffie-Hellman file to generate. - Line 19: One file per user in
ccd/, named after their certificate. - Line 22: Split tunnel: tell users to send only the office LAN through the VPN…
- Lines 25–26: …and the branch LAN, which lives behind one particular user.
- Line 28: A ping every 10 s; a user silent for 60 s is dropped (the server doubles it to 120 s for itself).
- Lines 29–30: Drop root privileges after start-up. This is what makes the CRL permissions matter later.
- Line 33: Who's connected, rewritten every 10 seconds.
# The branch router: a fixed address, and the branch LAN lives behind it.
ifconfig-push 10.8.0.20 255.255.255.0
iroute 10.20.0.0 255.255.255.0- Line 2: A fixed address for this user.
- Line 3: The branch LAN belongs to this user, inside OpenVPN.
# /etc/sysctl.d/99-openvpn.conf: route between the VPN and the office LAN.
net.ipv4.ip_forward = 1#!/usr/sbin/nft -f
# /etc/nftables.conf on the VPN server (enabled with: systemctl enable --now nftables)
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
ct state established,related accept
iif lo accept
icmp type echo-request accept
udp dport 1194 accept comment "OpenVPN"
tcp dport 22 accept comment "SSH"
}
chain forward {
type filter hook forward priority 0; policy drop;
ct state established,related accept
iifname "tun0" oifname "eth1" accept comment "VPN users to the office LAN"
iifname "eth1" oifname "tun0" ip daddr 10.20.0.0/24 accept comment "office to the branch LAN"
iifname "tun0" oifname "eth0" ip saddr 10.8.0.10 accept comment "alice's full tunnel to the internet"
counter comment "everything else, including user to user, is dropped"
}
}
table ip nat {
chain postrouting {
type nat hook postrouting priority srcnat;
oifname "eth0" ip saddr 10.8.0.0/24 masquerade comment "full-tunnel users appear as the server"
}
}- Lines 7–12: Into the server itself: only replies, OpenVPN, ping and SSH.
- Lines 16–17: Through the server: drop by default, allow replies.
- Lines 18–19: VPN users to the office; the office to the branch.
- Line 20: Only alice may use the VPN to reach the internet (her full tunnel).
- Line 21: Counts everything dropped, including user-to-user traffic. Watch it in the client-to-client section.
- Line 28: Full-tunnel users are NATed to the server's public address.
root@vpn:~/easy-rsa# cp pki/ca.crt pki/issued/server.crt pki/crl.pem /etc/openvpn/server/ && install -m 600 pki/private/server.key /etc/openvpn/server/
root@vpn:~# openvpn --genkey secret /etc/openvpn/server/tc.key && chmod 600 /etc/openvpn/server/tc.key
root@vpn:~# ls -l /etc/openvpn/server /etc/openvpn/server/ccd
/etc/openvpn/server:
total 32
-rw------- 1 root root 1216 Oct 21 05:17 ca.crt
drwxr-xr-x 2 root root 4096 Oct 21 05:17 ccd
-rw------- 1 root root 658 Oct 21 05:17 crl.pem
-rw-r--r-- 1 root root 905 Oct 21 05:17 server.conf
-rw------- 1 root root 4525 Oct 21 05:17 server.crt
-rw------- 1 root root 1704 Oct 21 05:17 server.key
-rw------- 1 root root 636 Oct 21 05:17 tc.key
/etc/openvpn/server/ccd:
total 12
-rw-r--r-- 1 root root 38 Oct 21 05:17 alice
-rw-r--r-- 1 root root 38 Oct 21 05:17 bob
-rw-r--r-- 1 root root 143 Oct 21 05:17 branch
root@vpn:~# sysctl --system | grep ip_forward
net.ipv4.ip_forward = 1
root@vpn:~# systemctl enable --now nftables && nft list ruleset | head -5
Created symlink '/etc/systemd/system/sysinit.target.wants/nftables.service' → '/usr/lib/systemd/system/nftables.service'.
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
ct state established,related accept
iif "lo" accept- Line 1: The CA certificate, the server's certificate and key, and the CRL into
/etc/openvpn/server/. - Line 3: The shared tls-crypt key.
- Line 10: Note the CRL is readable only by root. This comes back to bite later.
- Lines 13–14: The keys stay root-only, correctly.
- Lines 22–23: IP forwarding on, from
/etc/sysctl.d/99-openvpn.conf, and kept across reboots. - Lines 25–31: The firewall, enabled at boot.
root@vpn:~# systemctl enable --now openvpn-server@server
Created symlink '/etc/systemd/system/multi-user.target.wants/openvpn-server@server.service' → '/usr/lib/systemd/system/openvpn-server@.service'.
root@vpn:~# systemctl status openvpn-server@server --no-pager | head -12
● openvpn-server@server.service - OpenVPN service for server
Loaded: loaded (/usr/lib/systemd/system/openvpn-server@.service; enabled; preset: enabled)
Active: active (running) since Wed 2026-10-21 05:17:47 UTC; 3s ago
Invocation: 54ef1fc7da8145a7b6d15db531763de9
Docs: man:openvpn(8)
https://openvpn.net/community-resources/reference-manual-for-openvpn-2-6/
https://community.openvpn.net/openvpn/wiki/HOWTO
Main PID: 594 (openvpn)
Status: "Initialization Sequence Completed"
Tasks: 1 (limit: 9562)
Memory: 2M (peak: 2M)
CPU: 8ms
root@vpn:~# journalctl -u openvpn-server@server --no-pager -o cat | tail -15
TUN/TAP device tun0 opened
net_iface_mtu_set: mtu 1500 for tun0
net_iface_up: set tun0 up
net_addr_v4_add: 10.8.0.1/24 dev tun0
net_route_v4_add: 10.20.0.0/24 via 10.8.0.2 dev [NULL] table 0 metric -1
Could not determine IPv4/IPv6 protocol. Using AF_INET
Socket Buffers: R=[212992->212992] S=[212992->212992]
UDPv4 link local (bound): [AF_INET][undef]:1194
UDPv4 link remote: [AF_UNSPEC]
UID set to nobody
GID set to nogroup
Capabilities retained: CAP_NET_ADMIN
MULTI: multi_init called, r=256 v=256
IFCONFIG POOL IPv4: base=10.8.0.2 size=253
Initialization Sequence Completed
root@vpn:~# ip -br addr show tun0; ss -ulpn | grep 1194
tun0 UNKNOWN 10.8.0.1/24
UNCONN 0 0 0.0.0.0:1194 0.0.0.0:* users:(("openvpn",pid=594,fd=5))- Lines 1–2: Enabled at boot and started now.
- Line 7: Running.
- Line 13: OpenVPN reports to systemd when it's ready.
- Lines 19–23:
tun0created with 10.8.0.1, and the route for the branch LAN into the tunnel. - Line 26: Listening on UDP 1194.
- Lines 28–30: Root privileges dropped, keeping only the right to manage network interfaces.
- Line 33: Ready for users.