Skip to content

Instantly share code, notes, and snippets.

@qdm12
Last active October 29, 2024 23:50
Show Gist options
  • Save qdm12/4e0e4f9d1a34db9cf63ebb0997827d0d to your computer and use it in GitHub Desktop.
Save qdm12/4e0e4f9d1a34db9cf63ebb0997827d0d to your computer and use it in GitHub Desktop.
Wireguard and iptables restrictions for multiple users

Wireguard and iptables restrictions for multiple users

If you don't know what Wireguard is, well, you should. It's fast, easy to setup and highly configurable. We will configure Wireguard for multiple users with various restrictions using iptables.

Assumptions

This should fit most setups (not mine though 😉)

  • LAN network: 192.168.1.0/24 (192.168.1.1 => 192.168.1.254)
  • LAN DNS server address: 192.168.1.1
  • Wireguard is installed (kernel and tools) on a Linux host (it should also work on other platforms though).
  • The Linux host address: 192.168.1.10
  • The Linux host main interface: enp4s0 (find it with ip a)

Initial server setup

  1. Ensure your iptables firewall has its FORWARD table policy set to DROP:

    iptables -P FORWARD DROP
  2. Generate a Wireguard private key

    echo "Private key: $(wg genkey)"
    Private key: OM5BUrGVAswOm/r8asLtdUgJB8rrXflD6TVFL5aGAHk=
    
  3. Create a file /etc/wireguard/wg0.conf (or any other name than wg0) with content:

    [Interface]
    Address = 10.0.0.1/24
    ListenPort = 51820
    PrivateKey = OM5BUrGVAswOm/r8asLtdUgJB8rrXflD6TVFL5aGAHk=
    PostUp = /etc/wireguard/postup.sh
    PostDown = /etc/wireguard/postdown.sh
    

    Don't forget to replace the PrivateKey value with the one you generated

  4. Create a file /etc/wireguard/postup.sh with content:

    WIREGUARD_INTERFACE=wg0
    WIREGUARD_LAN=10.0.0.0/24
    MASQUERADE_INTERFACE=enp4s0
    
    iptables -t nat -I POSTROUTING -o $MASQUERADE_INTERFACE -j MASQUERADE -s $WIREGUARD_LAN
    
    # Add a WIREGUARD_wg0 chain to the FORWARD chain
    CHAIN_NAME="WIREGUARD_$WIREGUARD_INTERFACE"
    iptables -N $CHAIN_NAME
    iptables -A FORWARD -j $CHAIN_NAME
    
    # Accept related or established traffic
    iptables -A $CHAIN_NAME -o $WIREGUARD_INTERFACE -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
    
    # Accept traffic from any Wireguard IP address connected to the Wireguard server
    iptables -A $CHAIN_NAME -s $WIREGUARD_LAN -i $WIREGUARD_INTERFACE -j ACCEPT
    
    # Drop everything else coming through the Wireguard interface
    iptables -A $CHAIN_NAME -i $WIREGUARD_INTERFACE -j DROP
    
    # Return to FORWARD chain
    iptables -A $CHAIN_NAME -j RETURN
  5. Create a file /etc/wireguard/postdown.sh with content:

    WIREGUARD_INTERFACE=wg0
    WIREGUARD_LAN=10.0.0.0/24
    MASQUERADE_INTERFACE=enp4s0
    CHAIN_NAME="WIREGUARD_$WIREGUARD_INTERFACE"
    
    iptables -t nat -D POSTROUTING -o $MASQUERADE_INTERFACE -j MASQUERADE -s $WIREGUARD_LAN
    
    # Remove and delete the WIREGUARD_wg0 chain
    iptables -D FORWARD -j $CHAIN_NAME
    iptables -F $CHAIN_NAME
    iptables -X $CHAIN_NAME
  6. Start the Wireguard server (without any client configured yet):

    wg-quick up wg0

First admin client

