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
Designing Secure Network Access for Remote Employees

Designing secure remote access is never urgent—until it is

Remote access usually starts as a convenience. A few engineers need to reach an internal dashboard. A finance lead needs a report from a file share. A vendor needs a short maintenance window. At first, it feels manageable: a couple of firewall rules, a quick exception, a “temporary” path that becomes permanent.

Then the organization grows. Teams spread across time zones. Contractors rotate in and out. Devices multiply. The internal network becomes a dense map of legacy systems, SaaS connectors, and workloads that were never meant to be exposed. And the risk changes shape: it is no longer about “can someone get in,” but “what happens if one identity, one device, or one session is compromised.”

That is where Zero Trust Architecture stops being a buzzword and becomes a practical operating model. For Secure Remote Access, the goal is not to build a bigger perimeter. The goal is to make access policy-driven, identity-bound, device-aware, and narrowly scoped—so remote work scales without turning the internal network into a shared secret.

Architecture we are going to implement

We are going to implement a production-grade remote access design aligned with Zero Trust Architecture principles:

  • No implicit trust based on network location: remote users do not “join the LAN.” They receive only the minimum routes and permissions required.
  • Strong identity and device posture: access is granted based on who the user is and whether the device meets policy.
  • Policy-driven segmentation: we define access by application and role, not by broad network reach.
  • Continuous verification: sessions are logged, monitored, and can be revoked quickly.

Implementation-wise, we will build a secure remote access gateway using WireGuard as the encrypted transport, and we will enforce least privilege using routing and firewall policy. This gives us a clean, auditable foundation that can integrate with enterprise identity and device posture systems.

We will also explicitly avoid approaches that do not meet enterprise policy-driven requirements.

Prerequisites and assumptions

Before we touch configuration, we need to be explicit about the environment. Secure remote access fails in production when assumptions are implicit.

  • Server OS: Ubuntu Server 22.04 LTS (fresh install recommended). We assume systemd is present and enabled.
  • Privileges: we need root privileges (either direct root shell or a sudo-capable admin account).
  • Network placement: one Linux server will act as the remote access gateway. It must have:
    • One interface facing the internet (public IP directly or via NAT with port-forwarding).
    • Network reachability to internal resources we intend to publish (for example, an internal subnet like 10.20.0.0/16).
  • Firewall control: we will manage host firewall rules using UFW (which configures nftables/iptables under the hood). If an enterprise firewall exists upstream, we still enforce host-level policy.
  • DNS: we assume we can define internal DNS resolution for private services (either via an internal DNS server or split-horizon DNS). We will not rely on public DNS for internal names.
  • Client devices: remote employee endpoints can be Windows, macOS, or Linux. We will keep the server-side implementation consistent and provide verification guidance that works across environments.
  • Security baseline:
    • SSH access is restricted (ideally to a management subnet or a bastion).
    • Automatic security updates are enabled or centrally managed.
    • We have a logging destination (local journald at minimum; SIEM forwarding is preferred).

We are building this as an enterprise policy-driven system. That means we will treat access as a controlled product: documented, repeatable, and revocable.

Step 1: Prepare the gateway host and confirm network facts

Before installing anything, we will confirm the gateway’s interface names, default route, and current firewall state. This matters because production outages often come from applying rules to the wrong interface or assuming the wrong egress path.

sudo -s
uname -a
lsb_release -a
ip -br link
ip route show default
ss -tulpn | head -n 50
ufw status verbose || true

We are now running as root, confirmed the OS version, listed interfaces, checked the default route, inspected listening services, and checked whether UFW is already active. This gives us a baseline so we can prove what changed later.

Step 2: Install WireGuard and harden basic system settings

Next we will install WireGuard and supporting tooling. We are choosing WireGuard because it is minimal, fast, and straightforward to audit. The security posture comes from how we scope routes and enforce policy—not from complexity.

sudo -s
apt-get update
apt-get install -y wireguard wireguard-tools ufw qrencode

WireGuard and UFW are now installed. Nothing is exposed yet; we have only added packages. The next step is to enable IP forwarding in a controlled way, because the gateway must route traffic between the encrypted tunnel and internal networks.

Enable IPv4 forwarding persistently

We will enable IPv4 forwarding via sysctl so it persists across reboots. We will also avoid enabling unnecessary forwarding features.

