Why combine a VPN with a secure shell?

System administrators and developers often rely on SSH for secure remote access. However, using SSH alone can expose servers to fingerprinting, brute-force attempts, and network-layer restrictions. Adding a lightweight VPN layer such as WireGuard ahead of SSH gives you an additional encrypted transport, simplified network routing, and the ability to place services on private virtual networks without exposing them to the public internet.

Architecture overview

At a high level, the recommended architecture places WireGuard between the client and the server, creating a point-to-point encrypted tunnel. SSH runs as usual on the server, but you bind it to the private WireGuard address (or restrict access using firewall rules) so SSH is reachable only via the secure tunnel. This provides:

  • Reduced attack surface: SSH accessible only from VPN peers.
  • Performance: WireGuard’s modern cryptography and kernel-space implementation deliver high throughput and low latency.
  • Simplicity: Static peer configuration, minimal state, and easy key management.

Prerequisites

Before you begin, ensure you have:

  • Root or sudo access on a Linux server (Debian/Ubuntu/RHEL/CentOS/Almalinux).
  • A client machine with administrative rights (Linux, macOS, Windows with WSL or native client).
  • WireGuard packages available (kernel >= 5.6 recommended for native support; userspace tools otherwise).
  • Basic familiarity with SSH keypairs, iptables/nftables, and network routing.

Step 1 — Install WireGuard

On modern Linux distributions, install the kernel module and utilities. Examples:

Ubuntu/Debian:

sudo apt update && sudo apt install -y wireguard

RHEL/CentOS (with EPEL):

sudo yum install -y epel-release && sudo yum install -y kmod-wireguard wireguard-tools

macOS: install the WireGuard.app from the App Store or use Homebrew: brew install –cask wireguard

Windows: install the WireGuard client from wireguard.com.

Step 2 — Generate keypairs

WireGuard uses Curve25519 keypairs. Create server and client keys on their respective machines. On Linux:

wg genkey | tee server_private.key | wg pubkey > server_public.key

wg genkey | tee client_private.key | wg pubkey > client_public.key

Store private keys securely and restrict file permissions (chmod 600).

Step 3 — Create server configuration

Create a config file at /etc/wireguard/wg0.conf with the following structure. Replace with your keys and chosen addresses.

[Interface]

Address = 10.100.0.1/24

ListenPort = 51820

PrivateKey = <server_private_key>

SaveConfig = true

PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE

PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

Then add peers:

[Peer]

PublicKey = <client_public_key>

AllowedIPs = 10.100.0.2/32

Notes:

  • Use a private subnet such as 10.100.0.0/24 to avoid collisions with local networks.
  • PostUp/PostDown rules apply NAT if you want to route client traffic through the server; omit if you only want a point-to-point link.
  • ListenPort (default 51820) can be changed. If behind a firewall or NAT, forward UDP port to the server.

Step 4 — Configure client

On the client, create a WireGuard config (wg0.conf or via GUI). Example:

[Interface]

PrivateKey = <client_private_key>

Address = 10.100.0.2/32

DNS = 1.1.1.1

[Peer]

PublicKey = <server_public_key>

Endpoint = server.public.ip:51820

AllowedIPs = 10.100.0.0/24, 192.168.1.0/24

PersistentKeepalive = 25

Notes:

  • AllowedIPs acts as both route and ACL. If you set 0.0.0.0/0, all traffic will be routed through the server (full-tunnel).
  • PersistentKeepalive ensures NAT mappings stay alive when the client is behind NAT.

Step 5 — Enable and start the interface

Bring up WireGuard on the server:

sudo systemctl enable wg-quick@wg0 && sudo systemctl start wg-quick@wg0

Check status:

sudo wg show

The output shows peers, latest handshake, transfer counters, and allowed IPs — useful for debugging.

Step 6 — Restrict SSH to the WireGuard network

There are two complementary approaches:

  • Bind SSHD to the WireGuard address by editing /etc/ssh/sshd_config and adding: ListenAddress 10.100.0.1. Then restart sshd. This makes SSH unreachable from public interfaces.
  • Use firewall rules to allow SSH from only WireGuard peers (recommended when you keep SSH on the public interface for fallback).