Let's setup a client with full access to Internet and your LAN through Wireguard.

  1. Install Wireguard on your client device

  2. On your client device, create a configuration file client.conf with content:

    [Interface]
    Address = 10.0.0.2/32
    DNS = 192.168.1.1
    PrivateKey = YOUR_CLIENT_PRIVATE_KEY
    
    [Peer]
    AllowedIPs = 0.0.0.0/0, ::/0
    Endpoint = 192.168.1.10:51820
    PersistentKeepalive = 25
    PublicKey = YOUR_SERVER_PUBLIC_KEY
    
  3. Replace in the configuration above YOUR_SERVER_PUBLIC_KEY with the key shown using wg show wg0 public-key on your server. For example: p6aGalk69yCM8vNhbvC5mEH/HhJr1c8f55UaeJSChX0=.

  4. Generate keys for your client:

    priv=`wg genkey` && printf "Private key: $priv\nPublic key: `echo "$priv" | wg pubkey`\n" && unset -v priv
    Private key: 2L4L8YwusK4Ot4jVoo/1wwQfLAeRM6kJ/WWxzfnWKm4=
    Public key: GmVyaj+K36xEk7ko/8jijMB9XX9dFgi4mJxsAEFMHmA=
    
  5. Replace in client.conf YOUR_CLIENT_PRIVATE_KEY with the private key just generated above.

  6. Use the public key shown above to add the following block to /etc/wireguard/wg0.conf on your server:

    [Peer]
    # Your first admin client
    PublicKey = GmVyaj+K36xEk7ko/8jijMB9XX9dFgi4mJxsAEFMHmA=
    AllowedIPs = 10.0.0.2/32
    
  7. On your server, restart Wireguard: wg-quick down wg0 && wg-quick up wg0.

  8. You should now be able to connect to the Wireguard server from your client. You can check on the server with wg and it should show a latest handshake line.

LAN only user

Let's add a user who should only have access to the LAN.

  1. Repeat steps 1 to 5 from the First admin client section above.

  2. Use the public key shown in step 4 to add the following block to /etc/wireguard/wg0.conf on your server:

    [Peer]
    # LAN only user
    PublicKey = 7GneIV/Od7WEKfTpIXr+rTzPf3okaQTBwsfBs5Eqiyw=
    AllowedIPs = 10.0.0.3/32
    
  3. Shutdown Wireguard: wg-quick down wg0

  4. Modify /etc/wireguard/postup.sh:

    • Limit full access to our first admin client 10.0.0.2 only by changing:

      iptables -A $CHAIN_NAME -s 10.0.0.0/24 -i $WIREGUARD_INTERFACE -j ACCEPT

      to

      iptables -A $CHAIN_NAME -s 10.0.0.2 -i $WIREGUARD_INTERFACE -j ACCEPT
    • Add a new line to allow our new user to LAN access only

      iptables -A $CHAIN_NAME -s 10.0.0.3 -i $WIREGUARD_INTERFACE -d 192.168.1.0/24 -j ACCEPT
  5. Start Wireguard: wg-quick up wg0

  6. Note that the client should set its AllowedIPs = 0.0.0.0/0, ::/0 to AllowedIPs = 192.168.1.0/24 so it tunnels only to when trying to reach an address on the LAN.

Restrict the user to a port

Let's re-use the user we previously created. Let's only allow him to access port 445 (Samba) on a server 192.168.1.20 for example.

  1. Shutdown Wireguard: wg-quick down wg0

  2. Modify

    iptables -A $CHAIN_NAME -s 10.0.0.3 -i $WIREGUARD_INTERFACE -d 192.168.1.0/24 -j ACCEPT

    to

    iptables -A $CHAIN_NAME -s 10.0.0.3 -i $WIREGUARD_INTERFACE -d 192.168.10.20 -p tcp --dport 445 -j ACCEPT
  3. Start Wireguard: wg-quick up wg0

Restrict the user to limited Web browsing only

