Subscribe and receive upto $1000 discount on checkout. Learn more
Subscribe and receive upto $1000 discount on checkout. Learn more
Subscribe and receive upto $1000 discount on checkout. Learn more
Subscribe and receive upto $1000 discount on checkout. Learn more
Design Zero-Trust-Like Access Without Expensive Tools

When “just open a port” quietly becomes a business risk

It usually starts innocently. A small team needs access to an internal dashboard. Someone enables SSH from “anywhere” for convenience. Then a database needs remote access for a vendor. Then a new office opens, and suddenly we have three different ways people “get in.” Nothing breaks immediately, so the shortcuts feel harmless.

But over time, the environment grows. More services appear. More identities exist. More laptops connect from more networks. And the old approach—exposing internal services directly to the internet—turns into a permanent source of risk and operational noise.

For SMEs, the challenge is rarely a lack of intent. It is usually budget, time, and the reality that expensive platforms are not always an option. The good news is we can design access that behaves like a modern, controlled model using standard Ubuntu components and a small number of well-understood building blocks.

In this guide, we will build a practical access design on Ubuntu that keeps internal services private, forces all access through a controlled entry point, and limits what each connected device can reach. We will do it with WireGuard, a host firewall, and careful routing—no expensive tooling required.

Design overview: what we are building and why it works

We are going to create a small “access plane” that sits in front of internal services:

  • One Ubuntu server acts as the controlled entry point (a WireGuard gateway).
  • Internal services stay private (no direct inbound exposure from the internet).
  • Each device gets a unique cryptographic identity (WireGuard keys) and a fixed VPN address.
  • Firewall rules enforce reachability so devices can only access what they are allowed to access.
  • Everything persists across reboots (systemd services, sysctl settings, and firewall rules).

This is not about adding complexity. It is about reducing the number of ways into the environment and making access predictable and auditable.

Prerequisites and assumptions (read this before running commands)

We will assume the following so the commands are safe and consistent:

  • Platform: Ubuntu Server 22.04 LTS or 24.04 LTS.
  • Server role: A dedicated Ubuntu server (VM or bare metal) with:
    • One network interface with internet reachability (public IP or NAT with port-forwarding).
    • Reachability to internal services we want to protect (same LAN/VPC/subnet, or routed connectivity).
  • Privileges: We will run commands as a user with sudo privileges. Where root-owned files are edited, we will use sudo.
  • Network plan:
    • WireGuard VPN subnet: 10.44.0.0/24
    • WireGuard server VPN IP: 10.44.0.1
    • Example internal subnet: we will detect it on the server rather than hard-coding it.
  • Firewall: We will use UFW (which configures nftables/iptables under the hood depending on Ubuntu version). If another firewall system is already in place, we must reconcile rules rather than stacking multiple firewalls.
  • Change control: These steps affect network access. In production, we should schedule a maintenance window and ensure we have console access to the server in case we lock ourselves out.

Step 1: Confirm the server’s network facts before we touch anything

Before we install or configure access controls, we need to know which interface faces the internet, what the server’s default route is, and what internal networks are present. This prevents the most common failure: applying NAT or firewall rules to the wrong interface.

ip -br link
ip -4 -br addr
ip route show default
ip route

We have now listed interfaces, IPv4 addresses, and routing. From the default route line, we can see which interface is used for outbound internet traffic. We will capture that interface name into a variable so later commands remain copy/paste safe.

Step 2: Install WireGuard and baseline firewall tooling

Now we will install WireGuard and UFW. WireGuard provides the encrypted tunnel and device identity. UFW provides a manageable way to enforce inbound and forwarding policy on the gateway.

sudo apt-get update
sudo apt-get install -y wireguard ufw

WireGuard and UFW are now installed. Nothing is exposed yet, and no tunnels exist yet. Next we will enable IP forwarding so the gateway can route traffic from VPN clients to internal networks in a controlled way.

Step 3: Enable IP forwarding (persistently) for routed access

We are about to allow the server to forward packets between interfaces. Without this, VPN clients can connect to the gateway but cannot reach internal services behind it. We will enable forwarding persistently using sysctl configuration.