sudo -s
cat > /etc/sysctl.d/99-remote-access-gateway.conf <<'EOF'
# Remote access gateway: allow IPv4 forwarding for routed access
net.ipv4.ip_forward=1

# Basic hardening: do not accept redirects, do not send redirects
net.ipv4.conf.all.accept_redirects=0
net.ipv4.conf.default.accept_redirects=0
net.ipv4.conf.all.send_redirects=0
net.ipv4.conf.default.send_redirects=0

# Basic spoofing protection
net.ipv4.conf.all.rp_filter=1
net.ipv4.conf.default.rp_filter=1
EOF

sysctl --system
sysctl net.ipv4.ip_forward
sysctl net.ipv4.conf.all.accept_redirects
sysctl net.ipv4.conf.all.send_redirects
sysctl net.ipv4.conf.all.rp_filter

We have now enabled IPv4 forwarding persistently and applied a small set of safe network hardening settings. The verification commands confirm the kernel parameters are active.

Step 3: Define the Zero Trust access model before we configure the tunnel

Before generating keys, we will define what “remote access” means in this design. In Zero Trust Architecture terms, we are not granting “network access.” We are granting policy-defined access to specific internal destinations.

We will implement this with three controls:

  • Scoped routing: clients receive only the routes they need (for example, a single internal subnet or even a single host route).
  • Firewall enforcement: even if a client tries to reach other internal ranges, the gateway drops it.
  • Per-identity keys: each employee gets a unique keypair and a unique tunnel IP. Offboarding becomes a single-line removal.

For the rest of this guide, we will assume:

  • WireGuard tunnel network: 10.99.0.0/24 (gateway is 10.99.0.1)
  • Internal application subnet we will allow: 10.20.0.0/16

If our internal network differs, we will detect it and set variables so commands remain copy/paste-safe.

Step 4: Create WireGuard server keys and a production-ready configuration

Now we will generate the server keypair and write a complete WireGuard configuration. We will lock down file permissions because private keys must never be world-readable.

sudo -s
umask 077
wg genkey | tee /etc/wireguard/server.key | wg pubkey > /etc/wireguard/server.pub
ls -l /etc/wireguard/server.key /etc/wireguard/server.pub
cat /etc/wireguard/server.pub

We generated the server private key and public key with restrictive permissions. The public key is safe to share with clients; the private key must remain on the gateway.

Detect the external interface and internal CIDR variables

Next we will detect the external interface used for the default route. We will store it in a shell variable so subsequent firewall and NAT rules apply to the correct interface.

sudo -s
EXT_IFACE=$(ip route show default | awk '{print $5; exit}')
echo "External interface: ${EXT_IFACE}"

We now have the external interface name in EXT_IFACE. This reduces the risk of applying NAT to the wrong interface, which is a common production mistake.

Write the WireGuard server configuration

We will create /etc/wireguard/wg0.conf. This configuration does three important things:

  • Defines the tunnel IP for the gateway.
  • Listens on a single UDP port (51820 by default).
  • Applies NAT for traffic leaving via the external interface, so remote clients can reach internal networks without complex upstream routing changes.
sudo -s
SERVER_PRIV_KEY=$(cat /etc/wireguard/server.key)
cat > /etc/wireguard/wg0.conf <<EOF
[Interface]
Address = 10.99.0.1/24
ListenPort = 51820
PrivateKey = ${SERVER_PRIV_KEY}

# Policy enforcement hooks: keep them explicit and auditable.
# NAT is applied only for traffic leaving the external interface.
PostUp = ufw route allow in on wg0 out on ${EXT_IFACE}
PostUp = iptables -t nat -A POSTROUTING -o ${EXT_IFACE} -j MASQUERADE
PostDown = iptables -t nat -D POSTROUTING -o ${EXT_IFACE} -j MASQUERADE

# Peers will be appended below as employees are onboarded.
EOF

chmod 600 /etc/wireguard/wg0.conf
ls -l /etc/wireguard/wg0.conf
grep -n "^[Interface]" -n /etc/wireguard/wg0.conf

We created a complete server configuration with correct permissions. We also added explicit PostUp/PostDown hooks so routing and NAT behavior is consistent across reboots and service restarts. At this point, the tunnel is defined but not started.