That's useful for our friends who want to stream restricted content on Netflix without allowing them on your LAN 😉

  1. Shutdown Wireguard: wg-quick down wg0

  2. Modify

    iptables -A $CHAIN_NAME -s 10.0.0.3 -i $WIREGUARD_INTERFACE -d 192.168.10.20 -p tcp --dport 445 -j ACCEPT

    to

    # DNS
    iptables -A $CHAIN_NAME -s 10.0.0.3 -i $WIREGUARD_INTERFACE -d 192.168.1.1 -p udp --dport 53 -j ACCEPT
    # Drop traffic to your any private IP address
    iptables -A $CHAIN_NAME -s 10.0.0.3 -i $WIREGUARD_INTERFACE -d 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 -j DROP
    # Accept outgoing connections to HTTP(S) ports to any IP address (public because of rule above)
    iptables -A $CHAIN_NAME -s 10.0.0.3 -i $WIREGUARD_INTERFACE -d 0.0.0.0/0 -p tcp -m multiport --dports 80,443 -j ACCEPT
  3. Start Wireguard: wg-quick up wg0

  4. Don't forget to set back the client to AllowedIPs = 0.0.0.0/0, ::/0

More rules

Feel free to comment, I'll try my best to make you a rule 😉

Enjoy!

@ainformatico
Copy link

ainformatico commented Feb 21, 2024

Found this gist and thanks to all the comments above, found the inspiration to make my config work. Thanks for that.
Hope my config helps those seeking for something similar.

What I want

  • Laptop(10.11.0.100): trusted device with full access to the LAN network and the Internet.

  • Phone(10.11.0.3): guest device with full Internet access, but no LAN access, except to use the DNS running on the LAN (pihole) and a HTTP server also running on the LAN.

  • 192.168.0.10: Just runs Wireguard server

  • 192.168.0.53: Just runs Pihole server

  • 192.168.0.30: Just runs HTTP server

How I achieved it

🚨 IMPORTANT: The order of the rules in the file matter! If you move things around it will break.
If you know what you are doing, then you can take advantage of -I vs -A and move it as you wish.

WIREGUARD_INTERFACE=wg0
WIREGUARD_LAN=10.11.0.0/24
MASQUERADE_INTERFACE=eth0

iptables -t nat -I POSTROUTING -o $MASQUERADE_INTERFACE -j MASQUERADE -s $WIREGUARD_LAN

# Add a WIREGUARD_wg0 chain to the FORWARD chain
CHAIN_NAME="WIREGUARD_$WIREGUARD_INTERFACE"
iptables -N $CHAIN_NAME
iptables -A FORWARD -j $CHAIN_NAME

# Accept related or established traffic
iptables -A $CHAIN_NAME -o $WIREGUARD_INTERFACE -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

##################################
# TRUSTED CLIENT (10.11.0.100)
## Accept and forward _all traffic_ from the Wireguard IP address of the trusted client. No restrictions.
iptables -A $CHAIN_NAME -s 10.11.0.100 -i $WIREGUARD_INTERFACE -j ACCEPT
##################################

##################################
# GUEST (10.11.0.3)
## Allow guest to use a DNS that runs in your LAN(192.168.0.53) e.g. Pihole.
### This requires the guest to set the DNS server in the Wireguard config: `DNS = 192.168.0.53`.
### Remove if not necessary.
iptables -A $CHAIN_NAME -s 10.11.0.3 -i $WIREGUARD_INTERFACE -p udp --dport 53 -d 192.168.0.53 -j ACCEPT

## Allow guest to access a HTTP server running in a LAN host(192.168.0.30).
### Remove if not necessary.
iptables -A $CHAIN_NAME -s 10.11.0.3 -i $WIREGUARD_INTERFACE -p tcp --match multiport --dport 80,443 -d 192.168.0.30 -j ACCEPT