sudo tee /etc/sysctl.d/99-wg-forwarding.conf >/dev/null <<'EOF'
net.ipv4.ip_forward=1
EOF

sudo sysctl --system

We have created a dedicated sysctl file and applied it immediately. This change persists across reboots. Now we will verify that forwarding is actually enabled.

sysctl net.ipv4.ip_forward

If the output shows net.ipv4.ip_forward = 1, the gateway is ready to route traffic once the tunnel and firewall rules are in place.

Step 4: Generate WireGuard keys for the server

WireGuard uses public/private key pairs for identity. We will generate the server’s keys and store them with strict permissions. This is foundational: if key material is exposed, access control is compromised.

sudo install -d -m 0700 /etc/wireguard
sudo sh -c 'umask 077; wg genkey | tee /etc/wireguard/server.key | wg pubkey > /etc/wireguard/server.pub'
sudo chmod 600 /etc/wireguard/server.key /etc/wireguard/server.pub

We have created /etc/wireguard/server.key and /etc/wireguard/server.pub with root-only access. Next we will create the WireGuard interface configuration.

Step 5: Create the WireGuard gateway configuration

We are about to define the VPN interface wg0. This includes the VPN address, the UDP port WireGuard listens on, and (later) the list of allowed peers. We will also prepare for controlled routing by adding NAT rules in a way that works with UFW.

First, we will detect the external interface used for the default route and store it in a variable for later use.

EXT_IFACE=$(ip route show default | awk '{print $5; exit}')
echo "$EXT_IFACE"

We now have the external interface name in EXT_IFACE. Next, we will create /etc/wireguard/wg0.conf. We will keep it minimal and explicit.

SERVER_PRIV_KEY=$(sudo cat /etc/wireguard/server.key)

sudo tee /etc/wireguard/wg0.conf >/dev/null <<EOF
[Interface]
Address = 10.44.0.1/24
ListenPort = 51820
PrivateKey = ${SERVER_PRIV_KEY}

# We will add peers below after generating client keys.
EOF

sudo chmod 600 /etc/wireguard/wg0.conf

We have created the WireGuard interface configuration with the server’s VPN address and listening port. No clients can connect yet because we have not defined any peers. Next we will configure the firewall so only the WireGuard port is reachable from the internet, and internal services remain private.

Step 6: Configure UFW for a controlled entry point

We are about to enforce a simple rule: the only inbound service exposed to the internet on this gateway is WireGuard UDP/51820 (and optionally SSH for administration, ideally restricted). Everything else should be denied by default.

First, we will set UFW defaults. This ensures inbound is denied unless explicitly allowed, and forwarding is controlled by our rules.

sudo ufw default deny incoming
sudo ufw default allow outgoing

UFW now denies unsolicited inbound traffic by default. Next, we will allow WireGuard inbound on UDP/51820. This is the only port we need for remote access into the environment.

sudo ufw allow 51820/udp

WireGuard is now permitted through the firewall. If we need SSH for administration, we should restrict it. We will first detect our current SSH port and then allow it in a controlled way. If we already have console access and do not need SSH from the internet, we can skip this.

SSH_PORT=$(ss -lntp | awk '/sshd/ {print $4}' | sed -n 's/.*:([0-9]+)$/1/p' | head -n1)
echo "${SSH_PORT:-22}"

We have attempted to detect the SSH port. If nothing prints, SSH may not be running, or it may be socket-activated differently. We will safely allow port 22 only if we explicitly decide to. In many SME environments, it is better to allow SSH only over the VPN and not from the internet at all.

Option A: SSH only over the VPN (recommended)

We will not open SSH on the public interface. Instead, we will later allow SSH from the VPN subnet to the gateway. This keeps administration behind the same controlled entry point.

Option B: SSH from the internet (only if required)

If we must allow SSH from the internet, we should restrict it to known office IPs. We will first print the server’s public IP (best-effort) so we can confirm we are working on the right host, then we will show a safe pattern for allowing a specific source IP.