Step 5: Configure the firewall for a controlled, enterprise-friendly posture

Now we will configure UFW to allow only what we need:

  • WireGuard UDP port inbound
  • Forwarding from the tunnel to approved internal destinations
  • No broad “allow all” rules

We will also enable UFW logging at a reasonable level so we can troubleshoot without drowning in noise.

Allow WireGuard inbound and enable routed traffic

First we will allow inbound UDP/51820 and enable forwarding policy in UFW. UFW defaults are typically restrictive for forwarding, so we will set them explicitly.

sudo -s
ufw default deny incoming
ufw default allow outgoing
ufw default deny routed

ufw allow 51820/udp comment 'WireGuard remote access'

# Allow SSH only if we already have a safe management path.
# If SSH is already restricted upstream, we still keep this tight.
ufw allow 22/tcp comment 'SSH management (restrict upstream)'

# Enable logging for operational visibility
ufw logging medium

We have now set conservative defaults, allowed only the WireGuard port and SSH, and enabled logging. Routed traffic is still denied by default, which is what we want before we define explicit policy routes.

Allow only the internal destinations we intend to publish

Next we will allow forwarding from wg0 to the internal subnet we are publishing. This is where Zero Trust becomes real: we are not granting access to “the network,” we are granting access to a defined destination range.

We will store the internal CIDR in a variable so it is easy to audit and change.

sudo -s
INTERNAL_CIDR="10.20.0.0/16"
ufw route allow in on wg0 out on ${EXT_IFACE} to ${INTERNAL_CIDR} comment 'Remote access to approved internal subnet'

We have now allowed routed traffic from the tunnel to the approved internal subnet only. Everything else remains denied by default, which prevents accidental lateral movement.

Enable UFW and verify

Now we will enable UFW. This is a production-impacting step, so we verify status immediately after.

sudo -s
ufw --force enable
ufw status verbose
ufw status numbered

UFW is now active with explicit rules. The numbered output gives us a stable reference for change control and future audits.

Step 6: Start WireGuard and make it persistent

Now we will start the WireGuard interface and enable it at boot. Persistence matters: remote access that disappears after a reboot is not an enterprise service.

sudo -s
systemctl enable --now wg-quick@wg0
systemctl status wg-quick@wg0 --no-pager
wg show
ip addr show wg0
ss -lunp | grep -E ':(51820)b' || true

The WireGuard service is now enabled and running. We verified the interface exists, the service is healthy, and the UDP port is listening. At this stage, no clients can connect yet because we have not added any peers.

Step 7: Onboard a remote employee as a peer (policy-driven, per-identity)

We will now onboard one employee. The key principle is simple: one person, one keypair, one tunnel IP. This makes access revocation clean and auditable.

We will generate the employee keypair on the gateway for operational simplicity. In higher-assurance environments, we can generate keys on the endpoint and only import the public key to the gateway.

Create an employee peer with a fixed tunnel IP

We will create keys for an example employee identity called employee01 and assign 10.99.0.10/32. The /32 ensures the peer is pinned to a single address.

sudo -s
EMP_ID="employee01"
EMP_IP="10.99.0.10/32"

umask 077
wg genkey | tee /etc/wireguard/${EMP_ID}.key | wg pubkey > /etc/wireguard/${EMP_ID}.pub

EMP_PUB_KEY=$(cat /etc/wireguard/${EMP_ID}.pub)

cat >> /etc/wireguard/wg0.conf <<EOF

[Peer]
# ${EMP_ID}
PublicKey = ${EMP_PUB_KEY}
AllowedIPs = ${EMP_IP}
EOF

chmod 600 /etc/wireguard/${EMP_ID}.key /etc/wireguard/${EMP_ID}.pub
wg-quick strip wg0 > /tmp/wg0.stripped.conf
rm -f /tmp/wg0.stripped.conf

We generated a unique keypair for the employee, appended a peer stanza to the server configuration, and ensured key files are protected. The peer is constrained to a single tunnel IP, which supports identity-based policy and clean logging.

Apply the updated configuration safely

Now we will apply the new peer configuration without tearing down the interface. This reduces disruption for existing sessions.

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

The running WireGuard interface now includes the new peer. We verified with wg show, which should list the peer public key and allowed IPs.

Generate a client configuration (endpoint-side)