## Prevent guest from accessing **this** host. The one running the Wireguard server(192.168.0.10).
### Depending on your needs and setup, you might need to also create a rule for the Wireguard IP of the server too. Check first.
### IMPORTANT: this is an INPUT rule as we don't want "INPUT" to this host. Other rules stay as FORWARD because they are for other hosts.
### IMPORTANT: If used, add to the postdown.sh: iptables -D INPUT -s 10.11.0.3 -i $WIREGUARD_INTERFACE -p tcp -d 192.168.0.10 -j DROP
### Remove if not necessary.
iptables -A INPUT -s 10.11.0.3 -i $WIREGUARD_INTERFACE -p tcp -d 192.168.0.10 -j DROP

## Drop traffic to any LAN or Wireguard IP address.
iptables -A $CHAIN_NAME -s 10.11.0.3 -i $WIREGUARD_INTERFACE -d 10.11.0.0/24,192.168.0.0/24 -j DROP

## Accept outgoing connections to the Internet.
iptables -A $CHAIN_NAME -s 10.11.0.3 -i $WIREGUARD_INTERFACE -d 0.0.0.0/0 -j ACCEPT
##################################

# Drop everything else coming through the Wireguard interface
iptables -A $CHAIN_NAME -i $WIREGUARD_INTERFACE -j DROP

# Return to FORWARD chain
iptables -A $CHAIN_NAME -j RETURN

@zilexa
Copy link

zilexa commented Feb 21, 2024

@ainformatico thanks a lot. This is almost what I was looking for, for years.

In my case, I only have one LAN homeserver that runs multiple services including DNS.
So only 192.168.0.53, and I only want to allow clients to access the DNS service and a few other services, based on port.
No other access to the server should be allowed.

Do you perhaps now how to do that?

Its Basically your setup, but servers 192.168.0.53 and 192.168.0.30 are in my case just 1 server.

Also I noticed suddenly in your comments you say your Wireguard server is 192.168.0.10, not 192.168.0.53. That's quite confusing..

@ainformatico
Copy link

@zilexa, I have different servers (different virtual machines)

  • 192.168.0.10: Just runs Wireguard server
  • 192.168.0.53: Just runs Pihole server
  • 192.168.0.30: Just runs HTTP server

With different clients:

  • 10.11.0.100: Laptop
  • 10.11.0.3: Phone

I have updated my comment above to reflect more info.

Now, regarding your comment. I will assume that your Wireguard server is also your DNS and other services servers.
In that case, you just need to update the IPs and firewall chain accordingly. As it is the same host, you can't use FORWARD anymore. You have to go to the INPUT chain in iptables. The reason is that you are not really forwarding anything, it is the same host.

Assuming your server running all services is 192.168.0.60, and the client is 10.11.0.3 yo can:

## Allow DNS
iptables -A INPUT -s 10.11.0.3 -i $WIREGUARD_INTERFACE -p udp --dport 53 -d 192.168.0.60 -j ACCEPT

## Allow guest to access different services at different ports (80, 443, 8080, 9000.)
iptables -A INPUT -s 10.11.0.3 -i $WIREGUARD_INTERFACE -p tcp --match multiport --dport 80,443,8080,9000 -d 192.168.0.60 -j ACCEPT

In your postdown.sh you will have to undo those rules:

## Allow DNS
iptables -D INPUT -s 10.11.0.3 -i $WIREGUARD_INTERFACE -p udp --dport 53 -d 192.168.0.60 -j ACCEPT

## Allow guest to access different services at different ports (80, 443, 8080, 9000.)
iptables -D INPUT -s 10.11.0.3 -i $WIREGUARD_INTERFACE -p tcp --match multiport --dport 80,443,8080,9000 -d 192.168.0.60 -j ACCEPT

@DrilealoTatarara
Copy link

