This episode brings OpenVPN configuration into the reproducible realm: template-based config, hardening systemd units, the env-file pattern for secrets, and automatic provisioning with Ansible and community scripts like openvpn-install.sh.

Configurations written by hand and edited directly on the server are a time bomb. When a second server is needed, you can't remember every small change ever made. When something changes, there's no trace of who changed what.
Episode 12 covers configuration management and automation. You will learn to write template-based configurations that can be rendered from data, secure OpenVPN's systemd units, separate secrets into env-files, and provision servers and clients with Ansible. Plus openvpn-install.sh — a popular community script for quick setup.
Why does this matter? Consistency is the foundation of operations. If your configuration is generated from a single source of truth, drift between servers disappears, rollback becomes easy, and audits become fast.
Templates separate the parts of a configuration that vary (IPs, ports, names) from the parts that stay fixed. With a template engine like Jinja2, one template generates many configurations:
server:
name: vpn-primary
port: 1194
network: 10.8.0.0
netmask: 255.255.255.0port {{ server.port }}
proto udp
dev tun
server {{ server.network }} {{ server.netmask }}
topology subnetWith this pattern, adding a new server is just adding a data entry and re-rendering the template. Address typos can also be avoided because addresses are written only once, in the data.
Many OpenVPN implementations use a server.conf plus env-file scheme for values that vary between environments:
# /etc/openvpn/server.env
OVPN_NETWORK=10.8.0.0
OVPN_NETMASK=255.255.255.0OVPN_NETWORK=10.8.0.0 is stored as an env variable, then the configuration uses it through a wrapper script that renders the file. This pattern lets the same configuration be used in development and production with only a different env-file.
The default systemd unit gives the service full access to the system. OpenVPN doesn't need all of that — restrict it with sandbox options:
[Service]
LimitNOFILE=1048576
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/etc/openvpn /var/log
NoNewPrivileges=trueProtectSystem=strict makes the entire filesystem read-only except allowed paths. PrivateTmp gives the service its own /tmp namespace, and NoNewPrivileges prevents privilege escalation. This is a real example of the least privilege principle for a VPN daemon.
The OpenVPN placeholder unit uses the instance pattern: /etc/systemd/system/openvpn-server@.service. Each instance is invoked with the configuration name without its extension:
systemctl daemon-reload
systemctl enable openvpn-server@server
systemctl start openvpn-server@serversystemctl start openvpn-server@server runs the service with /etc/openvpn/server/server.conf. This pattern makes it easy to manage the multiple instances discussed in episode 11.
Ansible is the most popular tool for provisioning OpenVPN because it needs no agent on the target. A single playbook can prepare the server and generate client files:
- name: Setup OpenVPN server
hosts: vpn_servers
become: true
tasks:
- name: Install OpenVPN and Easy-RSA
apt:
name:
- openvpn
- easy-rsa
state: presentThis playbook is idempotent: running it repeatedly produces the same state. You can add tasks to upload server.conf, start the service, and ensure the firewall is open.
A playbook can also include tasks to create client certificates and render .ovpn files:
- name: Generate client config
template:
src: client.ovpn.j2
dest: /opt/clients/{{ item }}.ovpn
loop: "{{ clients }}"With a loop, one task generates client files for all users in the clients list. This solves client configuration distribution centrally — a theme started in episode 9.
For labs and small deployments, the community script openvpn-install.sh automates the entire installation — from packages, PKI, server configuration, to client creation:
curl -O https://raw.githubusercontent.com/angristan/openvpn-install/master/openvpn-install.sh
chmod +x openvpn-install.sh
./openvpn-install.sh./openvpn-install.sh guides you with interactive questions: port, protocol, DNS, and client name. The script produces a server.conf that works out of the box and .ovpn files to distribute to users.
Community scripts are great for prototypes and small environments, but less suitable for large production. Their limitations: the generated configuration isn't easily reproducible on a second server, and it isn't integrated with a configuration management pipeline. For production, make templates and playbooks the source of truth.
Key takeaways:
openvpn-install.sh suits prototypes and small environments.In the next episode, episode 13, we will discuss security hardening and best practice — securing the control channel with tls-crypt-v2, enforcing tls-version-min 1.2, choosing modern ciphers with forward secrecy, and firewall and ACL policy for defense-in-depth. After this episode, your server is ready to face security testing.