Protecting a VPN server from DDoS attacks
Why the address of a VPN server must be public and sits in every client file, what WireGuard does on its own with mac1 and the cookie reply, how tls-crypt relieves OpenVPN, and from which attack size on only filtering in the network in front of the server helps.
A VPN server has a problem that a web server does not have: its IP address must be publicly reachable, otherwise not a single client could get in, and it sits in plain text in the configuration file of every single user. You cannot hide it behind a CDN, and you cannot change it without reconfiguring every device. That is exactly why, for a VPN, filtering in the network in front of the server is not one option among several. It is the only one.
This article first explains why VPN servers get attacked at all, then what WireGuard and OpenVPN can do against a flood on their own, then which measurements actually prove an attack, and finally which rules on the server are worth writing and where they stop. Everything here refers to Debian 12, Debian 13, Ubuntu 22.04 LTS or Ubuntu 24.04 LTS. The commands are written for root. As a normal user, put sudo in front of them. If the attack is running right now, change nothing in the configuration and do not restart the service: capture the measurements first, because once the attack is over they are gone.
Why a VPN server gets attacked differently than a web server
For a web server, the public address is interchangeable. You put a proxy in front of it, you move the application to another machine, you change an A record in DNS, and a few minutes later the traffic lands somewhere else. For a VPN server none of these three routes works cleanly. That is not a misconfiguration, it follows from the nature of the thing.
The address is in every configuration file
A VPN client needs an endpoint, and that endpoint is an IP address or a name that resolves to one. With WireGuard it sits in the line Endpoint = 203.0.113.10:51820 of the client file, with OpenVPN in the line remote 203.0.113.10 1194. Everybody who ever received a profile knows the address: the employee who left and whose profile nobody deleted, the family member who passed the file on, and anybody who lost a phone with an imported profile.
On top of that comes a difference between the two protocols that matters precisely here. WireGuard resolves a hostname in the endpoint exactly once, namely when the interface comes up with wg-quick up. If the DNS record changes afterwards, a running connection never notices and keeps sending to the old address. That is why wireguard-tools ships the helper script reresolve-dns.sh, which re-resolves the name periodically and updates the peer through wg set. OpenVPN behaves differently: remote may appear several times, and together with resolv-retry infinite the client resolves again on every connection attempt. Changing the address is therefore workable with OpenVPN, and with WireGuard only with extra work on every device.
Why a CDN in front of a VPN server does not work
A CDN accepts HTTP and HTTPS requests, reads the hostname from the TLS extension SNI or from the Host header, and passes them to the right origin server. A VPN supplies none of that. WireGuard speaks its own UDP protocol with no HTTP layer and no TLS, so there is neither a hostname in the packet nor a session that could be terminated and rebuilt somewhere else. OpenVPN over UDP is the same. Even OpenVPN over TCP on port 443 is not HTTPS: it only looks that way as long as nobody looks closely, and an HTTP proxy cannot do anything with it.
What remains is pure packet forwarding at layers 3 and 4, and that is exactly what a filtering network does. The difference to a CDN matters, though: the protection has to sit where your address already belongs, which means in the provider network in front of the server. Putting something in front that you rent yourself and that you migrate your clients onto is not a practical answer for a VPN.
What an attacker achieves against a VPN server
An attack on a web server takes down a website. An attack on a VPN server takes down access to everything behind it. For a company VPN that means, concretely: nobody reaches the ERP system, the ticket system, the file share or the internal database any more, and that stays true even though all of those systems keep running completely undisturbed. Work stops because the one route to them is closed.
The second point is worse. Anybody who made their administrative access reachable only through the VPN (and that is exactly the recommended design) is standing outside their own door during the attack. SSH is gone, monitoring is gone, the panel is gone. That is why every VPN server needs a second, network independent route, more on that below. For what a DDoS attack technically is and how it is assembled, read What is a DDoS attack?.
The ports and protocols that are actually involved
A VPN server needs exactly one port on the open internet. Everything else in this table either does not belong on the internet at all, or only on your own address.
| Service | Port | Protocol | Where it is set | Open to the internet? |
|---|---|---|---|---|
| WireGuard | 51820 | UDP | /etc/wireguard/wg0.conf: ListenPort |
yes, the only one |
| OpenVPN (default) | 1194 | UDP | server.conf: port and proto udp |
yes, the only one |
| OpenVPN over TCP | 1194 or 443 | TCP | server.conf: proto tcp-server |
only if truly required |
| IKEv2/IPsec | 500 and 4500 | UDP | ipsec.conf or swanctl.conf |
yes, both together |
| OpenVPN management interface | 7505 (freely chosen) | TCP | server.conf: management |
no, 127.0.0.1 only |
| DNS resolver inside the VPN | 53 | UDP and TCP | dnsmasq or unbound, bound to the VPN interface | no, never |
| SSH | 22 | TCP | /etc/ssh/sshd_config |
your own address only |
One row of this table deserves special attention. If you run your own DNS resolver on the VPN server so that clients can resolve names, you have a problem exactly when that resolver accidentally listens on 0.0.0.0 instead of only on the VPN interface. The resolver then becomes an open amplifier that gets abused for attacks against third parties, and your line carries the load. The lines interface=wg0 and bind-dynamic in dnsmasq are therefore not cosmetics. How to set this up cleanly is in Set up a WireGuard VPN server.
WireGuard under attack: what the protocol itself does
One port, one protocol, and nothing else
WireGuard speaks UDP only and listens on exactly one port, 51820 by default. There is no TCP, no second control channel, no status port and no remote management interface. The entire network attack surface consists of a single UDP port, and on that port the protocol knows only four message types, identified by the first byte of the UDP payload:
| Type | Message | Size | Direction at the server | What it costs |
|---|---|---|---|---|
| 1 | Handshake Initiation | 148 bytes | inbound | expensive: Curve25519, only after mac1 passes |
| 2 | Handshake Response | 92 bytes | outbound | the server's answer |
| 3 | Cookie Reply | 64 bytes | outbound, only under load | cheap: one MAC over address and port |
| 4 | Transport Data | 32 byte header plus payload | inbound | ChaCha20Poly1305, inexpensive |
This table is more than theory, it is the basis of every useful filter rule on the server. A pure dial-in server that clients connect to receives type 1 and type 4 only. It receives type 2 and type 3 only when it initiates connections itself, meaning a site-to-site setup with a fixed Endpoint configured for the far side. Anything else arriving on port 51820 is not a valid WireGuard packet.
The handshake, and why mac1 is the actual filter
WireGuard builds its handshake on the Noise protocol framework, specifically on the Noise IKpsk2 variant with Curve25519 for key exchange, ChaCha20Poly1305 for encryption and BLAKE2s as the hash function. The expensive part of that is the elliptic curve computation, and that is exactly what you do not want to hand an attacker for free.
That is why every handshake message carries a field called mac1 at the end. It is a keyed hash over the whole message, and the key for it is derived from the recipient's public key. The consequence: anybody who does not know the server's public key cannot produce a valid mac1. The server checks mac1 first of all, with a single hash operation. If the check fails, the packet is dropped before a single curve operation has happened.
That is why a blind handshake flood against a WireGuard server costs surprisingly little CPU time: the attacker does not know the server's public key and therefore only produces packets that land in the bin after one hash. The bad news sits on the other side of the same coin. Anybody who owns a valid client profile knows the server's public key, because it sits under [Peer] PublicKey in every profile file. An angry former user can therefore absolutely build packets that pass the mac1 check and force the server into the expensive computation. That is the case that matters in practice.
The cookie reply under load, and what it achieves
WireGuard has a second mechanism for exactly that case. As soon as the server is under load, it answers an initiation not with a handshake but with a cookie reply (type 3, 64 bytes). Inside it, encrypted to the sender, sits a cookie computed from a server-side secret plus the source address and source port of the sender. The client has to fold that cookie into its next attempt as a second field called mac2, otherwise the attempt still counts as unconfirmed.
The effect is precisely bounded and should not be overstated. An attacker who spoofs the source address never gets to see the cookie reply, because it goes to the spoofed address. So they cannot build a valid mac2 and can never force the server into the expensive computation. The cookie reply makes spoofed handshake floods ineffective, but it does not make them invisible. The packets still arrive, still occupy your line and still cost one hash each. Against an attack that simply fills the line, the mechanism does nothing.
When does the server count as under load? In the kernel implementation, when the queue of incoming handshakes reaches one eighth of its capacity, meaning 512 of 4096 entries, or when that state occurred at least once within the last second. That is a technical detail with a practical consequence: in normal operation you never see a cookie reply, and under attack they are suddenly all you see.
The built-in rate limiter in the kernel
WireGuard additionally ships its own rate limiter for handshake processing. The values are fixed in the source of the kernel implementation and are not configurable: 20 handshake packets per second and source, with a burst allowance of five packets. For IPv4 the source is the full address, for IPv6 it is the entire /64. That choice is deliberate, because otherwise an attacker holding an IPv6 prefix would have an unlimited supply of apparently different addresses.
Remember the number 20 per second: it is the yardstick for how tightly you may set your own nftables rule without hitting real clients. A regular client builds a new handshake every 120 seconds and retries a failed attempt every five seconds. Even half a dozen devices behind one shared line stay well below one handshake per second.
Why WireGuard stays silent, and what that means for detection
A WireGuard server answers no packet at all that fails the mac1 check. It sends no error message, no ICMP port unreachable, nothing. To a port scan the port is therefore indistinguishable from a filtered or closed port, nmap reports open|filtered and gets no further.
That has two consequences, a good one and a bad one. The good one: WireGuard is useless as an amplifier for attacks against third parties. Even if an attacker holds the server's public key, the server answers 148 bytes with 92 bytes, or under load with 64 bytes. That is an amplification factor of 0.6 and 0.43 respectively, so a reduction. For comparison, the protocols that commonly get abused sit at factors of roughly 30 to more than 51,000, as covered in What is a DDoS attack?.
The bad consequence: your VPN service will not report an attack to you. Under a flood a web server writes thousands of lines into its access log, a WireGuard server writes nothing at all, because it does not log what it drops immediately. An attack on a WireGuard server is visible only in the counters of the network card and the kernel, never in the service itself. If you only look at wg show, all you see is that the handshakes are getting old, with no indication why.
OpenVPN under attack: UDP, TCP and the expensive handshake
UDP or TCP, and why TCP suffers more under attack
OpenVPN runs over either UDP or TCP, with UDP on port 1194 as the default. TCP is usually chosen when a restrictive network blocks everything except port 443. Under attack that choice is expensive, for three reasons.
First, every TCP connection creates state in the kernel before OpenVPN even sees it. A SYN flood with spoofed senders fills the queue of half-open connections, and a few tens of thousands of SYN packets per second are enough to cripple connection handling on a standard Linux box long before the line is full. SYN cookies help against that, because they do not store the state but recompute it from the client's answer (net.ipv4.tcp_syncookies must be 1). With UDP the problem does not exist, because there is no connection setup that could be left half open.
Second, TCP suffers inside TCP. The user's application usually speaks TCP itself, and that TCP then runs inside a TCP connection. If a segment is lost in the outer stream, the outer TCP enforces ordering and blocks everything behind it until the retransmission arrives. The inner TCP reads the resulting delay as congestion and throttles on top. Under packet loss, and packet loss is exactly what an attack produces, throughput collapses disproportionately. Over UDP a lost packet is simply missing, and the inner TCP sorts it out itself.
Third, an established but silent connection keeps the server busy. Anybody who completes a TCP connection and then says nothing occupies a slot until the handshake window expires. That window is controlled by hand-window and defaults to 60 seconds. With a few hundred such connections an attacker fills the server without spending any meaningful bandwidth. The recommendation is therefore unambiguous: run OpenVPN over UDP unless a network forces you onto TCP. If you need both, run two instances and keep the TCP instance small.
tls-auth and tls-crypt as a filter in front of the expensive TLS handshake
The actual reason OpenVPN suffers under a handshake flood is the control channel. A new client sends a packet of type P_CONTROL_HARD_RESET_CLIENT_V2, and the server responds by starting a full TLS handshake with certificate validation. That is orders of magnitude more expensive than anything WireGuard does, and in mode server OpenVPN is single threaded, so it works through those handshakes one after another.
There are mechanisms against this, all with the same goal: drop an unauthorized packet before TLS is touched at all.
tls-auth ta.key 0attaches an HMAC to every control channel packet, computed with a shared static key. OpenVPN drops packets without a valid HMAC before handing them to the TLS layer. This is the older mechanism, often called an HMAC firewall. The control channel stays readable, it just cannot be forged.tls-crypt ta.key, available from OpenVPN 2.4, goes one step further and additionally encrypts the control channel, with AES-256-CTR and HMAC-SHA-256. An observer then cannot even tell that a TLS handshake is happening here, and a scanner does not recognize the service as OpenVPN. The direction parameter is gone, the line is identical on both sides.tls-crypt-v2, available from OpenVPN 2.5, hands out a separate key per client instead of one shared key. That is the right choice as soon as you have more than a handful of users, because a departing user can then be removed without giving everybody else a new file.
In the server configuration it looks like this. Note that tls-crypt and tls-auth are mutually exclusive, using both is a configuration error:
proto udp
port 1194
dev tun
tls-crypt /etc/openvpn/server/ta.key
auth SHA256
cipher AES-256-GCM
tls-version-min 1.2
remote-cert-tls client
crl-verify /etc/openvpn/server/crl.pem
verb 3
mute 20
explicit-exit-notify 1
If you have been using tls-auth so far, you can switch to tls-crypt, but you have to replace all client profiles at the same time. The key material itself stays the same (openvpn --genkey secret ta.key produces 2048 bits of random material in both cases), only the usage changes.
connect-freq and connect-freq-initial
OpenVPN brings two brakes of its own, and both matter under attack. connect-freq n sec limits how many new connections the server accepts within a period at all. connect-freq-initial n sec exists from OpenVPN 2.6, applies to UDP only, and limits the number of replies to initial connection packets. The default is 100 replies per 10 seconds, and connection attempts that actually complete the setup do not count against it.
The stated purpose of that option is reflection defense: an OpenVPN server that answers every initial packet can be pressed into service as a reflector against a foreign target using a spoofed source address. The amplification factor is small, in the order of two, so far away from the factors of the well known amplifier protocols. The real damage lies elsewhere: your line carries the load, and your address ends up on the lists of networks that send suspicious traffic.
connect-freq 20 10
connect-freq-initial 30 10
max-clients 50
Set max-clients to the number you actually need and no higher. Every slot is a resource that can be occupied. And leave out duplicate-cn if every user has their own certificate: without that option a second connection using the same certificate name throws out the first, which an attacker holding a leaked profile can exploit on purpose.
WireGuard and OpenVPN side by side
| Property | WireGuard | OpenVPN |
|---|---|---|
| Transport | UDP only | UDP or TCP |
| Default port | 51820 UDP | 1194 UDP, often 443 TCP |
| Answer to unauthorized packets | none, always silent | an answer without tls-crypt, silent with it |
| Filter before the expensive work | mac1, one hash per packet | tls-auth or tls-crypt, one HMAC per packet |
| Defense against spoofed senders | cookie reply under load | connect-freq-initial, plus SYN cookies on TCP |
| Built-in rate limiter | 20 handshakes per second and source, hard coded | connect-freq, freely configurable |
| Processing | in the kernel, uses multiple cores | in a user process, handshakes one after another |
| Changing the server address | name resolved only at interface start | several remote entries, resolved on every attempt |
| Recognizable to scanners | no, open|filtered |
yes without tls-crypt, no with it |
| Usable as an amplifier | no, factor below 1 | marginally without tls-crypt, factor about 2 |
The four attack types that hit a VPN server
1. UDP flood against the VPN port
The simplest and most common variant. The attacker sends arbitrary UDP packets to 51820 or 1194, usually with spoofed source addresses. They need no knowledge about your VPN for this, not even the port number has to be right, because a full line is a full line no matter which port the packets knock on. WireGuard drops them after one hash, OpenVPN with tls-crypt after one HMAC. Both cost little CPU time and change nothing about the fact that the packets already occupied your line when they arrived.
2. Handshake flood
The more targeted variant, and the only one that requires prior knowledge. The attacker builds packets that pass as connection setup: with WireGuard an initiation with a valid mac1, which requires the server's public key, with OpenVPN a P_CONTROL_HARD_RESET_CLIENT_V2 with a valid HMAC, which requires the ta.key. Both sit in every client profile you ever handed out. The typical trigger is therefore not an anonymous attacker from the internet but somebody who once had access.
With OpenVPN the leverage is particularly large, because a full TLS handshake with certificate validation hangs behind it and the server works through those one at a time. With WireGuard the built-in rate limiter kicks in first and the cookie reply second. The countermeasure is the same in both cases and it costs nothing: take departed users out of the configuration. With WireGuard that is the relevant [Peer] block, with OpenVPN the certificate revocation in crl.pem, and with tls-crypt-v2 the client key as well.
3. Amplification attack against the line
Here the attacker is not interested in your VPN at all. They send small requests with your address spoofed as the sender to foreign, misconfigured services on the internet, and those services deliver their large answers to you. DNS, NTP, CLDAP, SSDP and, in the extreme case, memcached deliver factors of roughly 30 to more than 51,000. Your VPN server has nothing running on any of those ports and drops everything correctly, but the packets already filled your line before the kernel could even look at them.
This is the attack for which there is literally no setting on the server. It hits the address, not the service.
4. Targeted packet rate saturation
The nastiest variant, because it looks harmless on the bandwidth graph. Instead of large packets the attacker sends very many very small ones. A 1 Gbit/s line fits roughly 1.49 million packets per second at a packet size of 64 bytes, and depending on CPU and network card a normal server kernel processes only a few hundred thousand of those before it starts dropping. The bandwidth graph might show 200 Mbit/s, and still nothing is reachable.
For a VPN server there is an additional twist: every packet arriving on the VPN port has to be touched cryptographically at least once before it may be dropped. The cost per packet is therefore higher than on a web server, which drops a packet on a closed port immediately. A VPN server hits its packet rate ceiling earlier than other services on the same line.
How you know your VPN server is under attack
WireGuard: wg show and the latest handshakes
The first look goes at the state of the peers. Two commands are enough:
wg show wg0 latest-handshakes
wg show wg0 transfer
latest-handshakes prints a Unix timestamp per peer. Work out the difference to the current time: anything over 180 seconds means the session has expired and no new handshake came through. If all peers go stale at the same time, that is a network problem and not a client problem. If under transfer the received bytes stop while the sent bytes keep rising, your packets are going out and nothing is coming back.
For context on the timings: WireGuard renews a session after 120 seconds, discards it after 180 seconds, retries a failed handshake every five seconds, and gives up after 90 seconds until new traffic appears. That is exactly the explanation for the common phenomenon that a VPN does not come back by itself once an attack ends: the client gave up and is waiting for a packet from the inside. With PersistentKeepalive = 25 in the client file it keeps trying indefinitely instead.
OpenVPN: the journalctl lines that are unambiguous
OpenVPN is talkative, and under attack that is an advantage:
journalctl -u openvpn-server@server -n 200 --no-pager
journalctl -u openvpn-server@server -f
Three messages are meaningful, and each means something different:
TLS Error: cannot locate HMAC in incoming packet frommeans packets are arriving that do not know thetls-authortls-cryptkey. A few of those are scanners, thousands of them are a flood. The filter is working correctly here, the message is the proof of it.TLS Error: TLS key negotiation failed to occur within 60 secondsmeans a connection setup started and never finished. Those are either clients with packet loss or connections deliberately left hanging.Authenticate/Decrypt packet error: packet HMAC authentication failedconcerns the data channel of an existing session and points at an MTU or key problem rather than at an attack.
Under attack set verb 3 and no higher. A higher level writes one line per dropped packet, and then the logging cripples the server that the attack alone would not have taken down. mute 20 collapses repetitions and belongs in every server configuration.
nstat, ss and the counters of the network card
These are the measurements that hold regardless of the VPN software, and you need them in any case:
ip -s link show eth0
nstat -az | grep -E 'Udp|IpReasm'
ss -lnup
cat /proc/net/softnet_stat
ethtool -S eth0 | grep -iE 'drop|miss|discard'
Run ip -s link show eth0 twice ten seconds apart and take the difference, and you have your actual packet rate. If the dropped column rises in the same output, the network card or the driver is already dropping, and that is the hardest available proof of overload. In /proc/net/softnet_stat the second column per core is the number of packets dropped because that core's backlog was full. If anything other than zeros sits there, the packet rate is the cause and not the bandwidth.
With nstat the two VPN programs differ substantially, and this is the most practically important diagnostic rule in this article. Kernel WireGuard processes packets directly in the kernel network path and not through the queue of an application socket. The counter UdpRcvbufErrors therefore stays at zero there, even under a heavy attack. Losses show up only at the network card and in softnet_stat. With OpenVPN it is the other way around: the service reads from an ordinary UDP socket, and a rising UdpRcvbufErrors there means the process cannot keep up. Bigger buffers help against that:
sysctl -w net.core.rmem_max=16777216
sysctl -w net.core.rmem_default=1048576
sysctl -w net.core.netdev_max_backlog=16384
Also check IpReasmFails and IpReasmTimeout. If those rise, fragmented packets are arriving whose pieces never become complete. On a pure WireGuard server that is always suspicious, because WireGuard sets the Don't Fragment bit in the outer IP header and never produces fragments itself.
The cross check: attack, MTU problem or your own mistake
Before you report an attack, rule out the two common mix-ups. An MTU problem looks similar to an attack at first glance, because small packets get through and large ones hang: the handshake works, ping works, but web pages only load halfway. The test for it takes ten seconds, namely setting MTU = 1280 in the client file as an experiment. If everything runs cleanly with that, it was the MTU and not an attack.
The second case is your own firewall. Check whether your rules are reached at all by looking at the counters. If they stay at zero, the rule is never reached, and that is a completely different diagnosis from a rule that has no effect. The systematic approach, including how to tell a traffic surge from an attack, is in Detect a DDoS attack on your server, and the order of steps in an emergency is in What to do during a severe DDoS attack.
What helps on the server, and what the rules look like
This section is the longest, and that is deliberate. A cleanly configured VPN server survives small and medium attacks on its own, no matter who hosts it.
1. Inventory: what is actually listening?
Before you write a single rule, look at what your server offers to the outside. Do not guess, look:
ss -lntup
wg show
systemctl status openvpn-server@server
The interesting column is the local address. 0.0.0.0:51820 means reachable from the whole internet, 127.0.0.1:7505 means local only and needs no firewall rule. If a DNS resolver shows up on 0.0.0.0:53, that is the most urgent item on your list. The attacker's view comes from a scan from outside:
nmap -Pn -sU -p 51820,1194,500,4500 YOUR.SERVER.IP.ADDRESS
nmap -Pn -sT -p- --min-rate 1000 YOUR.SERVER.IP.ADDRESS
2. Rate limiting per source address on the VPN port
Now the core of it. The rule exploits the fact that WireGuard carries its message type in the first byte of the UDP payload and that the initiation has a fixed size: 148 bytes of payload, so 156 bytes in the length field of the UDP header. That lets you limit the expensive handshake separately from the inexpensive data traffic, and that is exactly the difference to a flat rule covering all packets.
#!/usr/sbin/nft -f
flush ruleset
define WG_PORT = 51820
define OVPN_PORT = 1194
table inet vpn {
set adminips {
type ipv4_addr
flags interval
elements = { 203.0.113.10 }
}
chain input {
type filter hook input priority filter; policy drop;
iif lo accept
ct state established,related accept
ct state invalid counter drop
ip saddr @adminips tcp dport 22 accept
udp dport $WG_PORT @th,64,8 == 4 accept
udp dport $WG_PORT udp length 156 @th,64,8 == 1 meter wg_hs { ip saddr limit rate over 2/second burst 10 packets } counter drop
udp dport $WG_PORT udp length 156 @th,64,8 == 1 accept
udp dport $WG_PORT counter drop
udp dport $OVPN_PORT @th,64,5 == 7 meter ovpn_reset { ip saddr limit rate over 2/second burst 10 packets } counter drop
udp dport $OVPN_PORT accept
icmp type echo-request limit rate 5/second accept
icmpv6 type { echo-request, nd-neighbor-solicit, nd-neighbor-advert, nd-router-solicit, nd-router-advert } accept
counter drop
}
chain forward {
type filter hook forward priority filter; policy drop;
iifname { "wg0", "tun0" } accept
oifname { "wg0", "tun0" } ct state established,related accept
}
chain output { type filter hook output priority filter; policy accept; }
}
Put your own fixed address under adminips before you load this, otherwise you lock yourself out of SSH. Checking and activating works like this, and the first command is the actual protection against a typo:
nft -c -f /etc/nftables.conf
systemctl enable --now nftables
nft list table inet vpn
nft list meter inet vpn wg_hs
Four points decide whether these rules work or cause damage.
@th,64,8 is the first byte after the UDP header. The UDP header is eight bytes long, which is 64 bits, and @th counts from its start. With WireGuard the message type sits there. With OpenVPN the same byte holds the opcode in the upper five bits and the key id in the lower three, which is why the rule uses @th,64,5 and the value 7 for P_CONTROL_HARD_RESET_CLIENT_V2. For clients using tls-crypt-v2 the value 10 comes on top, so add a line with @th,64,5 == 10.
The data packets deliberately come first. A packet that is going to be accepted should be accepted as early as possible, because every rule in front of it costs CPU time multiplied by the packet rate. On a running VPN more than 99 percent of all inbound packets are type 4.
ip saddr inside the meter only matches IPv4. In an inet table you need a second line with ip6 saddr for IPv6, otherwise all your IPv6 traffic passes unthrottled. Use a /64 as the key there rather than the full address, exactly as WireGuard does internally.
Two handshakes per second is a starting value, not a truth. A real client needs one every 120 seconds. An office or a shared flat behind one address needs correspondingly more, and a mobile carrier network can put hundreds of your users behind the same address. Measure a week of normal operation before you tighten the values, and check the counter regularly afterwards. If it rises while no attack is running, the value is too low.
3. Turn off conntrack for the VPN port
The kernel connection tracker creates an entry for every UDP tuple of addresses and ports, including for a single packet that goes nowhere. A UDP flood with spoofed senders therefore creates one entry per packet, and once the table is full the kernel reports nf_conntrack: table full, dropping packet and then drops packets from your real users as well. You can check it like this:
sysctl net.netfilter.nf_conntrack_max
cat /proc/sys/net/netfilter/nf_conntrack_count
dmesg | grep -i conntrack
For a VPN port that bookkeeping is useless anyway, because a packet's membership follows from the cryptography and not from a table in the packet filter. So take the port out of tracking:
table inet raw {
chain prerouting {
type filter hook prerouting priority raw; policy accept;
udp dport { 51820, 1194 } notrack
}
}
The consequence matters: packets on those ports no longer have a connection state, so the rule ct state established,related accept does not apply to them. That is exactly why the ruleset above has an explicit line for every VPN port. Anybody who sets notrack and forgets the explicit line switches their VPN off.
4. Port choice against scanners, and why it is not protection
A port other than 51820 or 1194 lowers the background noise. Mass scanners work through lists of known ports, and a VPN on 51820 is in every one of those lists. On an unusual port the server falls out of those runs, and that is measurably less traffic.
Against a targeted attack the port choice does nothing. The port sits in every client file right next to the address. Whoever has your profile has your port. And a volumetric attack does not care about ports at all: it fills the line no matter where the packets are addressed. So treat the port choice as hygiene against scanners, not as a protective measure, and do not change it in the middle of an attack, because that locks out all your clients at once.
If you do change the port, change it in two places: ListenPort in the server file and Endpoint in every client file. With OpenVPN it is port and remote. A migration works step by step if you temporarily allow both ports.
5. Set MTU and fragmentation properly
Fragmentation is doubly unpleasant on a VPN. First, only the first piece of a fragmented packet carries the UDP header and therefore the port number, none of the later ones do. A filter has to reassemble the pieces before it can decide where they belong at all, and that costs memory and time. Second, a flood of fragments is therefore an attack pattern of its own: the attacker sends first pieces whose remainder never arrives and fills the reassembly queue.
The relevant values can be read out and bounded:
sysctl net.ipv4.ipfrag_high_thresh net.ipv4.ipfrag_low_thresh net.ipv4.ipfrag_time
nstat -az | grep -i reasm
The right answer is not to manage fragments better, though, but to stop them from occurring. With WireGuard, wg-quick handles that by itself: it subtracts 80 bytes from the detected path MTU and lands at 1420 on a normal 1500 byte path. WireGuard also sets the Don't Fragment bit, so it never produces fragments itself. With OpenVPN 2.6 the default is mssfix 1492 mtu at a tun-mtu of 1500, whereas before it was the absolute mssfix 1450. That default is dropped automatically as soon as you set tun-mtu to anything other than 1500, and that is exactly where many migrations go wrong.
tun-mtu 1500
mssfix 1400 mtu
fragment 0
The quick cross check for unclear symptoms stays the same as always: MTU = 1280 on the client side. That is the smallest MTU IPv6 guarantees and works practically everywhere. If everything runs cleanly with it, the MTU was the problem.
6. Actually remove departed users
The most effective free measure against a handshake flood is a tidy user list, because only somebody with a profile can produce such a flood at all. With WireGuard you remove the peer while the service runs, without dropping the other connections:
wg set wg0 peer PUBLIC_KEY_OF_THE_PEER remove
wg syncconf wg0 <(wg-quick strip wg0)
Remember to delete the [Peer] block from /etc/wireguard/wg0.conf as well. A change made with wg set lives only in memory and is gone after the next restart. With OpenVPN you revoke the certificate and make sure the server actually reads the revocation list:
./easyrsa revoke username
./easyrsa gen-crl
cp pki/crl.pem /etc/openvpn/server/crl.pem
systemctl reload openvpn-server@server
For that to work, crl-verify /etc/openvpn/server/crl.pem has to be in the server configuration. Without that line the revocation has no effect, and that is one of the most common silent mistakes there is. Also keep in mind that a revocation list has an expiry date: once it has expired, OpenVPN refuses every connection, including the valid ones.
7. Keep a second route onto the server
A VPN server whose administration is reachable only through the VPN is unreachable during an attack. That sounds obvious and still gets built that way constantly. Keep at least one of the following routes open: SSH on a second address that is not publicly known, SSH restricted to your own fixed address, or a console that does not hang off the network stack of the system.
On KVM root servers and dedicated servers from KernelHost that is the VNC console in the customer area. It works independently of the guest system's network, so neither a full line nor a broken firewall rule can block it. How to secure SSH access itself without locking yourself out is in Secure SSH with key login, and the matching rescue path for the firewall is in Set up the UFW firewall.
8. Collect measurements while everything is normal
The most important step is the one almost nobody takes in advance: establish a baseline while nothing is happening. Without a normal value you cannot say after an incident whether 40,000 packets per second was a lot or simply Monday morning. With apt-get install -y vnstat sysstat the measurement runs permanently without you having to think about it.
For a VPN server four quantities make the right baseline: packets per second at the network card, bits per second in both directions, the number of peers with a handshake within the last three minutes, and the number of dropped packets. How to turn that into running monitoring that actually alerts you is in Set up monitoring for a single server. During an incident five commands are enough:
sar -n DEV 1 10
ip -s link show eth0
nstat -az | grep -E 'Udp|Reasm'
wg show wg0 latest-handshakes
tcpdump -ni eth0 udp port 51820 -c 200 -q
With tcpdump the rule is: always bound it with -c. A capture under full load puts extra strain on an already overloaded server, and the first two hundred packets already tell you everything you need to know.
Where these measures stop
Now the part that no configuration file can solve. All of the measures so far run on your server, which is at the end of the line. A filter rule decides about a packet that has already travelled across the cable. You can drop it, but you cannot un-send it.
Do the math once. A typical server sits on 1 Gbit/s, which is 125 megabytes per second, and the line is full as soon as somebody sends more. Attacks against projects of that size usually run between 5 and 50 Gbit/s, so five to fifty times your line. Whether your meter rule behind it is well chosen no longer matters then, because your users' packets do not get through in the first place.
The second quantity is the packet rate, and on a VPN it bites earlier than on other services. A 1 Gbit/s line fits roughly 1.49 million packets per second at 64 bytes each. Depending on CPU and network card a normal server kernel processes a few hundred thousand of those, and on a VPN every one of those packets additionally costs at least one cryptographic check before it may be dropped. Operators experience this as: the graph was not even high, and still everything was gone.
And the third point is the one most often overlooked on a VPN: an attacker does not follow your protocol. They send amplification traffic and floods at your address regardless of which port is open there. Your server drops those packets correctly, but they already occupied your line, and your VPN is gone without a single packet ever reaching the service. For a sense of the magnitudes that actually occur: on KernelHost servers a combined attack against a Minecraft server and an OpenVPN service (ports 25565 TCP and 1194 UDP) with more than sixteen distinct main attack patterns, more than 4 million packets per second and more than 8.6 Gbit/s was filtered. Both services stayed reachable throughout. There is no local setting for that. Volumetric attacks have to end in the network in front of the server.
What KernelHost puts against it
The always-on protection included with every server
DDoS protection at KernelHost is built in two stages and is permanently active, without you having to enable, order or configure anything:
- Stage 1: 17 Tbps of mitigation capacity in the global scrubbing network. Volumetric attacks are cleaned close to their source, before they reach the data center.
- Stage 2: Arbor real-time filtering with 3.2 Tbps in Frankfurt am Main. Directly in front of the server, protocol specific patterns are recognized and dropped, packet by packet. For a VPN that means, concretely: UDP floods against 51820 and 1194 as well as SYN floods against an OpenVPN service on TCP end here and not on your network card.
Three properties are decisive. The protection runs permanently and does not have to react to an attack first, so there are no minutes at the beginning during which the VPN is gone. No null routing is used: your IP address stays in the network, only the harmful packets are dropped. And, particularly important for a VPN: the filtering happens in the network in front of the server, not over some additional path that you would first have to build or migrate your clients onto. On your side, the VPN tunnel stays exactly the one your clients build anyway, and the profile files stay unchanged. The location is Frankfurt am Main. OpenVPN and WireGuard are explicitly covered, as is general TCP and UDP traffic on arbitrary ports.
Advanced DDoS Protection for permanently targeted VPN servers
Some access points are attacked not occasionally but deliberately and over weeks, and on a company VPN every hour of downtime costs real working time. For that there is Advanced DDoS Protection from €50.00 per month, PrePaid, with no minimum term and no setup fee. The difference is not more capacity, it is control:
- A dedicated protected IP from the Frankfurt core, onto which your server is switched inside our own network. Nothing needs rebuilding on your side, you simply enter the new address in the endpoint of your profiles.
- Protection rules per port and protocol that you manage yourself in the customer area: you allow exactly 51820 UDP or 1194 UDP and close everything else, without writing a ticket for it.
- Changes take effect in real time, so you can adjust during a running attack, for example by tightening the permitted packet rate per source address.
- A profile that matches the application. There are suitable profiles for OpenVPN and WireGuard as well as for custom or modified protocols on arbitrary TCP or UDP ports.
Advanced DDoS Protection is aimed at KernelHost customers, because the filtering is part of the network and not an add-on that could be installed on somebody else's server. If you currently run your VPN server elsewhere, you do not get the protection retrofitted, you get it by moving to KernelHost. With a VPN that move is comparatively easy: there is no database and no accumulated state, essentially just a configuration file and a set of keys.
The two stages side by side
| Feature | Included always-on DDoS protection | Advanced DDoS Protection |
|---|---|---|
| Price | included in every server package, no surcharge | from €50.00 per month, PrePaid |
| Filtering capacity | 17 Tbps global scrubbing plus Arbor real-time filtering with 3.2 Tbps in Frankfurt am Main | the same two-stage filtering |
| IP address | the IP address of your server | an additional dedicated protected IP |
| Rule set | automatic profiles, no configuration needed | your own rules per port and protocol in the customer area |
| Changes | applied automatically | take effect in real time, including during an attack |
| Profile for WireGuard and OpenVPN | automatic profiles for UDP and TCP services | a dedicated rule set for 51820 UDP, 1194 UDP and TCP operation |
| Null routing | no | no |
| Change to the client profiles | none | the new endpoint address, once |
| Term | tied to the server package | PrePaid, no minimum term, no notice period, no setup fee |
For most VPN projects the included always-on protection together with a clean configuration is enough. Advanced DDoS Protection is the answer to somebody taking it personally. If you would rather have a ready-configured access point than a self-built installation, there is the Dedicated Private VPN Server from €4.99 per month, with a dedicated IPv4 address plus a /64 IPv6 subnet, root access and the same included always-on protection. Which route suits whom is compared in Own VPN server or VPN service.
Common mistakes and how to fix them
The VPN does not come back by itself after the attack: that is not a fault, it is the intended behavior of WireGuard. The client attempts a handshake for 90 seconds and then gives up until traffic appears from the inside again. Set PersistentKeepalive = 25 in the client file, then traffic appears continuously and the client keeps trying. On phones that line is mandatory anyway, because mobile carrier NAT often forgets a UDP mapping after 30 to 60 seconds.
The rate limit hits your own users: typical for an office, a shared flat or a mobile carrier network where many devices share one public address. To the rule that looks like a single address with a suspicious number of handshakes. Check the counter with nft list meter inet vpn wg_hs: if it rises while no attack is running, the value is too low. Raise it step by step.
The VPN is gone after setting notrack: without connection tracking the rule ct state established,related accept no longer applies to those packets. Every VPN port then needs an explicit line in the rule set, exactly as shown above. Anybody who only sets notrack and changes nothing else locks their users out.
The handshake works but nothing behind it is reachable: that is almost never an attack, it is missing forwarding. Check sysctl net.ipv4.ip_forward (must be 1) and, if ufw is running, its own forwarding rule. The full troubleshooting for this is in Set up a WireGuard VPN server.
The log is full of TLS errors and the server is slow anyway: check your own log level first. From verb 4 onwards OpenVPN writes a line per dropped packet, and under a flood the writing creates more load than the attack. Set verb 3 and mute 20.
Revoking a certificate has no effect: in nine out of ten cases crl-verify is missing from the server configuration, or the revocation list has expired and OpenVPN then rejects every connection. Check both with openssl crl -in /etc/openvpn/server/crl.pem -noout -nextupdate.
Your previous provider blocked the IP address: that is null routing. The provider is protecting its own network with it, and for you the result is identical to a successful attack, usually for hours afterwards. On a VPN that weighs especially heavily, because your users cannot switch to a replacement address while the old one is in their profiles. When in doubt, ask whether traffic is filtered or null routed. The answer says more about your availability than any hardware specification. The fundamentals are in How to protect your server from DDoS attacks.
tcpdump shows nothing unusual: if the traffic is already filtered in the network in front of the server, then nothing arrives on the server, as expected. That is the normal case when filtering works. The reverse also holds: if the line is saturated, you may not even reach the SSH session you wanted to measure from. Use the VNC console in the customer area in that case.
In short
- The IP address of a VPN server has to be publicly reachable and sits in every client file. It can neither be hidden behind a CDN nor changed without touching every device. That is why filtering in the network in front of the server is the only solid answer to a volumetric attack on a VPN.
- A WireGuard server needs exactly one open port, 51820 UDP by default, and knows four message types on it. A pure dial-in server receives only type 1 (handshake, 148 bytes) and type 4 (data).
- WireGuard checks the mac1 field first, which is derived from the server's public key. Packets without a valid mac1 cost a single hash. Only somebody holding a client profile can produce a genuinely expensive handshake flood.
- The cookie reply under load binds the handshake to the real source address and makes spoofed handshake floods ineffective. It does not prevent the packets from arriving and filling the line.
- WireGuard limits handshakes to a fixed 20 per second per IPv4 address, or per IPv6
/64. A real client needs one handshake every 120 seconds. - On OpenVPN,
tls-cryptis the most effective free switch: it drops unauthorized control packets ahead of the expensive TLS handshake, makes the service invisible to scanners and prevents abuse as a reflector. - OpenVPN over TCP suffers twice under attack: SYN floods create kernel state, and TCP inside TCP collapses disproportionately under packet loss. Use UDP wherever you can.
- On the server, what works is a rate limit per source address that hits only the handshake packets and lets the data packets through unthrottled. In nftables that is done through the message type in the first byte of the UDP payload.
- A port other than 51820 lowers the background noise from mass scanners and is not protection. The port sits in every client file, and a volumetric attack does not follow ports anyway.
- Once the line is full, no rule on the server helps any more. At KernelHost a two-stage always-on protection filters with no surcharge and no null routing: 17 Tbps of mitigation capacity in the global scrubbing network and Arbor real-time filtering with 3.2 Tbps in Frankfurt am Main.
Frequently asked questions
Why can a VPN server's IP address not be hidden?
Which port and which protocol does a VPN server need?
What does WireGuard do on its own against a handshake flood?
What does the WireGuard cookie reply achieve, and what does it not?
Why does a WireGuard server not show an attack in its logs?
Why does tls-crypt help OpenVPN against a flood?
Is OpenVPN over UDP or over TCP better against attacks?
Does moving WireGuard off port 51820 help?
Can I defend my VPN against a DDoS attack with nftables?
Why should I turn off conntrack for the VPN port?
At what attack size can my VPN server no longer cope alone?
Does my VPN server at KernelHost go offline during an attack?
Does DDoS protection cost extra, and when do I need the Advanced tier?
2026 KernelHost GmbH. All rights reserved. This guide is protected by copyright. Republishing it on other websites, in whole, in part or in edited form, is not permitted without our written consent. Quoting with a source credit and a link is expressly welcome.