curl -fsS https://ifconfig.me || true
hostname -f
ip -4 -br addr

We have printed identifying information. If we proceed with public SSH, we should replace the source IP with a real office IP. Because copy/paste safety matters, we will not include a fake IP in an allow rule. Instead, we will keep SSH closed publicly and allow it over the VPN in the next steps.

Now we will enable UFW. This is the point where firewall policy becomes active, so we should ensure we have a working session and a recovery path.

sudo ufw enable
sudo ufw status verbose

UFW is now active, and we have confirmed the current rules. Next we will add NAT and forwarding rules so VPN clients can reach internal networks through this gateway without exposing those networks to the internet.

Step 7: Add NAT and forwarding rules for VPN-to-internal access

We are about to do two things:

  • NAT (masquerade) so traffic from the VPN subnet can reach internal networks and return correctly in environments where internal routes are not updated.
  • Forwarding policy so only the VPN subnet is allowed to forward through the gateway, and only to destinations we approve.

First, we will capture the external interface again (in case the shell session changed) and confirm it.

EXT_IFACE=$(ip route show default | awk '{print $5; exit}')
echo "$EXT_IFACE"

Now we will add a NAT rule to UFW’s before rules. This is a standard approach on Ubuntu when using UFW with routed VPN traffic.

sudo cp -a /etc/ufw/before.rules /etc/ufw/before.rules.bak

sudo tee /etc/ufw/before.rules >/dev/null <<EOF
#
# rules.before
#

*nat
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -s 10.44.0.0/24 -o ${EXT_IFACE} -j MASQUERADE
COMMIT

*filter
:ufw-before-input - [0:0]
:ufw-before-output - [0:0]
:ufw-before-forward - [0:0]
:ufw-not-local - [0:0]

# Allow all on loopback
-A ufw-before-input -i lo -j ACCEPT
-A ufw-before-output -o lo -j ACCEPT

# Quickly drop invalid packets
-A ufw-before-input -m conntrack --ctstate INVALID -j DROP

# Allow established/related
-A ufw-before-input -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A ufw-before-output -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A ufw-before-forward -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

# Allow ICMP (ping)
-A ufw-before-input -p icmp --icmp-type echo-request -j ACCEPT

# Allow DHCP client
-A ufw-before-input -p udp --sport 67 --dport 68 -j ACCEPT

# Include the rest of UFW's default before rules behavior
-A ufw-not-local -m addrtype --dst-type LOCAL -j RETURN
-A ufw-not-local -m addrtype --dst-type MULTICAST -j RETURN
-A ufw-not-local -m addrtype --dst-type BROADCAST -j RETURN
-A ufw-not-local -m limit --limit 3/min --limit-burst 10 -j ufw-logging-deny
-A ufw-not-local -j DROP

-A ufw-before-input -j ufw-not-local

# End required lines
EOF

We have inserted a NAT masquerade rule for traffic sourced from 10.44.0.0/24 going out via the external interface. We also preserved a minimal, safe baseline for UFW’s before rules. Next, we must ensure UFW is configured to allow forwarding.

We will set UFW’s default forward policy to ACCEPT so our explicit route rules can work. Without this, VPN clients may connect but cannot pass traffic.

sudo cp -a /etc/default/ufw /etc/default/ufw.bak
sudo sed -i 's/^DEFAULT_FORWARD_POLICY=.*/DEFAULT_FORWARD_POLICY="ACCEPT"/' /etc/default/ufw
grep -n '^DEFAULT_FORWARD_POLICY' /etc/default/ufw

We have updated UFW’s forwarding policy and verified the line. Now we will add explicit UFW route rules to control what VPN clients can reach. This is where we keep access intentional.

First, we will detect the server’s internal subnet candidates from routing. In many SME environments, the internal network is the directly connected LAN subnet. We will print routes that look like private IPv4 ranges.

ip route | awk '$1 ~ /^(10.|192.168.|172.(1[6-9]|2[0-9]|3[0-1]).)/ {print}'

