Hosting a website behind CG-NAT: WireGuard on a VPS

(aka It's Always SEPolicy)

11 April 2023 - Namkhai B.

Due to the peculiar layout of my homelab, all my devices are either in the same network or connected via TailScale (laptop and phone). Unfortunately this would mean nobody can access [insert service here]. And, in the event I wanted to access [insert service here] from another computer, I wouldn't be able to.

As Artemis (my SuperMicro hypervisor) is behind a CG-NAT in a residential area, I can't open ports 80/443 nor any others I'd need.

Option 1 would be to use Cloudflare tunnels, and it's something I looked into but unfortunately they require you to give them DNS control over your domain, and I'm pretty happy with my setup at Hetzner.

Option 2 is to rent a VPS, setup a WireGuard server there, connect the www VM to the VPS and route all traffic through the WireGuard interface. This is what I ended up doing, but as I already have a server with Hetzner, that's what I used instead of a VPS.

Creating the WireGuard server

On the host:

[Interface]
Address = 10.0.2.1/24
ListenPort = 51820
PrivateKey = HOST_PRIVATE_KEY

[Peer]
PublicKey = PEER_PUBLIC_KEY
AllowedIPs = 10.0.2.2/32

On the client:

[Interface]
Address = 10.0.2.2/32

# Load privatekey from file
PostUp = wg set %i private-key /etc/wireguard/privatekey

[Peer]
PublicKey = HOST_PUBLICKEY
Endpoint = HOST_IP:51820
AllowedIPs = 10.0.2.0/24
PersistentKeepalive = 25

But, there's one slight problem with this setup.

The Issue

The VPS (or in this case, my server with Hetzner) won't see the client (aka myles on Artemis) unless myles sends some traffic to the host. This is something easy, just add PostUp = ping -c1 10.0.2.1...

Or so you'd think.

See, this is the solution. But myles is running Rocky Linux, which has... SEPolicy.

And if there's One Thing I've learnt while building Android, it's that it's Always SEPolicy.

If we use plain ol' wg-quick to start the interface, things work fine.

But the problems begin when we use the wg-quick@ SystemD service, because now sepolicy kicks in...

systemctl status wg-quick@lab0 will fail to start:

Apr 12 20:25:12 myles.lab.e79.it wg-quick[89902]: /usr/bin/wg-quick: line 295: /usr/bin/ping: Permission denie

and /var/log/audit/audit.log will show this:

type=AVC msg=audit(1681332529.940:43): avc:  denied  { execute_no_trans } for  pid=1383 comm="wg-quick" path="/usr/bin/ping" dev="dm-0" ino=4332526 scontext=system_u:system_r:wireguard_t:s0 tcontext=system_u:object_r:ping_exec_t:s0 tclass=file permissive=0

After spending an entire afternoon debugging sepolicy, asking ChatGPT and getting an incorrect answer (no surprise there...), I got to...

The Solution

This is my custom sepolicy for WireGuard:

Enable this boolean:

$ sudo semanage boolean -m --on domain_can_mmap_files

Save this to my-wireguard.te

module wireguard-ping 1.0;

require {
        type ping_exec_t;
        type wireguard_t;
        class file { execute_no_trans map unlink };
        class process setcap;
        class icmp_socket { create getopt setopt read write };
        class rawip_socket create;
        class udp_socket { create connect getattr };
}

#============= wireguard_t ==============

allow wireguard_t ping_exec_t:file { execute_no_trans map };
allow wireguard_t self:process setcap;
allow wireguard_t self:icmp_socket { create getopt setopt read write };
allow wireguard_t self:rawip_socket create;
allow wireguard_t self:udp_socket { create connect getattr };

Build it, and load it:

$ checkmodule -M -m -o wireguard-ping.mod wireguard-ping.te
$ semodule_package -o wireguard-ping.pp -m wireguard-ping.mod
$ sudo semodule -i wireguard-ping.pp

And with that, we are golden!

On the next post I'll (hopefully) share my HAProxy setup on the Hetzner server to split a single IPv4 address into two different hosts, a FreeBSD jail for most stuff and myles for anything on e79.it

~f9