Next we will generate a client configuration file. We will keep routing scoped: the client will only route traffic for the approved internal subnet and the tunnel itself. This prevents “send all traffic through the tunnel” behavior, which is rarely acceptable in enterprise policy-driven designs.

We need the gateway’s public IP (or DNS name) and the server public key. We will print the server public key and detect the public IP if the gateway is directly exposed. If the gateway is behind NAT, we will use the externally assigned IP/DNS from our network team.

sudo -s
SERVER_PUB_KEY=$(cat /etc/wireguard/server.pub)
echo "Server public key: ${SERVER_PUB_KEY}"

# Best-effort public IP detection (works when the host has direct internet egress).
PUBLIC_IP=$(curl -fsS https://api.ipify.org || true)
echo "Detected public IP (verify with network team): ${PUBLIC_IP}"

We now have the server public key and a best-effort detected public IP. In controlled enterprise environments, we should confirm the correct public endpoint via DNS and change control rather than trusting auto-detection.

Now we will generate the client config content. We will not embed uncertain values directly into commands. Instead, we will set variables first and then render the file.

sudo -s
EMP_PRIV_KEY=$(cat /etc/wireguard/${EMP_ID}.key)

# Set the endpoint explicitly. Prefer a managed DNS name in production.
WG_ENDPOINT="${PUBLIC_IP}:51820"

# Define the internal CIDR we are publishing.
INTERNAL_CIDR="10.20.0.0/16"

cat > /etc/wireguard/${EMP_ID}.conf <<EOF
[Interface]
PrivateKey = ${EMP_PRIV_KEY}
Address = 10.99.0.10/32
DNS = 10.20.0.53

[Peer]
PublicKey = ${SERVER_PUB_KEY}
Endpoint = ${WG_ENDPOINT}
AllowedIPs = ${INTERNAL_CIDR}, 10.99.0.0/24
PersistentKeepalive = 25
EOF

chmod 600 /etc/wireguard/${EMP_ID}.conf
sed -n '1,200p' /etc/wireguard/${EMP_ID}.conf

We created a client configuration that routes only the approved internal subnet and the tunnel network. We also set a DNS server (example: 10.20.0.53) to support internal name resolution. PersistentKeepalive helps when clients are behind restrictive NAT.

Step 8: Verification checklist (gateway and client)

Verification is where production readiness shows up. We will verify in layers: service health, cryptographic handshake, routing, and application reachability.

Gateway verification

On the gateway, we will confirm the service is running, the interface is up, and the firewall rules are active.

sudo -s
systemctl is-enabled wg-quick@wg0
systemctl is-active wg-quick@wg0
wg show
ip route show | grep -E '10.99.0.0/24' || true
ufw status verbose
journalctl -u wg-quick@wg0 --no-pager -n 100

We confirmed the service is enabled and active, the tunnel network is present, UFW is enforcing policy, and logs are available for troubleshooting.

Client verification

On the client device, after importing the configuration into the WireGuard client, we verify:

  • The tunnel shows as connected.
  • A handshake occurs (on the gateway, wg show should show “latest handshake”).
  • We can reach an internal service that policy allows.

From the gateway, once the client connects, we should see handshake and transfer counters increment:

sudo -s
wg show

If the handshake timestamp updates and transfer counters increase, the encrypted transport is working. If internal access still fails, the issue is usually routing, firewall policy, or DNS—not the tunnel itself.

Step 9: Operational controls for CTO-level governance

Secure Remote Access in a Zero Trust Architecture is not “set and forget.” We need operational controls that match enterprise policy-driven expectations.

Offboarding: revoke access cleanly

When an employee leaves or a device is lost, we remove the peer stanza and sync the configuration. This is immediate and does not require rotating everyone else’s keys.

sudo -s
EMP_ID="employee01"

# Show current peers for audit context
wg show

# Remove the peer block from the config safely by rewriting the file without that peer comment tag.
# We keep this operationally simple: edit with a controlled process (change request) in production.
grep -n "^[Peer]|^# ${EMP_ID}|^PublicKey|^AllowedIPs" /etc/wireguard/wg0.conf | sed -n '1,200p'

We printed the relevant lines to identify the exact peer block. In production, we remove that block via a controlled edit (tracked change), then apply the update:

sudo -s
systemctl reload wg-quick@wg0 || true
wg syncconf wg0 <(wg-quick strip wg0)
wg show

The peer is now removed from the running configuration. Any attempt to connect with that key will fail because the gateway no longer recognizes it.

Logging and audit posture

We should treat remote access logs as security telemetry. At minimum, we keep:

  • UFW logs for blocked/allowed forwarded traffic
  • WireGuard service logs (systemd/journald)
  • Change history for /etc/wireguard/wg0.conf and firewall rules

In enterprise environments, we forward these logs to a SIEM and correlate with identity provider events and device posture signals.

Troubleshooting

When remote access breaks, we want fast, deterministic checks. We will troubleshoot in the same order the system works: service, port reachability, handshake, routing, firewall, DNS.

Symptom: client cannot connect, no handshake appears on the gateway

  • Likely causes:
    • UDP/51820 not reachable (upstream firewall/NAT missing)
    • Wrong endpoint IP/DNS in client config
    • Server not listening or service down
  • Fix:
    • Verify the service and listener:
      sudo -s
      systemctl status wg-quick@wg0 --no-pager
      ss -lunp | grep -E ':(51820)b' || true
    • Verify UFW allows the port:
      sudo -s
      ufw status verbose | sed -n '1,200p'
    • Confirm upstream NAT/ACL forwards UDP/51820 to this gateway.

Symptom: handshake works, but internal resources are unreachable

  • Likely causes:
    • Forwarding policy missing or too strict
    • Internal network routes do not return traffic to the gateway (asymmetric routing)
    • Internal firewalls block traffic from the tunnel subnet (10.99.0.0/24)
  • Fix:
    • Verify forwarding and UFW route rules:
      sudo -s
      sysctl net.ipv4.ip_forward
      ufw status verbose
      ufw status numbered
    • Confirm NAT rule exists:
      sudo -s
      iptables -t nat -S | sed -n '1,200p'
    • Check UFW logs for drops:
      sudo -s
      journalctl -u ufw --no-pager -n 200 || true
      journalctl --no-pager -n 200 | grep -i ufw || true
    • If internal systems require explicit allowlists, allow source 10.99.0.0/24 to the required ports only.

Symptom: internal hostnames do not resolve, but IP access works

  • Likely causes:
    • Client DNS not set to an internal resolver
    • Internal DNS server not reachable from the tunnel
    • Split-horizon DNS not configured
  • Fix:
    • Confirm the DNS server in the client config is correct and reachable.
    • Ensure firewall policy allows DNS (TCP/UDP 53) from 10.99.0.0/24 to the internal DNS server.

Common mistakes

  • Mistake: Applying NAT to the wrong interface.
    Symptom: Handshake works, but internal access fails and return traffic never arrives.
    Fix: Re-detect the default route interface and ensure EXT_IFACE matches it:

    sudo -s
    ip route show default
    EXT_IFACE=$(ip route show default | awk '{print $5; exit}')
    echo "${EXT_IFACE}"
  • Mistake: Over-broad AllowedIPs on the client.
    Symptom: Unexpected traffic paths, policy violations, or internal monitoring flags because too much traffic is routed into the tunnel.
    Fix: Restrict AllowedIPs to only approved internal CIDRs and the tunnel network, and enforce the same scope on the gateway firewall.
  • Mistake: Forgetting persistence across reboots.
    Symptom: Remote access works until the gateway reboots, then fails silently.
    Fix: Ensure the service is enabled and UFW is enabled:

    sudo -s
    systemctl enable wg-quick@wg0
    systemctl is-enabled wg-quick@wg0
    ufw status verbose
  • Mistake: Treating the tunnel as “internal network membership.”
    Symptom: Lateral movement risk increases over time; audits fail because access is not least-privilege.
    Fix: Keep routing scoped, enforce UFW route rules to specific destinations, and publish applications intentionally rather than exposing broad subnets.

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 enterprise policy driven secure remote access on Zero Trust Architecture foundations. That includes access modeling, segmentation strategy, identity and device posture integration, production hardening, logging and audit readiness, and operational runbooks that hold up under real incident pressure.

Website: https://www.niilaa.com
Email: [email protected]
LinkedIn: https://www.linkedin.com/company/niilaa
Facebook: https://www.facebook.com/niilaa.llc

Leave A Comment

All fields marked with an asterisk (*) are required