This episode covers how to give each client different configuration: the client-config-dir directive, the client-connect and client-disconnect scripts as lifecycle hooks, and static IP allocation with ifconfig-push and iroute.

So far all clients have been treated the same: an address from the same pool, the same routes, the same policy. In the real world that's rarely the case. Managers need access to the entire network, finance admins only to one server, and engineering teams need fixed IP addresses for inbound firewall rules.
Episode 9 covers client config distribution (CCD) — OpenVPN's mechanism for giving each client different configuration. You will learn the client-config-dir directive, the client-connect and client-disconnect scripts as hooks at the start and end of a session, and static IP allocation with ifconfig-push and iroute.
This is the episode that turns OpenVPN from a mere tunnel into an infrastructure with real policy. After this episode, you can grant different levels of access to each user or site.
The client-config-dir directive points to a directory containing per-client configuration files. Each file is named after the client certificate's common name:
client-config-dir /etc/openvpn/server/ccdWhen a client connects, OpenVPN looks in that directory for a file with the same name as the client certificate's CN. Example: a client with CN arman will load the file /etc/openvpn/server/ccd/arman. If the file doesn't exist, the client gets the default configuration from server.conf.
The CCD directory handles network access control, so make sure only root can write to it:
mkdir -p /etc/openvpn/server/ccd
chown root:root /etc/openvpn/server/ccd
chmod 700 /etc/openvpn/server/ccdchmod 700 ensures only the directory owner can access it. A CCD file writable by another user is a security hole: an attacker could inject push directives to hijack other clients' routing.
A CCD file is a snippet of server configuration loaded specifically for that client. Common directives written here are iroute, ifconfig-push, and push:
iroute 192.168.10.0 255.255.255.0
ifconfig-push 10.8.0.10 255.255.255.0
push "route 10.20.0.0 255.255.255.0"The push "route ..." line inside a CCD file is added to the client's configuration, just like a push in server.conf — but now only for that particular client.
When a client needs a fixed IP address inside the VPN subnet, use ifconfig-push in its CCD file. Combined with topology subnet on the server, this gives the client the same static address on every connect:
ifconfig-push 10.8.0.50 255.255.255.0Addresses allocated with ifconfig-push must lie outside the automatic server pool to avoid conflicts. A good habit: allocate static IPs starting from the lower end of the subnet range and let the dynamic pool fill the upper end.
If you use topology net30 or p2p, the ifconfig-push format is different: it needs two addresses, one for the client and one for the server side. Don't mix formats without understanding the active topology, because a format error produces an option ifconfig-push on non-subnet topology error in the log.
After a client connects, verify in openvpn-status.log that the client received the expected IP. If two clients receive the same IP, hunt down the conflict source in the CCD files — OpenVPN does not automatically reject duplicate addresses.
iroute tells the server that the subnet behind a client can be reached through that client's tunnel. This is essential for site-to-site and for clients sitting behind NAT:
iroute 192.168.10.0 255.255.255.0A common pair of directives for a client representing a site: ifconfig-push gives the client a fixed IP, while iroute gives the server a way to reach the subnet behind the client. Both are usually written side by side in the same CCD file.
For traffic to flow both ways, the server also needs to tell the client the routes to other networks. In the CCD file, add push "route ..." for the networks the client should reach. Without push, the client knows how to send to the server's subnet, but the server must also know how to reach the client's subnet via iroute.
OpenVPN calls scripts at certain moments in a client session's lifecycle. The two most useful are client-connect, called after successful authentication before the tunnel is activated, and client-disconnect, called after the session ends:
client-connect /etc/openvpn/server/scripts/on-connect.sh
client-disconnect /etc/openvpn/server/scripts/on-disconnect.sh
script-security 2script-security 2 must be enabled so OpenVPN will run external scripts. Scripts receive client information through environment variables such as common_name, untrusted_ip, and ifconfig_pool_remote_ip.
Script hooks are often used to apply dynamic firewall rules, log access to a database, or send notifications. A simple example that records a new session:
#!/bin/bash
echo "$(date) client $common_name connected from $untrusted_ip" >> /var/log/openvpn-ccd.logAfter creating the script, don't forget chmod +x so it can be executed. Small mistakes like wrong permissions often make scripts silently fail to run.
Because client-connect is called before the tunnel is active, this script can reject connections with a non-zero exit code. This pattern is used for additional access control beyond certificates — for example rejecting clients not on an allowlist, or enforcing time-based policy.
Key takeaways:
client-config-dir maps per-client config files based on the certificate CN.ifconfig-push gives a client a fixed IP address in the VPN subnet.iroute tells the server how to reach a subnet behind a client.push "route ..." inside a CCD file is specific to that client.client-connect and client-disconnect are session lifecycle hooks.In the next episode, episode 10, we will discuss advanced authentication — LDAP and RADIUS integration to replace static user files, the auth-pam and auth-radius plugins, and adding MFA with OTP and TOTP as a second security layer. After this episode, your OpenVPN authentication is ready for enterprise scale.