Hi, im having problems configuring a tunnel with wireguard between the client, alocated in a AWS clowd instance with public IP 52.71.59.33, and the server, alocated in a pod of Kubernetes. This pod's cluster is virtualized with vSphere in a machine of the WAN network of the company. I'd like you to help me to staclish the tunnel between boths. I dont have much knowledge about iptables and forwarding, etc. Please help me! :(

@krin-san
Copy link

Thank you for sharing this Gist! Unfortunately it didn't really help me on TrueNAS Scale running wg-easy k3s node, but this Gist and an idea of using a custom chain in combination with @ainformatico comment about INPUT chain helped me to find the config which works and which doesn't ruin networking in the whole k3s cluster... yeah, turns out iptables -A FORWARD -j $CHAIN_NAME rule starts handling whole cluster requests.

Just for context:

  • NAS along wg-easy and other apps running on this NAS all have an IP Address 192.168.1.3. That is useful as I allowed some hosts to access only the NAS itself, or only specific app on the NAS;
  • Files below are stored in the same directory with configuration files – I have chosen a HostPath for "WG-Easy Config Storage";
  • wg-easy app has WG_POST_UP set to /etc/wireguard/post-up.sh and WG_POST_DOWN set to /etc/wireguard/post-down.sh;
  • INPUT chain rule handles packets from Wireguard clients to NAS;
  • FORWARD chain rule handles packets from Wireguard clients to Internet;
  • Using a custom chain allows a "safe" removal of those rules in WG_POST_DOWN script without duplicating rules with the only difference being-A and -D arguments.

.env

WIREGUARD_INTERFACE=wg0
WIREGUARD_LAN=10.8.0.0/24
MASQUERADE_INTERFACE=eth0
CHAIN_NAME="WIREGUARD_$WIREGUARD_INTERFACE"

post-up.sh

#!/bin/sh

# Clean-up after a previous unsuccessful node restart
"$(dirname "$0")/post-down.sh" &> /dev/null

source "$(dirname "$0")/.env"

iptables -t nat -I POSTROUTING -s $WIREGUARD_LAN -o $MASQUERADE_INTERFACE -j MASQUERADE

# Add a WIREGUARD_wg0 chain to the INPUT chain
iptables -N $CHAIN_NAME
iptables -A INPUT -i $WIREGUARD_INTERFACE -j $CHAIN_NAME
iptables -A FORWARD -i $WIREGUARD_INTERFACE -j $CHAIN_NAME

# Accept related or established traffic
iptables -A $CHAIN_NAME -o $WIREGUARD_INTERFACE -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

##################################

# Admin
iptables -A $CHAIN_NAME -s 10.8.0.2 -i $WIREGUARD_INTERFACE -j ACCEPT

# NAS only
iptables -A $CHAIN_NAME -s 10.8.0.3 -i $WIREGUARD_INTERFACE -d 192.168.1.3 -j ACCEPT

# NAS only for DNS + SomeApp
iptables -A $CHAIN_NAME -s 10.8.0.4 -i $WIREGUARD_INTERFACE -d 192.168.1.3 -p udp --dport 53 -j ACCEPT
iptables -A $CHAIN_NAME -s 10.8.0.4 -i $WIREGUARD_INTERFACE -d 192.168.1.3 -p tcp --dport 20001 -j ACCEPT

##################################

# Drop everything else coming through the Wireguard interface
iptables -A $CHAIN_NAME -i $WIREGUARD_INTERFACE -j DROP

# Return to INPUT chain
iptables -A $CHAIN_NAME -j RETURN

post-down.sh

#!/bin/sh

source "$(dirname "$0")/.env"

iptables -t nat -D POSTROUTING -s $WIREGUARD_LAN -o $MASQUERADE_INTERFACE -j MASQUERADE

# Remove and delete the WIREGUARD_wg0 chain
iptables -D INPUT -i $WIREGUARD_INTERFACE -j $CHAIN_NAME
iptables -D FORWARD -i $WIREGUARD_INTERFACE -j $CHAIN_NAME
iptables -F $CHAIN_NAME
iptables -X $CHAIN_NAME

If any of these doesn't seem correct, please let me know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment