Deploy an OpenVPN server on Ubuntu 24.04 LTS
Build a routed full-tunnel OpenVPN service with Easy-RSA, an owned nftables policy, a tested encrypted client profile, and rehearsed certificate revocation.
Provide certificate-authenticated remote access and IPv4 internet egress through a dedicated gateway without weakening server identity checks or leaving revocation untested.
- Ubuntu Server 24.04 LTS
- OpenVPN 2.6.x
- Easy-RSA 3.x
- Recovery access Keep console access while changing forwarding and firewall rules.
- Network plan Reserve a non-overlapping VPN subnet, public UDP port, and allowed private routes.
- Stable endpoint Point a DNS name at the server public address.
- Current-state backup Export firewall and sysctl configuration before the change.
sudo nft list ruleset
OneLiners never runs these steps or stores secrets. Review placeholders, versions, current state, and change-control requirements before using a command.
Full guide
What you will build
- A certificate-authenticated OpenVPN 2.6 server on a dedicated Ubuntu 24.04 LTS host. The server listens on one public UDP port, assigns addresses from a private tunnel subnet, and rejects peers that do not present a client certificate signed by your certificate authority.
- A deliberately owned nftables policy that permits management access from a named source network, accepts the OpenVPN listener, forwards authenticated tunnel traffic, and performs IPv4 masquerading for full-tunnel internet access.
- A repeatable client lifecycle: issue one encrypted private key per person or device, assemble a restricted inline profile, test it from an external network, and revoke it by publishing a new certificate revocation list.
- The OpenVPN systemd instance is active, UDP port 1194 or your selected replacement is listening, and the journal contains an Initialization Sequence Completed message without certificate, cipher, route, or permission errors.
- A test client receives an address from the planned VPN subnet, installs two def1 routes through the tunnel, resolves DNS through the selected resolver, and reaches the server-side tunnel address.
- After a disposable certificate is revoked and the new CRL is installed, that profile cannot establish a new session while certificates that remain valid continue to connect normally.
Architecture
How the parts fit together
This guide uses routed TUN mode rather than Ethernet bridging. A client first reaches the server's public UDP socket and authenticates the server certificate. The server then validates the client's certificate against the same CA and its current CRL. Only after the TLS control channel succeeds does OpenVPN create encrypted data-channel keys and assign a tunnel address. Linux forwards that authenticated traffic from tun0 to the public interface, while nftables limits the allowed path and rewrites the private source address for internet egress.
- The client resolves the stable VPN hostname and sends a tls-crypt-protected OpenVPN control packet to the selected UDP port.
- The client verifies that the server certificate has TLS Web Server Authentication usage; the server requires a certificate with TLS Web Client Authentication usage.
- OpenVPN negotiates a mutually supported AEAD data cipher and assigns the client an address from the 10.8.0.0/24 example pool.
- The server pushes full-tunnel routes and a DNS resolver. Linux forwards accepted packets from tun0 to the public interface.
- nftables allows established replies and masquerades the private tunnel source address before the packet leaves the gateway.
Assumptions
- The target is a dedicated or carefully isolated VPN gateway. Do not apply the example ruleset unchanged to a Docker host, Kubernetes node, hypervisor, router, or machine whose existing firewall is managed by UFW, firewalld, configuration management, or another nftables owner.
- You have provider console, hypervisor console, or physical access throughout the firewall change. A second SSH session is open from the exact management network that will be placed in {{managementCidr}}.
- The public interface has stable connectivity and the provider or upstream firewall forwards {{vpnPort}}/UDP to this host. The hostname in {{vpnHost}} already resolves to the public address clients will use.
- The VPN address pool does not overlap the client's home network, corporate networks, container networks, cloud VPCs, or another VPN. The examples use 10.8.0.0/24 only after that collision check.
- This procedure provides an IPv4 full tunnel. IPv6 requires an explicit address plan, OpenVPN IPv6 directives, forwarding policy, leak testing, and either routed IPv6 or a deliberate decision to disable it on the client.
Key concepts
- Certificate authority (CA)
- The trust root that signs every server and client certificate. Possession of the CA private key allows an operator or attacker to mint identities trusted by the VPN, so the key should be passphrase-protected and preferably kept away from the internet-facing gateway.
- Control channel
- The TLS-protected conversation used for peer authentication, capability negotiation, and creation of short-lived data-channel keys. tls-crypt additionally encrypts and authenticates the outer control packets before the TLS stack processes them.
- Data channel
- The encrypted path carrying user packets after authentication. OpenVPN 2.6 negotiates a cipher from data-ciphers; the server's first cipher also supported by the client wins.
- TUN routed mode
- A layer-3 virtual interface that transports IP packets. It avoids stretching Ethernet broadcasts across the internet and makes access policy, routes, and address ownership explicit.
- Certificate revocation list (CRL)
- A signed list of certificates that the CA no longer trusts. Revocation has no effect until a newly generated CRL is copied to the server path referenced by crl-verify and the running service reads it.
- Masquerading
- A form of source NAT used when the egress address may change. nftables replaces a packet's 10.8.0.0/24 source with the gateway's public-interface address so internet replies return through the VPN server.
Before you copy
Values used in this guide
{{vpnHost}}Public DNS name clients use. It must resolve externally and should remain stable across server maintenance.
Example: vpn.example.net{{vpnPort}}Public UDP listener. The guide uses OpenVPN's conventional port unless your network policy assigns another.
Example: 1194{{publicInterface}}Linux interface carrying the default route and public traffic. Confirm it rather than assuming a legacy eth0 name.
Example: ens3{{managementCidr}}Trusted IPv4 source network allowed to reach SSH after the nftables policy becomes active.
Example: 203.0.113.32/28{{sshPort}}Actual SSH listening port that must remain reachable from the management network.
Example: 22{{vpnNetwork}}Network address passed to OpenVPN's server helper directive.
Example: 10.8.0.0{{vpnNetmask}}IPv4 netmask paired with the VPN network address.
Example: 255.255.255.0{{vpnNetworkCidr}}The same tunnel pool in CIDR notation for nftables source matching.
Example: 10.8.0.0/24{{dnsServer}}Resolver reachable through the completed tunnel. Confirm its privacy policy and availability before pushing it to clients.
Example: 1.1.1.1{{clientName}}Unique certificate identity for one device or operator. Use only letters, digits, underscore, and hyphen so it is safe in file names.
Example: laptop-aliceSecurity and production boundaries
- The CA private key is more powerful than the server key. Keep it encrypted and back it up independently; for a production deployment, perform signing on an administrative workstation or offline CA rather than leaving the CA on the public gateway.
- The example client profile contains its private key. File mode 0600 reduces accidental disclosure but does not protect a copied file, so the Easy-RSA client key should remain passphrase-protected and the profile must never be sent through public chat, tickets, or source control.
- Replacing the complete nftables ruleset can disconnect administrators or break unrelated forwarding. This guide deliberately limits that action to a dedicated gateway and requires console access, a saved ruleset, syntax validation, and a second SSH session.
- A full tunnel changes the client's effective network path and DNS. Test for DNS, IPv6, and application leaks from a disposable client before claiming that the VPN supplies a privacy or compliance boundary.
- Revocation prevents future authentication but does not remotely erase a profile. Treat suspected theft as an incident: revoke, publish the CRL, inspect active sessions and logs, and rotate broader secrets if the CA or tls-crypt key may also be exposed.
Stop before continuing if
- Stop before changing nftables if you cannot identify the current firewall owner, cannot obtain console access, or cannot explain every existing input and forwarding exception that would be replaced.
- Stop if {{vpnNetworkCidr}} overlaps any route shown on the server or the external test client. Renumbering after issuing profiles is disruptive, while an overlap can silently route production traffic to the wrong network.
- Stop if the server certificate and key do not match, if certificate purposes are wrong, or if the server key is readable by untrusted users. Do not weaken verification to make a broken PKI start.
- Stop after any failed checkpoint. Preserve the journal and current ruleset, restore the previous known-good state, and diagnose the failed layer before proceeding to client distribution.
command
Install the supported packages
Install OpenVPN, Easy-RSA, and nftables from Ubuntu repositories.
Why this step matters
Using Ubuntu's signed packages keeps the OpenVPN binary, systemd templates, Easy-RSA tooling, and nftables integration within the distribution's security-update path. Confirming the actual version immediately prevents the rest of the tutorial from silently relying on OpenVPN 2.6 behavior when an older repository, container image, or manually installed binary is taking precedence.
What to understand
apt update refreshes repository metadata but does not upgrade unrelated packages. apt install resolves the supported OpenVPN, Easy-RSA, and nftables packages for Ubuntu 24.04. The final version commands are evidence: save them with the change record because service names, available directives, and cipher behavior are version-sensitive.
Inspect existing packages and listeners before installation on a reused host. A previous OpenVPN package, snap, container, or hand-built /usr/local/sbin/openvpn can make the shell and systemd run different binaries. Resolve that ambiguity now instead of debugging it after certificates and firewall policy have changed.
System changes
- Installs the OpenVPN daemon, Easy-RSA PKI scripts, nftables userspace tools, systemd service templates, package documentation, and their dependencies.
- Does not start a configured VPN instance because /etc/openvpn/server/server.conf does not exist yet.
Syntax explained
apt update- Downloads current package indexes from configured repositories without installing an upgrade.
apt install --yes- Installs the named packages and accepts the package transaction after apt has displayed it.
openvpn --version- Prints the binary version and linked crypto capabilities used to validate the OpenVPN 2.6 baseline.
sudo apt update && sudo apt install --yes openvpn easy-rsa nftables && openvpn --version | head -n 1 && nft --versionOpenVPN 2.6.15 x86_64-pc-linux-gnu nftables v1.0.9 (Old Doc Yak #3)
Checkpoint: Supported tools are present
openvpn --version | head -n 1 && test -x /usr/share/easy-rsa/easyrsa && nft --versionContinue whenOpenVPN reports 2.6.x, the Easy-RSA executable exists, and nftables prints a version without an error.
Stop whenOpenVPN is older than 2.6, comes from an unexpected path, or apt reports a broken or partially configured package transaction.
If this step fails
apt cannot locate openvpn or easy-rsa.
Likely causeRepository metadata is stale, the standard Ubuntu repositories are disabled, or the machine is not actually running the declared Ubuntu release.
cat /etc/os-releaseapt-cache policy openvpn easy-rsa nftablesgrep -R --line-number '^Types\|^URIs\|^Suites' /etc/apt/sources.list.d
ResolutionRestore the appropriate official Ubuntu repository configuration and rerun apt update. Do not add an unreviewed third-party repository merely to continue.
Security notes
- Review the apt transaction before accepting it on a production host and record whether a reboot is already pending from earlier maintenance.
Alternatives
- If organizational policy requires an internal mirror, verify that it mirrors Ubuntu Noble security updates and does not pin OpenVPN below the documented baseline.
Stop conditions
- Do not continue when the installed binary or package origin does not match the reviewed Ubuntu 24.04 environment.
command
Create the CA and server identity
Build an isolated PKI. Protect the CA private key and never embed it in a client profile.
Why this step matters
Mutual certificate authentication gives every server and client a distinct identity and makes individual revocation possible. The CA is created before the server identity because the server certificate must be signed by a trust root that clients can verify. Easy-RSA assigns the server certificate the extended key usage expected by remote-cert-tls verification.
What to understand
make-cadir creates a writable Easy-RSA workspace owned by the administrator rather than modifying package files under /usr/share. init-pki starts a new certificate database. build-ca should prompt for a strong passphrase and a recognizable CA name; that passphrase protects the trust root at rest.
build-server-full server nopass creates a server certificate and an unencrypted server private key. An unattended systemd service cannot type a passphrase during boot, so the server key relies on strict file permissions and host security. This convenience must not be copied to the CA key.
System changes
- Creates ~/openvpn-ca with the CA database, CA certificate, encrypted CA private key, server request, signed server certificate, and unencrypted server private key.
- Does not yet expose PKI material to /etc/openvpn/server or start a network listener.
Syntax explained
make-cadir ~/openvpn-ca- Copies the distribution's Easy-RSA scripts and variables into an administrator-owned working directory.
./easyrsa init-pki- Creates a fresh PKI database. Running it against an existing PKI can be destructive and requires a separate migration plan.
./easyrsa build-ca- Creates the self-signed CA certificate and prompts for protection of the CA private key.
build-server-full server nopass- Generates and signs a certificate with server purpose; nopass leaves only the server key unencrypted for unattended startup.
make-cadir ~/openvpn-ca && cd ~/openvpn-ca && ./easyrsa init-pki && ./easyrsa build-ca && ./easyrsa build-server-full server nopassCertificate created at: pki/issued/server.crt
Checkpoint: CA and server certificate form one valid chain
cd ~/openvpn-ca && openssl verify -CAfile pki/ca.crt pki/issued/server.crt && openssl x509 -in pki/issued/server.crt -noout -subject -issuer -datesContinue whenopenssl verify prints pki/issued/server.crt: OK, and the issuer matches the new CA with a validity period acceptable to your policy.
Stop whenVerification fails, an unexpected CA signed the certificate, the certificate is already near expiry, or the CA private key was created without the intended protection.
If this step fails
Easy-RSA reports that a PKI already exists.
Likely causeThe selected directory contains an earlier CA or an interrupted attempt. Reinitializing it could destroy issuance and revocation history.
cd ~/openvpn-ca && ./easyrsa show-cafind ~/openvpn-ca/pki -maxdepth 2 -type f -printf '%M %p\n'
ResolutionIdentify and back up the existing PKI. Reuse it only if it is the intended authority; otherwise create a new, clearly named working directory rather than running init-pki over unknown state.
Security notes
- Back up pki/ca.crt, pki/private/ca.key, index.txt, serial, and the revocation database together. A CA key without its database makes lifecycle management unreliable.
Alternatives
- For production, generate the server request on the gateway, import and sign it on an offline CA, then return only the signed certificate and CA certificate.
Stop conditions
- Stop if the directory contains an unidentified existing CA or if the CA key is accidentally disclosed in terminal output, shell history, backup logs, or a shared location.
command
Generate and install server cryptographic material
Create DH parameters, the initial revocation list, and a tls-crypt key, then install only the gateway runtime material under restrictive permissions.
Why this step matters
OpenVPN needs the server identity, a trusted CA, revocation evidence, and control-channel protection in the runtime directory. Generating and installing them as one reviewed stage avoids a service that starts with only part of the intended policy. The CRL exists before the first client is issued, so revocation is not an afterthought.
What to understand
Easy-RSA gen-dh creates Diffie-Hellman parameters used by this TLS server configuration. gen-crl signs the initial empty revocation list. openvpn --genkey secret creates the shared tls-crypt key that clients will also embed in their profiles.
Public certificates, DH parameters, and the CRL are installed read-only at mode 0644. The server private key and tls-crypt key are installed at mode 0600. Absolute paths in server.conf make the runtime location explicit and independent of the service's working directory.
System changes
- Creates pki/dh.pem, pki/crl.pem, and pki/tls-crypt.key in the CA workspace.
- Creates /etc/openvpn/server and installs only the files required by the public gateway; the CA private key remains outside that directory.
Syntax explained
./easyrsa gen-dh- Generates DH parameters for the server configuration and may take noticeable time on a small VM.
./easyrsa gen-crl- Publishes the CA's current revocation state as a signed PEM CRL.
openvpn --genkey secret- Creates a static key in OpenVPN format for tls-crypt control-channel protection.
install -m 0600- Copies secret material with owner-only permissions in one operation instead of relying on a later chmod.
cd ~/openvpn-ca && ./easyrsa gen-dh && ./easyrsa gen-crl && openvpn --genkey secret pki/tls-crypt.key && sudo install -d -m 0750 /etc/openvpn/server && sudo install -m 0644 pki/ca.crt pki/issued/server.crt pki/dh.pem pki/crl.pem /etc/openvpn/server/ && sudo install -m 0600 pki/private/server.key pki/tls-crypt.key /etc/openvpn/server/DH parameters of size 2048 created at pki/dh.pem CRL file: pki/crl.pem 600 server.key 600 tls-crypt.key
Checkpoint: Runtime material is complete and internally consistent
sudo find /etc/openvpn/server -maxdepth 1 -type f -printf '%m %f\n' | sort && sudo openssl verify -CAfile /etc/openvpn/server/ca.crt /etc/openvpn/server/server.crt && sudo openssl crl -in /etc/openvpn/server/crl.pem -noout -lastupdate -nextupdateContinue whenAll six required files are present, secret keys show mode 600, certificate verification is OK, and the CRL has a current update window.
Stop whenA file is missing, a secret is group/world-readable, certificate verification fails, or the CRL cannot be parsed.
If this step fails
gen-dh appears to hang or is terminated.
Likely causeThe operation is CPU-intensive on a small VM, or the process was interrupted before pki/dh.pem was completed.
pgrep -af 'easyrsa|openssl.*dh'test -s ~/openvpn-ca/pki/dh.pem && openssl dhparam -in ~/openvpn-ca/pki/dh.pem -check -noout
ResolutionAllow the active process to finish. If it was interrupted, remove only the incomplete dh.pem after confirming no process uses it, then rerun gen-dh.
Security notes
- The tls-crypt key is shared by every profile in this design. Exposure does not reveal the CA key, but it removes the outer control-channel protection and requires profile-wide rotation.
Alternatives
- OpenVPN 2.6 supports tls-crypt-v2 for client-specific wrapped keys; adopt it when per-client control-key lifecycle is worth the additional provisioning complexity.
Stop conditions
- Do not copy pki/private/ca.key into /etc/openvpn/server or any client profile.
config
Write the routed server configuration
Use modern subnet topology, mutual certificate-purpose verification, explicit revocation, AEAD negotiation, and full-tunnel IPv4 routes.
Why this step matters
The server configuration turns the PKI and address plan into enforceable runtime behavior. It explicitly selects modern routed topology, requires client-purpose certificates, checks the CRL, limits TLS versions, negotiates modern AEAD ciphers, and drops privileges after initialization rather than relying on historical defaults.
What to understand
topology subnet makes the 10.8.0.0/24 example behave like an ordinary routed subnet for modern clients. The server helper assigns 10.8.0.1 to the server and allocates the remaining pool. redirect-gateway def1 installs two more-specific half-default routes, which preserve the client's direct route to the VPN endpoint while sending ordinary IPv4 traffic through the tunnel.
data-ciphers is an ordered negotiation list, not a command to use every cipher. remote-cert-tls client checks client certificate purpose. crl-verify rejects newly connecting certificates listed in the installed CRL. user and group reduce runtime privilege after OpenVPN has opened protected files and configured the tunnel.
System changes
- Creates /etc/openvpn/server/server.conf, defining the public listener, tunnel pool, pushed routes and DNS, certificate paths, cipher policy, keepalive behavior, and runtime privilege.
Syntax explained
proto udp4- Listens with IPv4 UDP transport, matching this guide's explicit IPv4 firewall and forwarding design.
topology subnet- Uses modern subnet addressing instead of the legacy net30 allocation model.
server {{vpnNetwork}} {{vpnNetmask}}- Enables multi-client server mode and allocates tunnel addresses from the reviewed pool.
push "redirect-gateway def1 bypass-dhcp"- Sends full-tunnel IPv4 routing to clients while preserving their local DHCP path.
remote-cert-tls client- Requires a connecting certificate with TLS Web Client Authentication purpose.
data-ciphers- Orders acceptable AEAD data ciphers; the first mutually supported value is selected.
tls-crypt- Encrypts and authenticates control-channel packets before the TLS exchange is exposed.
user nobody / group nogroup- Drops the daemon's user and group privilege after privileged initialization.
/etc/openvpn/server/server.confValues stay on this page and are never sent or saved.
port {{vpnPort}}
proto udp4
dev tun
topology subnet
server {{vpnNetwork}} {{vpnNetmask}}
push "redirect-gateway def1 bypass-dhcp"
push "dhcp-option DNS {{dnsServer}}"
ca /etc/openvpn/server/ca.crt
cert /etc/openvpn/server/server.crt
key /etc/openvpn/server/server.key
dh /etc/openvpn/server/dh.pem
crl-verify /etc/openvpn/server/crl.pem
verify-client-cert require
remote-cert-tls client
tls-version-min 1.2
tls-crypt /etc/openvpn/server/tls-crypt.key
data-ciphers AES-256-GCM:AES-128-GCM:?CHACHA20-POLY1305
auth SHA256
user nobody
group nogroup
persist-key
persist-tun
keepalive 10 120
explicit-exit-notify 1
verb 3Configuration saved at /etc/openvpn/server/server.conf with unresolved placeholders replaced.
Checkpoint: Configuration references the intended material and network
sudo grep -Ev '^(#|$)' /etc/openvpn/server/server.conf && sudo test -r /etc/openvpn/server/server.key && sudo test -r /etc/openvpn/server/crl.pemContinue whenThe rendered configuration contains the planned values and every referenced credential file is readable by the initializing service.
Stop whenAny placeholder remains unresolved, the VPN pool overlaps a real route, or the configuration points at credentials from another PKI.
If this step fails
OpenVPN later reports an unrecognized option.
Likely causeThe running binary is older than the documented baseline, a directive was misspelled, or a configuration line was copied with typographic quotes.
openvpn --version | head -n 1sudo journalctl -u openvpn-server@server -b --no-pager -n 100sudo sed -n '=' /etc/openvpn/server/server.conf
ResolutionCorrect the exact directive against the OpenVPN 2.6 manual or restore the supported package. Do not use ignore-unknown-option to hide a security-relevant mistake.
Security notes
- Do not add compression for ordinary VPN traffic. Compression can expose secrets when attacker-controlled and sensitive data share a compressed encrypted stream.
Alternatives
- For split tunneling, remove redirect-gateway and push only the specific network prefixes clients are authorized to reach.
Stop conditions
- Do not proceed while unresolved {{placeholders}} or an unreviewed redirect, script, plugin, management socket, or compatibility cipher remains in the file.
config
Enable forwarding deliberately
Persist forwarding only when clients must reach networks beyond the VPN server.
Why this step matters
A working tunnel interface does not automatically make the host a router. Linux must be allowed to forward IPv4 packets between tun0 and the public interface. Keeping this setting in a dedicated sysctl file makes the change visible, reviewable, and removable during rollback.
What to understand
net.ipv4.ip_forward controls forwarding globally for the network namespace. The value 1 permits the kernel to consider routing a packet; it does not bypass nftables. The forward chain still decides which traffic is allowed.
sysctl --system reloads all system configuration files, so review its complete output for an unrelated invalid setting. Query the final key explicitly because the last loaded assignment wins.
System changes
- Creates /etc/sysctl.d/99-openvpn-forward.conf and changes the live net.ipv4.ip_forward value to 1.
Syntax explained
net.ipv4.ip_forward = 1- Enables IPv4 routing in the current network namespace after sysctl applies the file.
sysctl --system- Loads sysctl configuration from the standard system directories in precedence order.
/etc/sysctl.d/99-openvpn-forward.confnet.ipv4.ip_forward = 1sudo sysctl --system && sysctl net.ipv4.ip_forward* Applying /etc/sysctl.d/99-openvpn-forward.conf ... net.ipv4.ip_forward = 1
Checkpoint: IPv4 forwarding is active and persistent
sysctl net.ipv4.ip_forward && cat /etc/sysctl.d/99-openvpn-forward.confContinue whenBoth the live value and the dedicated file show net.ipv4.ip_forward = 1.
Stop whensysctl reports an error, a later file forces the value back to 0, or forwarding is prohibited by the host's security design.
If this step fails
The setting returns to 0 after sysctl --system or reboot.
Likely causeAnother later-loading sysctl file overrides it, or configuration management restores a conflicting policy.
grep -R --line-number 'net.ipv4.ip_forward' /etc/sysctl.conf /etc/sysctl.d /usr/lib/sysctl.djournalctl -b -u systemd-sysctl --no-pager
ResolutionRemove the conflict from the authoritative configuration source or choose a filename and policy ordering approved for the environment. Do not repeatedly overwrite a managed file by hand.
Security notes
- Forwarding expands the host's role from endpoint to router. The following firewall policy must be applied before untrusted clients are allowed to connect.
Alternatives
- Use a dedicated network namespace or separate VPN appliance when the base host must not become a general-purpose router.
Stop conditions
- Stop if another platform owns sysctl networking policy or if enabling forwarding would violate the host's isolation assumptions.
warning
Validate and activate the dedicated gateway firewall
Stage a complete default-drop policy, preserve the previous file, validate before installation, and keep console plus a second SSH session open.
Why this step matters
The firewall is the enforcement boundary between an authenticated VPN identity and the rest of the host or internet. This example deliberately owns the complete nftables ruleset on a dedicated gateway: it preserves loopback and established traffic, limits SSH to one management CIDR, opens only the VPN UDP port, allows forwarding only from the tunnel pool to the public interface, and performs IPv4 masquerading.
What to understand
Write the proposed policy to /etc/nftables.conf.oneliners first. The apply command saves the existing persistent file, validates the complete candidate without changing live rules, installs it only after that check succeeds, and then enables nftables. Keep the console and a second SSH session open throughout.
flush ruleset makes this file authoritative. That is appropriate only under the dedicated-host assumptions stated above. The input and forward chains default to drop, while output defaults to accept. Established replies are accepted before narrow new-connection rules. NAT occurs only for packets whose source belongs to {{vpnNetworkCidr}} and whose egress interface is {{publicInterface}}.
System changes
- Replaces the persistent /etc/nftables.conf after creating /etc/nftables.conf.before-openvpn.
- Flushes the live nftables ruleset when the service loads the new file, establishes default-drop input and forward chains, and creates an IPv4 postrouting masquerade rule.
Syntax explained
nft --check --file- Parses and validates the complete candidate ruleset without applying it.
policy drop- Drops packets that reach the end of the input or forward chain without an explicit accept rule.
ct state established,related accept- Allows reply traffic for connections that the policy already permitted and related protocol flows tracked by conntrack.
ip saddr $management_net- Restricts new SSH connections to the reviewed IPv4 management source range.
masquerade- Rewrites VPN client source addresses to the current address of the public egress interface.
/etc/nftables.conf.onelinersValues stay on this page and are never sent or saved.
#!/usr/sbin/nft -f
flush ruleset
define public_if = "{{publicInterface}}"
define management_net = {{managementCidr}}
define vpn_net = {{vpnNetworkCidr}}
define ssh_port = {{sshPort}}
define vpn_port = {{vpnPort}}
table inet oneliners_openvpn_filter {
chain input {
type filter hook input priority filter; policy drop;
iifname "lo" accept
ct state invalid drop
ct state established,related accept
ip protocol icmp accept
ip6 nexthdr icmpv6 accept
iifname $public_if ip saddr $management_net tcp dport $ssh_port accept
iifname $public_if udp dport $vpn_port accept
}
chain forward {
type filter hook forward priority filter; policy drop;
ct state invalid drop
ct state established,related accept
iifname "tun0" oifname $public_if ip saddr $vpn_net accept
}
chain output {
type filter hook output priority filter; policy accept;
}
}
table ip oneliners_openvpn_nat {
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
oifname $public_if ip saddr $vpn_net masquerade
}
}sudo cp --archive /etc/nftables.conf /etc/nftables.conf.before-openvpn && sudo nft --check --file /etc/nftables.conf.oneliners && echo 'nftables syntax: valid' && sudo install -o root -g root -m 0644 /etc/nftables.conf.oneliners /etc/nftables.conf && sudo systemctl enable --now nftables && systemctl is-active nftablesnftables syntax: valid active
Checkpoint: Policy is active without losing recovery access
sudo nft list ruleset && systemctl is-active nftables && ss -lnt | grep ':{{sshPort}}'Continue whenThe named OpenVPN tables and rules are present, nftables is active, the SSH listener remains present, and a second management session still works.
Stop whenThe candidate fails validation, the interface or CIDR is wrong, the second SSH connection fails, or unrelated production forwarding disappears.
If this step fails
nft --check reports a syntax error.
Likely causeA placeholder was not replaced, an interface name contains an unexpected value, or the file was damaged while copying.
grep -n '{{\|}}' /etc/nftables.conf.onelinersip -brief linksudo nft --check --debug=parser --file /etc/nftables.conf.oneliners
ResolutionCorrect the candidate and rerun the check. Because validation happens before install, leave the current live and persistent policy untouched until the candidate is clean.
SSH disconnects or a new management session cannot connect after activation.
Likely cause{{managementCidr}}, {{sshPort}}, or {{publicInterface}} does not match the real management path, or an upstream firewall uses a different source after NAT.
ip -brief addressss -lntpsudo nft monitor trace
ResolutionUse console access to restore /etc/nftables.conf.before-openvpn and restart nftables. Determine the actual observed source and listener before preparing a corrected candidate.
Security notes
- Do not broaden SSH to 0.0.0.0/0 as a quick repair. Restore the prior ruleset through the console and correct the management path deliberately.
Alternatives
- If an existing firewall manager owns the host, translate these narrow input, forward, and NAT requirements into that manager instead of loading this complete ruleset.
Stop conditions
- Do not activate the candidate without a successful syntax check, console recovery, and a verified second SSH session from the intended management source.
command
Start and inspect the VPN
Enable the instance and inspect first startup before distributing profiles.
Why this step matters
Starting the service is the first point where configuration, protected files, kernel TUN support, socket binding, and privilege dropping are exercised together. Inspect the journal immediately rather than treating systemd's active state alone as proof that the VPN is usable.
What to understand
openvpn-server@server maps to /etc/openvpn/server/server.conf in Ubuntu's packaged systemd template. enable records the intended boot behavior; --now starts the instance in the current boot. The status and journal provide separate evidence of process state and OpenVPN initialization.
The phrase Initialization Sequence Completed indicates that the daemon passed configuration and tunnel initialization. It does not yet prove external reachability, client authentication, forwarding, DNS, or revocation; those are tested later.
System changes
- Enables openvpn-server@server for future boots, binds {{vpnPort}}/UDP, creates tun0, assigns the server tunnel address, and starts journal logging.
Syntax explained
systemctl enable --now- Enables the instance for boot and starts it immediately as one explicit operation.
openvpn-server@server- Selects the packaged server template and the server.conf instance name.
journalctl -u- Restricts diagnostic output to the exact OpenVPN systemd instance.
sudo systemctl enable --now openvpn-server@server && systemctl --no-pager --full status openvpn-server@serverActive: active (running)
Checkpoint: The server initialized and is listening
systemctl is-active openvpn-server@server && sudo ss -lunp | grep ':{{vpnPort}}' && sudo journalctl -u openvpn-server@server -b --no-pager | tail -n 30Continue whenThe unit is active, OpenVPN owns the intended UDP socket, tun0 has 10.8.0.1/24, and the journal reports Initialization Sequence Completed.
Stop whenThe unit restarts repeatedly, another process owns the port, tun0 is absent, or the journal contains certificate, CRL, cipher, permission, or route errors.
If this step fails
The unit fails with Cannot open TUN/TAP dev /dev/net/tun.
Likely causeThe TUN kernel device is unavailable in the VM or container, the module is not loaded, or the hosting platform forbids it.
ls -l /dev/net/tunsudo modprobe tunsystemd-detect-virt
ResolutionEnable TUN support in the hypervisor or hosting platform and verify /dev/net/tun. Do not run a privileged container as a shortcut without reviewing the expanded host access.
Security notes
- Do not distribute client profiles until the service, firewall, and journal all pass their checkpoints.
Alternatives
- Keep the instance disabled during a change window by starting without enable; enable it only after the external client test succeeds.
Stop conditions
- Stop on any repeating systemd restart or OpenVPN initialization warning instead of increasing restart limits or log verbosity indefinitely.
command
Issue a unique client certificate and restricted profile
Generate one passphrase-protected identity per person or device and assemble a self-contained profile with only client-side material.
Why this step matters
A unique certificate per person or device creates an identity that can be audited and revoked without affecting every user. The inline profile packages the public CA, client certificate, encrypted client private key, and tls-crypt key into one restricted file while preserving server-purpose verification and modern cipher negotiation.
What to understand
build-client-full prompts for the CA passphrase to sign and for a client-key passphrase unless local Easy-RSA policy says otherwise. The profile intentionally omits nopass so copying the file alone is not sufficient to use the private key. Use a client name that maps to an asset or operator record, not a shared department identity.
The shell block writes fixed client directives and then appends reviewed PEM sections. sed starts the certificate at BEGIN CERTIFICATE so Easy-RSA's human-readable preamble is not embedded. The resulting file is mode 0600 and should move to the client over an authenticated, confidential administrative channel.
System changes
- Adds one client request, encrypted private key, signed certificate, and issuance record to the CA workspace.
- Creates ~/client-configs/{{clientName}}.ovpn containing the client's complete credential bundle and connection policy.
Syntax explained
build-client-full {{clientName}}- Generates a client-purpose key and certificate and records the issuance in the CA database.
remote-cert-tls server- Prevents a different valid client certificate from impersonating the VPN server.
auth-nocache- Requests that OpenVPN not retain interactive authentication material in virtual memory after use.
<ca>, <cert>, <key>, <tls-crypt>- Embeds the four referenced PEM or OpenVPN key files in a portable profile without external file paths.
chmod 0600- Limits local profile access to its owner because the file contains a private key.
Values stay on this page and are never sent or saved.
cd ~/openvpn-ca && ./easyrsa build-client-full {{clientName}} && install -d -m 0700 "$HOME/client-configs" && { printf '%s\n' 'client' 'dev tun' 'proto udp4' 'remote {{vpnHost}} {{vpnPort}}' 'resolv-retry infinite' 'nobind' 'persist-key' 'persist-tun' 'remote-cert-tls server' 'auth-nocache' 'data-ciphers AES-256-GCM:AES-128-GCM:?CHACHA20-POLY1305' 'verb 3' '<ca>'; cat pki/ca.crt; printf '%s\n' '</ca>' '<cert>'; sed -ne '/-----BEGIN CERTIFICATE-----/,$p' "pki/issued/{{clientName}}.crt"; printf '%s\n' '</cert>' '<key>'; cat "pki/private/{{clientName}}.key"; printf '%s\n' '</key>' '<tls-crypt>'; cat pki/tls-crypt.key; printf '%s\n' '</tls-crypt>'; } > "$HOME/client-configs/{{clientName}}.ovpn" && chmod 0600 "$HOME/client-configs/{{clientName}}.ovpn" && stat -c '%a %n' "$HOME/client-configs/{{clientName}}.ovpn"Certificate created at: pki/issued/laptop-alice.crt 600 /home/admin/client-configs/laptop-alice.ovpn
Checkpoint: Profile contains one matching client identity
cd ~/openvpn-ca && openssl verify -CAfile pki/ca.crt pki/issued/{{clientName}}.crt && openssl x509 -in pki/issued/{{clientName}}.crt -noout -subject -issuer -dates && stat -c '%a %n' ~/client-configs/{{clientName}}.ovpnContinue whenCertificate verification is OK, the subject matches {{clientName}}, validity is acceptable, and the profile mode is 600.
Stop whenThe certificate name is shared, the key was created without the intended passphrase, verification fails, or the profile is readable by another user.
If this step fails
Easy-RSA says a certificate with this name already exists.
Likely causeThe identity was previously issued. Reusing the name would make audit and revocation ambiguous.
cd ~/openvpn-ca && ./easyrsa show-cert {{clientName}}grep -F '/CN={{clientName}}' ~/openvpn-ca/pki/index.txt
ResolutionDetermine whether the existing certificate belongs to the same asset. Renew or revoke through the documented lifecycle, or choose a new unique identity; do not delete CA database rows by hand.
Security notes
- Treat the generated .ovpn file as a secret even when its private key has a passphrase. Do not paste it into the OneLiners site, tickets, chat, or source control.
Alternatives
- Use separate certificate and key files when the client platform can protect them in an OS keychain, TPM, smart card, or managed configuration profile.
Stop conditions
- Do not distribute a profile until its certificate purpose, chain, name, validity, file permissions, and destination owner are verified.
verification
Test from an external network
Confirm tunnel address, intended routes, and connectivity from outside the server network.
Why this step matters
Testing from outside the server network proves the layers that local checks cannot: public DNS, upstream firewall, UDP reachability, mutual certificate authentication, pushed routes, DNS, forwarding, and NAT. A successful process on the server alone is not evidence that a remote operator can recover access.
What to understand
Install the test profile on a disposable Ubuntu client connected through a different network, such as a mobile hotspot. The packaged openvpn-client@ template reads /etc/openvpn/client/{{clientName}}.conf. Keep the client journal open during the first connection.
The route lookup should select tun0 for an internet address after redirect-gateway def1. The tunnel address should be inside {{vpnNetworkCidr}}, and 10.8.0.1 should respond as the server endpoint. DNS resolution must succeed after the pushed resolver is active.
System changes
- Installs the restricted profile on the external test client, enables one OpenVPN client instance, creates tun0, and changes that client's IPv4 routing and DNS behavior while connected.
Syntax explained
openvpn-client@{{clientName}}- Maps the instance name to /etc/openvpn/client/{{clientName}}.conf on Ubuntu.
ip route get 1.1.1.1- Shows which interface and next hop the kernel would use without sending application data to that address.
getent ahosts example.com- Exercises the client's configured resolver path through the standard name-service switch.
Values stay on this page and are never sent or saved.
sudo install -m 0600 "$HOME/client-configs/{{clientName}}.ovpn" "/etc/openvpn/client/{{clientName}}.conf" && sudo systemctl enable --now "openvpn-client@{{clientName}}" && systemctl is-active "openvpn-client@{{clientName}}" && ip -brief address show dev tun0 && ip route get 1.1.1.1 && ping -c 3 10.8.0.1 && getent ahosts example.com | headactive tun0 UP 10.8.0.2/24 1.1.1.1 via 10.8.0.1 dev tun0 src 10.8.0.2 3 packets transmitted, 3 received 93.184.216.34 STREAM example.com
Checkpoint: Remote client uses the intended tunnel
systemctl is-active openvpn-client@{{clientName}} && ip -brief address show dev tun0 && ip route get 1.1.1.1 && ping -c 3 10.8.0.1 && getent ahosts example.com | headContinue whenThe unit is active, tun0 has an address in {{vpnNetworkCidr}}, route lookup selects tun0, all three server-tunnel pings return, and DNS produces addresses.
Stop whenThe route still uses the local gateway, DNS stops working, IPv6 bypasses the intended policy, or access succeeds only from the server's own network.
If this step fails
The client times out without any corresponding server journal entry.
Likely causeThe hostname resolves incorrectly, the upstream or provider firewall blocks UDP, the port is not forwarded, or the client network blocks the selected transport.
getent ahosts {{vpnHost}}sudo tcpdump -ni {{publicInterface}} udp port {{vpnPort}}sudo ss -lunp | grep ':{{vpnPort}}'
ResolutionCorrect DNS or the upstream firewall and confirm packets arrive at the public interface. Do not weaken certificate verification when the TLS handshake never reached the server.
Security notes
- A connected client now sends ordinary IPv4 traffic through the gateway. Do not perform the first test from a workstation whose critical access depends on an untested local route.
Alternatives
- For cross-platform acceptance, repeat the same evidence collection on every supported client family after the Ubuntu command-line canary passes.
Stop conditions
- Stop deployment if any traffic family or DNS path bypasses the intended tunnel and the security requirement is full-tunnel enforcement.
instruction
Exercise certificate revocation
Revoke a disposable client, regenerate and deploy the CRL, then confirm the profile cannot reconnect.
Why this step matters
Issuance without a tested revocation path leaves the operator unable to remove a stolen or retired device safely. Revoking a disposable canary proves that the CA database, CRL generation, server installation path, service restart, and client rejection behavior work together before a real incident.
What to understand
revoke changes the certificate status in the CA database; gen-crl creates a newly signed list. Installing that CRL at /etc/openvpn/server/crl.pem publishes the decision to the gateway. The service restart ensures the running instance reads the new file.
The client certificate and key files may remain on disk for evidence, but they must not be reissued as valid. Compare the revoked certificate serial with the CRL and then attempt a new connection. An already established session should be disconnected during an actual compromise.
System changes
- Marks {{clientName}} revoked in the CA database, replaces the generated and installed CRL, and restarts the OpenVPN server instance.
- Prevents new TLS authentication by the revoked certificate while leaving other certificates under the same CA valid.
Syntax explained
./easyrsa revoke {{clientName}}- Records the certificate as revoked in the authoritative CA database.
./easyrsa gen-crl- Generates a signed CRL containing every currently revoked certificate serial.
install -m 0644 pki/crl.pem- Publishes the reviewed CRL atomically at the server path without exposing private key material.
systemctl restart- Forces the server instance to reopen configuration and revocation material.
Values stay on this page and are never sent or saved.
cd ~/openvpn-ca && ./easyrsa revoke {{clientName}} && ./easyrsa gen-crl && sudo install -o root -g root -m 0644 pki/crl.pem /etc/openvpn/server/crl.pem && sudo systemctl restart openvpn-server@server && systemctl is-active openvpn-server@serverRevoking Certificate ... CRL file: pki/crl.pem active
Checkpoint: Revoked serial is rejected while service remains healthy
openssl x509 -in ~/openvpn-ca/pki/issued/{{clientName}}.crt -noout -serial && openssl crl -in /etc/openvpn/server/crl.pem -noout -text | grep -A2 'Revoked Certificates' && systemctl is-active openvpn-server@serverContinue whenThe client serial appears in the CRL, the server remains active, and a fresh connection using that profile fails with a revocation-related verification message.
Stop whenThe serial is absent, the wrong CA produced the CRL, the server fails to restart, or other valid canary clients can no longer connect.
If this step fails
The CRL shows no revoked certificates after revoke.
Likely causegen-crl ran in a different Easy-RSA workspace or the revoke operation did not update the intended CA database.
grep -F '/CN={{clientName}}' ~/openvpn-ca/pki/index.txtopenssl x509 -in ~/openvpn-ca/pki/issued/{{clientName}}.crt -noout -issuer -serialopenssl crl -in ~/openvpn-ca/pki/crl.pem -noout -issuer -lastupdate
ResolutionReturn to the authoritative CA workspace, confirm the certificate row is marked revoked, regenerate the CRL, and verify issuer and serial before installation.
Security notes
- If the CA private key is suspected compromised, individual revocation is insufficient. Build a new PKI and rotate every server and client identity.
Alternatives
- For routine retirement, revoke after a replacement identity is proven. For active compromise, revoke immediately and terminate the current session before issuing replacement credentials.
Stop conditions
- Do not declare revocation complete until a real new handshake with the revoked profile is rejected and a separate valid canary still succeeds.
Finish line
Verification checklist
systemctl is-active openvpn-server@serverReturns active.ss -lunp | grep ':1194'Shows OpenVPN on the intended UDP address.ip address show dev tun0Client receives an address from the planned subnet.Recovery guidance
Common problems and safe checks
The systemd instance exits immediately and the journal reports that a certificate, key, DH file, CRL, or tls-crypt file cannot be opened.
Likely causeA path in server.conf does not match the installed PKI layout, a file was copied under the wrong name, or permissions prevent the process from opening material during initialization.
sudo journalctl -u openvpn-server@server -b --no-pager -n 100sudo namei -l /etc/openvpn/server/server.key /etc/openvpn/server/crl.pemsudo openssl x509 -in /etc/openvpn/server/server.crt -noout -subject -issuer -dates
ResolutionCorrect the path or reinstall the intended file from the reviewed PKI. Keep the private key at mode 0600 and public certificates or the CRL readable as required. Do not replace a missing file with material from an unrelated CA.
The client repeatedly reaches the server but fails with VERIFY ERROR, unsupported certificate purpose, or no shared cipher.
Likely causeThe client trusts a different CA, the profile contains the wrong peer certificate, the certificate was issued with the wrong Easy-RSA type, the certificate is expired or revoked, or old client cipher settings cannot negotiate with OpenVPN 2.6.
openssl x509 -in pki/issued/server.crt -noout -text | grep -A4 'Extended Key Usage'openssl verify -CAfile pki/ca.crt pki/issued/server.crtopenvpn --show-ciphers | grep -E 'AES-256-GCM|AES-128-GCM|CHACHA20-POLY1305'
ResolutionIssue a correctly typed certificate from the intended CA and keep remote-cert-tls verification enabled. Align data-ciphers on supported modern values; add a legacy fallback only after documenting the old peer and accepting the reduced security.
The tunnel connects and receives 10.8.0.x, but internet traffic or DNS does not work.
Likely causeIPv4 forwarding is disabled, the forward chain does not match tun0 and the real egress interface, masquerading is absent, the provider blocks forwarding, or the pushed resolver is not reachable through the selected route.
sysctl net.ipv4.ip_forwardsudo nft list rulesetip route get 1.1.1.1sudo tcpdump -ni tun0 -c 20
ResolutionFix the first failing layer rather than adding broad accept rules. Confirm packets enter tun0, match the intended forward rule, leave the actual public interface, and receive replies. Select a resolver that is reachable after the full-tunnel routes are installed.
A revoked client still reconnects successfully.
Likely causeThe certificate was revoked in a different PKI, gen-crl was not run, the new crl.pem was not installed at the server's crl-verify path, or an already active session was never terminated.
openssl crl -in /etc/openvpn/server/crl.pem -noout -lastupdate -nextupdateopenssl x509 -in pki/issued/{{clientName}}.crt -noout -serialsudo journalctl -u openvpn-server@server --since '-10 min' --no-pager
ResolutionGenerate the CRL from the CA that issued the certificate, install it atomically at the configured path, and restart the instance. Disconnect an existing session during the incident and confirm a new handshake is rejected for the revoked serial.
Reference
Frequently asked questions
Why use UDP instead of TCP?
OpenVPN carries application traffic that may already use TCP. UDP avoids nesting one congestion-controlled TCP connection inside another, which can amplify retransmission and head-of-line blocking. TCP transport remains useful where a network permits only TCP, but it should be an intentional compatibility choice.
Why does the client profile verify remote-cert-tls server?
Trusting the CA alone proves only that a certificate was signed by that CA. Certificate-purpose verification prevents another valid client certificate from impersonating the server and is an important part of mutual TLS identity separation.
Should the CA be created with nopass?
Not for a production trust root. The server key usually needs unattended startup and is commonly generated without an interactive passphrase, but the CA key is used only during issuance and revocation and should be encrypted. Protect the CA passphrase separately from its key backup.
Can this ruleset be added to a Docker or Kubernetes host?
Not unchanged. Those platforms create and depend on forwarding, NAT, and bridge rules of their own. Use a dedicated VPN gateway or merge the intended OpenVPN rules into the platform's authoritative firewall design after testing every workload and recovery path.
Recovery
Rollback
Disable the VPN and restore the previous network policy.
- Stop and disable openvpn-server@server.
- Restore, validate, and reload the saved firewall configuration.
- Remove the forwarding sysctl file and run sudo sysctl --system.
- Retain the PKI securely until every issued profile is retired.
Evidence