So the server is running great, the auto-deploy pipeline works, the webhook is properly sandboxed... but something was still nagging me. My VPN endpoint โ€” the one with the random subdomain and non-default port โ€” is technically exposed to the internet. And even though it requires a valid certificate to connect, what about a plain old DoS attack? Someone could just flood that UDP port and saturate my home internet connection.

Time to harden the VPN.

Part 1: Rate Limiting with UFW

The idea is simple: if any single IP sends more than 20 new connections in 10 seconds, drop everything from that IP. Normal VPN usage will never hit that limit โ€” my phone connects once and stays connected. But a flood of packets? Blocked.

Since I'm already using UFW as my firewall, the right way to add custom rate limiting is through its before.rules file. These rules get loaded before UFW's own rules, so they're processed first:

sudo nano /etc/ufw/before.rules

Added right after the # End required lines comment:

# Rate limit OpenVPN
-A ufw-before-input -p udp --dport [VPN_PORT] -m conntrack --ctstate NEW -m recent --set --name vpn
-A ufw-before-input -p udp --dport [VPN_PORT] -m conntrack --ctstate NEW -m recent --update --seconds 10 --hitcount 20 --name vpn -j DROP

Then sudo ufw reload and it's active. UFW manages the main firewall, and the rate limiter sits on top of it. Any IP that sends more than 20 new connections in 10 seconds gets dropped. Normal VPN usage never hits that โ€” my phone connects once and stays connected.

Part 2: The tls-auth Investigation

With the rate limiter in place, I wanted to add another layer: tls-auth. The idea is that the server uses an HMAC key to authenticate packets before processing them. If a packet doesn't have the right HMAC signature, the server silently drops it โ€” doesn't respond, doesn't acknowledge, nothing. To a port scanner, the server looks like it doesn't exist.

I generated the key and added it to the server config:

sudo openvpn --genkey secret /etc/openvpn/server/ta.key

# Added to server.conf:
tls-auth /etc/openvpn/server/ta.key 0

Restarted OpenVPN, generated a new client file (mose-v2.ovpn)... and then noticed something interesting. The new client file didn't have tls-auth in it. Instead, it had this:

<tls-crypt-v2>
-----BEGIN OpenVPN tls-crypt-v2 client key-----
...
-----END OpenVPN tls-crypt-v2 client key-----
</tls-crypt-v2>

Wait... tls-crypt-v2?

I didn't configure tls-crypt-v2. Where did it come from? Checked the server config:

grep "tls" /etc/openvpn/server/server.conf

And there it was: tls-crypt-v2 tls-crypt-v2.key. The angristan install script had already set it up when I first installed OpenVPN! I just didn't know it was there.

tls-auth vs tls-crypt-v2

Turns out tls-crypt-v2 is actually the successor to tls-auth and does everything it does, plus more:

  • tls-auth: Authenticates packets with HMAC. Server drops unauthenticated packets silently. All clients share the same key.
  • tls-crypt-v2: Does the same HMAC authentication AND encrypts the entire control channel. Plus, each client gets a unique key, so you can revoke individual clients without affecting the others.

So the protection I was trying to add... was already there. The whole time. My server was already invisible to port scanners, already dropping unauthenticated packets, already encrypting the control channel. The install script had done it for me and I never noticed.

The Cleanup

Since tls-crypt-v2 and tls-auth conflict with each other, I removed the tls-auth line from the server config and deleted the unnecessary key:

sudo rm /etc/openvpn/server/ta.key

And the extra mose-v2 client? Revoked it โ€” the original client already had the tls-crypt-v2 key embedded in it from day one.

Checking My Old Client

Just to be sure, I checked the original .ovpn file:

grep "tls" ~/mose.ovpn

And sure enough:

tls-client
tls-version-min 1.2
tls-cipher TLS-ECDHE-ECDSA-WITH-AES-128-GCM-SHA256
<tls-crypt-v2>
-----BEGIN OpenVPN tls-crypt-v2 client key-----
...
-----END OpenVPN tls-crypt-v2 client key-----
</tls-crypt-v2>

It was there all along. Every client I ever generated had the encryption key. The server was never "naked" โ€” I just didn't realize it was already dressed.

"Sometimes the best security investigation ends with discovering you were already protected."

Current VPN Security Stack

So after all of this, here's what protects the VPN endpoint:

  1. Obscure subdomain โ€” random string, not guessable
  2. Non-default port โ€” not the standard 1194 that scanners check first
  3. tls-crypt-v2 โ€” server is invisible to anything without the right key
  4. UFW rate limiting โ€” 20 connections per 10 seconds max per IP
  5. Certificate authentication โ€” each client needs a valid cert to establish a session
  6. Cloudflare proxy on the web โ€” real home IP hidden from website visitors

Six layers. For a home server running on an old laptop in a basement. I think that's enough ahahah.


Lessons Learned

  • Always check what your install scripts already configured before adding things manually
  • tls-crypt-v2 > tls-auth โ€” if your setup supports it, use it
  • Sometimes the scariest security problems are already solved and you just don't know it yet
  • Use UFW's before.rules for custom iptables rules โ€” keeps everything in one place