We now have visibility into private routes. We will choose one internal subnet to allow. To keep commands copy/paste safe, we will store the internal subnet in a variable by selecting the first private route destination. In production, we should confirm it matches the intended internal network.

INTERNAL_SUBNET=$(ip route | awk '$1 ~ /^(10.|192.168.|172.(1[6-9]|2[0-9]|3[0-1]).)/ {print $1; exit}')
echo "$INTERNAL_SUBNET"

We have selected an internal subnet candidate. If this prints nothing, the server may not have a private route (for example, it is only on a public network). In that case, we must define the internal subnet explicitly based on the environment design before proceeding.

Now we will allow routed traffic from the VPN subnet to the internal subnet. This does not open inbound internet access to internal services; it only allows traffic that arrives via the VPN interface and is forwarded onward.

sudo ufw route allow in on wg0 out on ${EXT_IFACE} from 10.44.0.0/24 to ${INTERNAL_SUBNET}
sudo ufw route allow in on wg0 out on ${EXT_IFACE} from 10.44.0.0/24 to any port 53 proto udp
sudo ufw route allow in on wg0 out on ${EXT_IFACE} from 10.44.0.0/24 to any port 53 proto tcp

We have added route rules. The first rule allows VPN clients to reach the internal subnet. The DNS rules are optional and only relevant if we intend to let VPN clients use external DNS through the gateway; many environments will instead use internal DNS. Next, we will reload UFW so the NAT and forwarding changes take effect.

sudo ufw reload
sudo ufw status verbose

UFW has reloaded, and the updated policy is active. Next we will bring up WireGuard and confirm it is listening.

Step 8: Start WireGuard and verify the gateway is ready

We are about to start the WireGuard interface using systemd. This ensures the VPN comes up on boot and stays managed like any other production service.

sudo systemctl enable --now wg-quick@wg0
sudo systemctl status wg-quick@wg0 --no-pager

WireGuard is now enabled and started. Next we will verify that the interface exists, has the correct IP, and is listening on UDP/51820.

ip -br addr show wg0
sudo wg show
ss -lunp | awk 'NR==1 || /:51820/'

We have confirmed the interface address, WireGuard status, and the listening socket. At this point, the gateway is ready, but no clients can connect until we add peers.

Step 9: Create a client identity and add it as a peer

We are about to create a client key pair and assign it a fixed VPN IP. This is where access becomes traceable: each device has a unique identity and a predictable address. In production, we should create one peer per person or per device, not shared.

We will generate keys for a first client called client1 on the server for simplicity. In stricter environments, we generate client keys on the client device and only copy the public key to the server.

sudo sh -c 'umask 077; wg genkey | tee /etc/wireguard/client1.key | wg pubkey > /etc/wireguard/client1.pub'
sudo chmod 600 /etc/wireguard/client1.key /etc/wireguard/client1.pub
sudo cat /etc/wireguard/client1.pub

We have created the client’s keys and printed the public key. Next we will add this client as a peer in wg0.conf with a fixed VPN IP of 10.44.0.10.

CLIENT1_PUB_KEY=$(sudo cat /etc/wireguard/client1.pub)

sudo tee -a /etc/wireguard/wg0.conf >/dev/null <<EOF

[Peer]
PublicKey = ${CLIENT1_PUB_KEY}
AllowedIPs = 10.44.0.10/32
EOF

We have appended a peer definition. The AllowedIPs line is important: it defines which source IPs this peer is allowed to use inside the VPN. This prevents a client from impersonating another VPN address.

Now we will apply the configuration change without dropping the interface. This is safer than restarting in environments where other peers may already be connected.

sudo wg syncconf wg0 <(sudo wg-quick strip wg0)
sudo wg show

The running WireGuard interface now includes the new peer. Next we will create a client configuration file that can be used on an Ubuntu client (or imported into a WireGuard client on other platforms if needed).

Step 10: Build the client configuration with safe, verifiable values

We are about to create a client configuration that points to the gateway’s public endpoint. Because public IPs and DNS names vary, we will first detect what we can from the server side and then set an explicit variable for the endpoint.