Example iptables rule to allow 22 only from VPN subnet and drop others:

iptables -A INPUT -p tcp –dport 22 -s 10.100.0.0/24 -j ACCEPT

iptables -A INPUT -p tcp –dport 22 -j DROP

Make sure management access isn’t accidentally locked out — keep a root console or alternative access path when testing.

Performance tuning and MTU

WireGuard encapsulates packets, so MTU issues can cause fragmentation. Default MTU is often 1420-1424. You can set MTU in the Interface stanza:

MTU = 1420

Adjust and test using ping with DF (do not fragment) to find the largest size that succeeds. Reducing MSS on TCP can also help:

iptables -t mangle -A FORWARD -p tcp –tcp-flags SYN,RST SYN -j TCPMSS –clamp-mss-to-pmtu

Key management and rotation

WireGuard does not provide an in-protocol key rotation mechanism like some TLS systems. Manage keys via configuration updates:

  • Regenerate the keypair on the peer and update the counterpart’s Peer entry.
  • Use short-lived keys for ephemeral clients and automate distribution via provisioning tools (Ansible, scripts).
  • Keep an inventory mapping public keys to users and expiration metadata in a secure vault if you operate many peers.

Security hardening

Beyond the baseline setup, apply these hardening steps:

  • Disable SSH password authentication and require key-based auth: set PasswordAuthentication no in /etc/ssh/sshd_config.
  • Use Fail2ban but scope it to public-facing services. With SSH behind WireGuard, Fail2ban plays a lesser role but remains useful for other exposed services.
  • Enable UFW/nftables to strictly define accepted traffic. Only permit the WireGuard UDP port and necessary egress rules.
  • Restrict the server’s allowed IPs for each peer — assign minimal necessary ranges.
  • Keep WireGuard and kernel packages updated. While WireGuard’s codebase is small and audited, kernel bugs can affect security.

Troubleshooting checklist

Common issues and quick checks:

  • No handshake: verify UDP port reachability (nc -u or online UDP checkers), correct public keys and endpoint IPs, and NAT configuration.
  • No routes: confirm AllowedIPs on peer include the target addresses. Check ip route show and wg show.
  • SSH unreachable: ensure SSHD is listening on the WireGuard address or firewall allows VPN-subnet traffic to port 22.
  • Slow transfer: check MTU and MSS, CPU usage, and ensure the interface is using kernel-space WireGuard (wg-quick uses kernel module; userspace alternatives like wireguard-go are slower).

Scaling to many users

For dozens or hundreds of peers, manage configuration with automation:

  • Use Ansible or Terraform to generate keys and distribute client configs.
  • Consider dynamic provisioning where clients request a signed client certificate or token from an API that updates server Peer entries. Implement strict auditing.
  • Monitoring: capture metrics (handshake time, throughput per peer) using logs or SNMP and integrate with Prometheus/Grafana.

Advanced topics

Split-tunneling and split-dns: configure client AllowedIPs to route only specific subnets through WireGuard and set DNS only for VPN-resolved names. This reduces unnecessary server egress and improves performance.

High availability: place WireGuard on a floating IP managed by keepalived or use a load balancer for UDP with consistent hashing. Keep state in sync by rotating allowed IPs across HA nodes.

Auditing and compliance: record peer PublicKey fingerprints, assignment timestamps, and the responsible operator for compliance. Rotate keys periodically and log configuration changes.

Wrap-up

Using WireGuard as a secure transport in front of SSH gives administrators a fast, modern, and maintainable way to protect remote access. The combination reduces exposure to the internet, simplifies routing, and yields excellent throughput thanks to WireGuard’s efficient crypto. Implement the basic setup, lock down SSH to the VPN, tune MTU, and automate key management as your environment grows.

For a clean deployment in production, document peer assignments, automate configuration management, and integrate monitoring and alerting so you can keep tight control over who can access servers via the VPN-protected SSH channel.

Published by Dedicated-IP-VPN