Skip to content


How to setup and use OpenVPN

The next recipe works on CentOS 7 as for beginning of 2025 year.

  1. yum install openvpn easy-rsa
  2. cd /etc/openvpn
  3. /usr/share/easy-rsa/3.0.8/easyrsa init-pki
  4. /usr/share/easy-rsa/3.0.8/easyrsa build-ca nopass
  5. /usr/share/easy-rsa/3.0.8/easyrsa --days=365 build-server-full server nopass
  6. /usr/share/easy-rsa/3.0.8/easyrsa --days=365 build-client-full client1 nopass
  7. /usr/share/easy-rsa/3.0.8/easyrsa --days=365 build-client-full client2 nopass
  8. openssl dhparam -out /etc/openvpn/server/dh.pem 2048
  9. Create config-file /etc/openvpn/server.conf:
    port 1194
    proto udp
    dev tun
    ca /etc/openvpn/server/pki/ca.crt
    cert /etc/openvpn/server/pki/issued/server.crt
    key /etc/openvpn/server/pki/private/server.key
    dh /etc/openvpn/server/dh.pem
    server 10.8.0.0 255.255.255.0
    ifconfig-pool-persist ipp.txt
    push "redirect-gateway def1"
    push "dhcp-option DNS 8.8.8.8"
    push "dhcp-option DNS 8.8.4.4"
    keepalive 10 120
    cipher AES-256-CBC
    user openvpn
    group openvpn
    persist-key
    persist-tun
    status /var/log/openvpn-server-status.log
    log-append /var/log/openvpn.log
    verb 3
  10. Create .ovpn file for client:
    client
    dev tun
    proto udp
    remote vpn-server.host.name 1194 udp
    resolv-retry infinite
    nobind
    persist-key
    persist-tun
    cipher AES-256-CBC
    verb 3
    <cert>
    -----BEGIN CERTIFICATE-----
    // client base64-encoded cert goes here
    -----END CERTIFICATE-----
    </cert>
    <key>
    -----BEGIN PRIVATE KEY-----
    // client base64-encoded private key goes here
    -----END PRIVATE KEY-----
    </key>
    <ca>
    -----BEGIN CERTIFICATE-----
    // ca base64-encoded cert goes here
    -----END CERTIFICATE-----
    </ca>

    Or, alternatively, if you don't want to embed certs and key to the .ovpn config file itself, you can reference them with these 3 lines:

    ca ca.crt
    cert client1.crt
    key client1.key
  11. Open UDP port 1194 in the server's firewall.
  12. Setup NAT and forwarding if needed.
  13. To automatically start server using systemd place server config file to the directory /etc/openvpn/server and name it accordingly, for example udp1194.conf. Then the command should be:
    systemctl enable openvpn-server@udp9997 --now

What is the difference between tun and tap devices which openvpn provides?

They're two different kernel interface types from the same tun module, and the distinction is what layer of the OSI stack the tunnel carries.

tun — layer 3, point-to-point. Carries bare IP packets. No Ethernet header, no MAC addresses, no ARP, no broadcast domain. The interface has no L2 identity at all — ip link show tun0 shows link/none.

tap — layer 2, virtual Ethernet. Carries full Ethernet frames. The interface has a MAC address, participates in ARP, and can be enslaved to a bridge alongside a physical NIC, putting remote clients on the same broadcast domain as the LAN.

  tun tap
Encapsulates IP packets Ethernet frames
Per-packet overhead none extra +14 bytes (Ethernet header)
Broadcast/multicast not carried carried, floods to all clients
Non-IP protocols no yes (IPX, AppleTalk, STP, raw ARP)
DHCP from LAN server no (OpenVPN assigns via push) yes, client can DHCP off the real LAN
Bridging to physical NIC no yes
Android / iOS clients supported not supported
OpenVPN DCO supported not supported
Scales to many clients yes poorly

Why tun is the default answer

The tap cost is mostly invisible until you have clients: every ARP request, every DHCP discover, every Windows NetBIOS/mDNS/SSDP broadcast on the LAN gets replicated and pushed down every single tunnel. On a link with 20 remote clients over consumer uplinks that's a constant background load doing nothing useful, and it's why bridged setups feel fine at 2 clients and terrible at 30.
Additional practical constraints:

  • Mobile clients can't do tap at all. The Android and iOS VPN APIs only expose an L3 interface. If any client is a phone, the decision is already made.
  • DCO rules out tap. If you're using ovpn-dco / ovpn kernel offload on Rocky or Fedora for throughput, it's L3-only. Configuring dev tap silently falls back to userspace tunneling (or errors, depending on version), losing the reason you enabled it.
  • tap on Windows requires the separate tap-windows6 driver; the newer dco-win/wintun path doesn't cover it.

When tap is right choice

  • Non-IP protocols on the wire, or legacy software that discovers peers only via L2 broadcast.
  • Windows browsing/discovery that must work without a WINS or mDNS repeater.
  • Wake-on-LAN over the tunnel, or anything needing raw frames.
  • Small site-to-site links (2–3 endpoints) where flattening the L2 domain saves a lot of routing config.

How to avoid default route replacement on client after VPN-connection initializing?

Fix on the server (affects all clients)

Just don't push it:

# /etc/openvpn/server/server.conf — remove or comment
#push "redirect-gateway def1 bypass-dhcp"

and push only the routes to subnets that should go through the tunnel, /etc/openvpn/server/server.conf file should contain:

push "route 10.10.3.0 255.255.255.0"
push "route 192.168.50.0 255.255.255.0"

Or it can be done per-client instead of globally, via client-config-dir:

# server.conf:
client-config-dir /etc/openvpn/server/ccd
# /etc/openvpn/server/ccd/<common-name>  — must match the client's cert CN exactly
push "route 10.10.3.0 255.255.255.0"

Note ccd files can't un-push a global push "redirect-gateway ..." — a global push always applies. If some clients need full tunnel and others don't, move redirect-gateway out of the global section entirely and push it only from the ccd files for specific clients that want it.

Fix on the client (when you can't change the server, for example)

The surgical option — drop just that one directive, keep everything else including DNS and routes:

# client.ovpn
pull-filter ignore "redirect-gateway"
pull-filter ignore "dhcp-option DNS"     # If you need to drop the pushed DNS as well
pull-filter ignore "dhcp-option DOMAIN"  # If you need to drop the pushed DNS as well
pull-filter ignore "block-outside-dns"   # If you need to drop the pushed DNS as well

pull-filter uses a prefix match on the whole option string, evaluated in the order the filters appear. So:

pull-filter ignore "route"  # ALSO kills route-gateway and route-metric → breaks tunnel routing
pull-filter ignore "route " # Note trailing space: only actual "route x.x.x.x" lines

The blunt option — ignore all pushed routes and DHCP options. Note, that route-nopull kwyword also discards pushed dhcp-option (DNS, DOMAIN, block-outside-dns), so you must re-add anything you still need locally:

# client.ovpn
route-nopull
route 10.10.3.0 255.255.255.0
route 192.168.50.0 255.255.255.0
dhcp-option DNS 10.10.3.10

Third option — receive routes but install them yourself in a script:

# client.ovpn
route-noexec
up /etc/openvpn/my-routes.sh

route-noexec still populates $route_network_1, $route_gateway_1, $ifconfig_local etc. In the my-routes.sh script environment, so you can install selectively. Useful when you want the pushed list but with your own metrics or table.

Posted in Howto.

Tagged with .


0 Responses

Stay in touch with the conversation, subscribe to the RSS feed for comments on this post.

You must be logged in to post a comment.