First, we will print the server’s public IP (best-effort). If the server is behind NAT, this will show the NAT public IP, but we still must ensure UDP/51820 is forwarded to this server.

PUBLIC_IP=$(curl -fsS https://ifconfig.me || true)
echo "$PUBLIC_IP"

If this printed an IP, we can use it as the endpoint. If it printed nothing, we should use a known DNS name or the known public IP from our ISP/cloud provider. We will set an endpoint variable in a way that remains copy/paste safe by using the detected value when available.

WG_ENDPOINT="${PUBLIC_IP}"
echo "$WG_ENDPOINT"

If WG_ENDPOINT is empty, we must set it to a real DNS name or IP before generating the client config. In production, a stable DNS name is preferred.

Now we will generate the client configuration file. We will route only the internal subnet and the gateway itself through the VPN. This keeps the VPN focused on private access rather than forcing all internet traffic through the gateway.

SERVER_PUB_KEY=$(sudo cat /etc/wireguard/server.pub)
CLIENT1_PRIV_KEY=$(sudo cat /etc/wireguard/client1.key)
INTERNAL_SUBNET=$(ip route | awk '$1 ~ /^(10.|192.168.|172.(1[6-9]|2[0-9]|3[0-1]).)/ {print $1; exit}')

sudo tee /etc/wireguard/client1.conf >/dev/null <<EOF
[Interface]
PrivateKey = ${CLIENT1_PRIV_KEY}
Address = 10.44.0.10/32
DNS = 1.1.1.1

[Peer]
PublicKey = ${SERVER_PUB_KEY}
Endpoint = ${WG_ENDPOINT}:51820
AllowedIPs = 10.44.0.1/32, ${INTERNAL_SUBNET}
PersistentKeepalive = 25
EOF

sudo chmod 600 /etc/wireguard/client1.conf
sudo sed -n '1,200p' /etc/wireguard/client1.conf

We have created a client configuration at /etc/wireguard/client1.conf. The AllowedIPs on the client side defines what traffic goes into the tunnel. We included the gateway’s VPN IP and the internal subnet so only private access uses the VPN.

Step 11: Connect from an Ubuntu client and verify end-to-end access

We are about to bring up the tunnel on an Ubuntu client. We will install WireGuard, place the config, start the interface, and then verify connectivity and routing.

On the Ubuntu client: install WireGuard

We will install WireGuard on the client so it can establish the encrypted tunnel.

sudo apt-get update
sudo apt-get install -y wireguard

WireGuard is now installed on the client. Next we will place the configuration file.

On the Ubuntu client: place the configuration and start the tunnel

We will create /etc/wireguard/wg0.conf on the client using the contents of client1.conf from the server. We should transfer it securely (for example, via a secure admin channel). Once in place, we will start it with systemd for persistence.

sudo install -d -m 0700 /etc/wireguard
sudo chmod 700 /etc/wireguard
sudo nano /etc/wireguard/wg0.conf

We have opened an editor to paste the client configuration. After saving the file, we will lock down permissions and start the service.

sudo chmod 600 /etc/wireguard/wg0.conf
sudo systemctl enable --now wg-quick@wg0
sudo systemctl status wg-quick@wg0 --no-pager

The client tunnel is now enabled and started. Next we will verify that the client has the VPN IP and that the handshake occurs.

ip -br addr show wg0
sudo wg show

If the handshake timestamp updates after traffic is sent, the tunnel is working. Now we will verify reachability to the gateway and then to an internal host.

ping -c 3 10.44.0.1

If ping succeeds, the client can reach the gateway over the VPN. Next we will verify that routes exist for the internal subnet and test a real internal service.

ip route | grep -E '10.44.0.1|10.|192.168.|172.(1[6-9]|2[0-9]|3[0-1])' || true

We have inspected routing. Now we should test an internal service by IP and port. Because internal services differ, we will first discover a reachable internal IP from the client side by checking ARP/neighbors only if we are on the same L2 (often we are not). In routed environments, we should use a known internal service IP.

nc -vz 10.44.0.1 22 || true

This confirms whether SSH on the gateway is reachable over the VPN (if SSH is running). For internal services, we should test the specific service port (for example, 443 for an internal web app) using the known internal IP.

Step 12: Enforce “least reach” with firewall rules per peer

At this point, the VPN provides controlled entry. Now we will make it intentional: not every connected device should reach every internal service. On a single gateway, the most practical approach is to enforce policy based on the client’s VPN IP.

We will demonstrate a pattern: allow client1 (10.44.0.10) to reach only one internal host on one port, and deny everything else to the internal subnet. This is how we keep access tight without buying additional platforms.

First, we will choose an internal target. We will not guess it. We will print the gateway’s private routes again so we can pick the correct internal subnet, and then we will set variables for an internal host IP and port.

ip route | awk '$1 ~ /^(10.|192.168.|172.(1[6-9]|2[0-9]|3[0-1]).)/ {print}'
INTERNAL_SUBNET=$(ip route | awk '$1 ~ /^(10.|192.168.|172.(1[6-9]|2[0-9]|3[0-1]).)/ {print $1; exit}')
echo "$INTERNAL_SUBNET"

Now we will set an internal host and port as shell variables. We will not embed fake values into firewall commands. We will print the variables so we can confirm they are set before applying rules.

INTERNAL_HOST_IP=""
INTERNAL_HOST_PORT=""

echo "INTERNAL_HOST_IP=${INTERNAL_HOST_IP}"
echo "INTERNAL_HOST_PORT=${INTERNAL_HOST_PORT}"

If these are empty, we must set them to real values before proceeding. Once set, we can apply rules. The approach below is production-safe: we explicitly allow what we need, then explicitly deny broader access.

Now we will allow client1 to reach the specific internal host and port, routed via the gateway.

sudo ufw route allow proto tcp from 10.44.0.10 to ${INTERNAL_HOST_IP} port ${INTERNAL_HOST_PORT}

We have allowed a single, specific path. Next we will deny client1 from reaching the rest of the internal subnet. This ensures the allow rule is the exception, not the default.

sudo ufw route deny from 10.44.0.10 to ${INTERNAL_SUBNET}

We have added a deny rule for broader internal access. UFW processes rules in order, and more specific rules should be placed before broader denies. If we need to adjust ordering, we can insert rules with numbered positions. Now we will verify the active rules.

sudo ufw status numbered

We can now see the allow and deny rules. From the client, we should verify that the allowed service works and that other internal destinations fail as expected. This is the point where access becomes controlled and predictable.

Operational checks we should keep in our runbook

In production, we want quick commands that answer: is the service up, is it listening, are peers connected, and is the firewall doing what we think it is doing?

  • WireGuard service state:
sudo systemctl status wg-quick@wg0 --no-pager

This confirms whether the VPN interface is managed and running.

  • WireGuard peer and handshake status:
sudo wg show

This shows peers, allowed IPs, and handshake timestamps.

  • Listening port check:
ss -lunp | awk 'NR==1 || /:51820/'

This confirms the UDP port is open locally.

  • Firewall status and rules:
sudo ufw status verbose
sudo ufw status numbered

This confirms what is allowed and denied.

  • Forwarding state:
sysctl net.ipv4.ip_forward

This confirms routing capability is still enabled after reboots and updates.

Troubleshooting

Symptom: Client connects but there is no handshake

  • Likely causes:
    • UDP/51820 is blocked by an upstream firewall or ISP.
    • NAT/port-forwarding is missing if the gateway is behind a router.
    • Endpoint IP/DNS is wrong in the client config.
  • Fix:
    • On the gateway, confirm WireGuard is listening: ss -lunp | awk 'NR==1 || /:51820/'
    • On the gateway, confirm UFW allows the port: sudo ufw status verbose
    • On the client, confirm the endpoint value in /etc/wireguard/wg0.conf is correct.
    • If behind NAT, configure port-forwarding UDP/51820 to the gateway’s LAN IP.

Symptom: Handshake works, but internal services are unreachable

  • Likely causes:
    • IP forwarding is disabled.
    • UFW forwarding policy is not set to ACCEPT.
    • NAT rule is missing or uses the wrong external interface.
    • Route rules are too strict or missing.
  • Fix:
    • Check forwarding: sysctl net.ipv4.ip_forward (must be 1).
    • Check UFW forward policy: grep -n '^DEFAULT_FORWARD_POLICY' /etc/default/ufw
    • Confirm NAT rule references the correct interface in /etc/ufw/before.rules.
    • Reload UFW: sudo ufw reload
    • Review route rules: sudo ufw status numbered

Symptom: Client can reach some internal hosts but not others

  • Likely causes:
    • Per-peer firewall rules deny broader access (by design).
    • Internal host firewall blocks the gateway or VPN subnet.
    • Internal routing expects a different return path.
  • Fix:
    • Confirm UFW route rules for the client VPN IP: sudo ufw status numbered
    • On the internal host, allow traffic from the gateway or VPN subnet as appropriate.
    • If avoiding NAT and using pure routing, ensure internal routers know how to reach 10.44.0.0/24 via the gateway.

Symptom: After reboot, VPN does not come back

  • Likely causes:
    • The service was started but not enabled.
    • Configuration file permissions or syntax errors prevent startup.
  • Fix:
    • Enable and start: sudo systemctl enable --now wg-quick@wg0
    • Check logs: sudo journalctl -u wg-quick@wg0 --no-pager -n 200
    • Validate config permissions: sudo ls -l /etc/wireguard/wg0.conf (should be 600).

Common mistakes

  • Mistake: NAT rule uses the wrong interface name.
    Symptom: Handshake works, but internal access times out; gateway can reach internal hosts locally, client cannot.
    Fix: Re-detect the default route interface and update /etc/ufw/before.rules:

    EXT_IFACE=$(ip route show default | awk '{print $5; exit}')
    echo "$EXT_IFACE"
    sudo grep -n 'POSTROUTING' /etc/ufw/before.rules
    sudo ufw reload

    This confirms the correct interface and reloads firewall rules.

  • Mistake: Client AllowedIPs is too broad or too narrow.
    Symptom: Either nothing routes through the tunnel, or unexpected traffic routes through the tunnel.
    Fix: Keep it explicit (gateway VPN IP + internal subnet). On the client:

    sudo sed -n '1,200p' /etc/wireguard/wg0.conf
    sudo systemctl restart wg-quick@wg0
    sudo wg show

    This confirms the config and restarts the client tunnel cleanly.

  • Mistake: Peer AllowedIPs on the server overlaps between clients.
    Symptom: One client connects and another client loses access or traffic goes to the wrong peer.
    Fix: Ensure each peer has a unique /32 VPN IP on the server. Verify on the gateway:

    sudo wg show
    sudo awk 'BEGIN{RS="\[Peer\]"} NR>1{print $0 "n---"}' /etc/wireguard/wg0.conf

    This shows peer blocks so we can spot overlapping assignments.

  • Mistake: UFW is enabled but forwarding policy was not updated.
    Symptom: Client can ping 10.44.0.1 but cannot reach internal services.
    Fix: Set forward policy and reload:

    sudo sed -i 's/^DEFAULT_FORWARD_POLICY=.*/DEFAULT_FORWARD_POLICY="ACCEPT"/' /etc/default/ufw
    grep -n '^DEFAULT_FORWARD_POLICY' /etc/default/ufw
    sudo ufw reload

    This enables forwarding behavior under UFW control.

How do we at NIILAA look at this

This setup is not impressive because it is complex. It is impressive because it is controlled. Every component is intentional. Every configuration has a reason. This is how infrastructure should scale — quietly, predictably, and without drama.

At NIILAA, we help organizations design, deploy, secure, and maintain this kind of access in real production environments—whether that is a small office, a growing SME with multiple sites, or a regulated environment that needs tighter controls. We focus on clear network boundaries, identity-driven access, operational runbooks, and changes that survive reboots, audits, and team turnover.

Leave A Comment

All fields marked with an asterisk (*) are required