Preventing Lateral Movement With WireGuard

One common tactic you see again and again in cybersecurity attacks is an adversary compromising a low-value device (like a low-level employee’s laptop, or an ancient webserver running in the corner of a datacenter), and then moving laterally from that foothold to gain access to the company’s most valuable data and systems. WireGuard can help prevent this, allowing you to implement network segmentation at a very fine level (aka micro-segmentation) that ensures an adversary can’t easily leverage an opening on one endpoint in your network to access the rest of the network.

Of course, before you implement micro-segmentation, you should try to care of the basics first, like:

  • Ensure all your systems are fully patched

  • Enforce strong passwords

  • Require multi-factor authentication or hardware-backed credentials

  • Enforce least privileged access

  • Separate accounts for admin functionality

  • Implement endpoint monitoring and intrusion detection

All of those things will also help prevent lateral movement — in addition to preventing an adversary from gaining a foothold on your systems in the first place.

Micro-segmentation With WireGuard

Traditional network segmentation often just entails grouping “like with like” — putting the Sales Department PCs on one network and the Engineering Department PCs on another network, or your webservers on one network and your data analytics systems on another — and then simply not enabling access between networks which don’t need it. More fine-grained segmentation beyond that can be difficult to maintain, especially on networks which assign IP addresses dynamically (eg with DHCP) or otherwise have computers added to and removed from them frequently.

WireGuard makes micro-segmentation much more practical, using cryptokey routing to securely bind a private IP address to a device. This allows you to use to WireGuard in combination with basic firewall settings on the device itself to prevent any unauthorized access from any other devices on any network — even the network to which the device is physically connected.

For example, for a Linux server with nftables for its firewall, you might use the following simple nftables firewall configuration to prevent access to the server except through WireGuard:

#!/usr/sbin/nft -f
flush ruleset

define pub_iface = "eth0"
define wg_iface = "wg0"
define wg_port = 51820

table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;

        # accept all loopback packets
        iif "lo" accept
        # accept all icmp/icmpv6 packets
        meta l4proto { icmp, ipv6-icmp } accept
        # accept all packets that are part of an already-established connection
        ct state vmap { invalid : drop, established : accept, related : accept }
        # drop new connections over rate limit
        ct state new limit rate over 1/second burst 10 packets drop

        # accept all DHCPv6 packets received at a link-local address
        ip6 daddr fe80::/64 udp dport dhcpv6-client accept
        # accept all WireGuard packets received on a public interface
        iifname $pub_iface udp dport $wg_port accept
        # accept all packets from the WireGuard network
        iifname $wg_iface accept

        # reject with polite "port unreachable" icmp response
        reject
    }

    chain forward {
        type filter hook forward priority 0; policy drop;
        reject with icmpx type host-unreachable
    }
}

With a host-based firewall like the above in place, network access to the system (except for a few basic operational elements like ICMP or DHCP) is restricted only to WireGuard peers, authorized in the device’s own WireGuard configuration.

Point-to-Point Access

If a device only requires access a small, fixed set of other devices, you may wish to set up it and its peers with a point-to-point WireGuard configuration. For example, a back-end server that processes jobs from an event queue and stores the results in a database might just need three static connections: one to the event queue, one to the database, and one to a configuration-management system that can update the back-end server with the latest software and config settings it needs to run.

WireGuard Point-to-Point Network Segmentation
Figure 1. Example point-to-point WireGuard network with micro-segmentation

The WireGuard configuration of the back-end server in this example might look the following:

# /etc/wireguard/wg0.conf

# local settings for back-end server
[Interface]
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=
Address = 10.0.0.1/32
ListenPort = 51820

# remote settings for event queue
[Peer]
PublicKey = fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=
Endpoint = eq.ue12.eng.corp:51820
AllowedIPs = 10.0.0.2/32

# remote settings for database
[Peer]
PublicKey = jUd41n3XYa3yXBzyBvWqlLhYgRef5RiBD7jwo70U+Rw=
Endpoint = db.ue12.eng.corp:51820
AllowedIPs = 10.0.0.3/32

# remote settings for configuration management
[Peer]
PublicKey = MMsBeTT9v/7XNWB8a/jSMn9O8olPVNduUwvUPJ6eB14=
AllowedIPs = 10.0.0.4/32

The server is configured with its own WireGuard private key and WireGuard IP address, as well as the public key and WireGuard IP address of each peer authorized to connect to it. Each peer is configured in its own [Peer] block.

One side of each WireGuard connection needs to know the physical IP address and port it can use to connect to the other end of the connection, which is specified via the Endpoint setting for that peer. In the above example, the back-end server knows how to connect to the event queue and database servers (using their internal DNS names), so it has an Endpoint setting specified for each. It has no Endpoint specified for the configuration management server, as it expects the configuration management server to know how to connect to it via the back-end server’s own physical network IP address.

The configuration-management server’s own WireGuard configuration config file might look like the following:

# /etc/wireguard/wg0.conf

# local settings for configuration management
[Interface]
PrivateKey = CDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDHA=
Address = 10.0.0.4/32
ListenPort = 51820

# remote settings for back-end server
[Peer]
PublicKey = /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=
Endpoint = be34.ue12.eng.corp:51820
AllowedIPs = 10.0.0.1/32

# remote settings for other servers ...

Notice how WireGuard config for the back-end server requires the public key of the configuration-management’s server, and the WireGuard config for configuration-management server requires public key of the back-end server. Because each side of the WireGuard connection must authenticate to the other (aka mutual authentication), if a peer is compromised, an adversary can’t simply reconfigure it to connect to other peers — the other peers themselves must be pre-configured to allow a connection.

Similarly, an adversary can’t simply reconfigure a compromised peer to use a WireGuard IP address of her choosing — the AllowedIPs setting on the other side of the connection limits the IP addresses that will be accepted for the peer. The AllowedIPs setting for the configuration-management server in the back-end server’s WireGuard config file above only accepts packets from (and sends packets to) the IP address 10.0.0.4. And the AllowedIPs setting for the back-end server in the configuration-management server’s WireGuard config only accepts packets from (and sends packets to) the IP address 10.0.0.1.

Hub-and-Spoke Access

For devices that require access from a lot of other devices, or that are not set up with configuration management or other tools to manage their WireGuard configuration, you might rather arrange them with a hub-and-spoke WireGuard configuration. For example, a mail server that allows access from most of your organization’s end-user devices might be best set up as a spoke that is connected through a WireGuard hub to the devices that need access to it.

WireGuard Hub-and-Spoke Network Segmentation
Figure 2. Example hub-and-spoke WireGuard network with micro-segmentation

The advantage of a hub-and-spoke topology is that each individual spoke only has to be configured with a single peer — the hub — rather than of all the other peers to which it is allowed to connect.

The WireGuard configuration of a spoke in this example might look the following:

# /etc/wireguard/wg0.conf

# local settings for mail server
[Interface]
PrivateKey = AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEE=
Address = 10.0.0.1/32
ListenPort = 51820

# remote settings for hub
[Peer]
PublicKey = fE/wdxzl0klVp/IR8UcaoGUMjqaWi3jAd7KzHKFS6Ds=
Endpoint = hub.ue1.wk.corp:51820
AllowedIPs = 10.0.0.0/24
PersistentKeepalive = 25

And the hub’s own WireGuard config might look like the following:

# /etc/wireguard/wg0.conf

# local settings for hub
[Interface]
PrivateKey = ABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBFA=
Address = 10.0.0.2/32
ListenPort = 51820

# remote settings for mail server
[Peer]
PublicKey = /TOE4TKtAqVsePRVR+5AA43HkAK5DSntkOCO7nYq5xU=
AllowedIPs = 10.0.0.1/32

# remote settings for Alice's workstation
[Peer]
PublicKey = jUd41n3XYa3yXBzyBvWqlLhYgRef5RiBD7jwo70U+Rw=
AllowedIPs = 10.0.0.3/32

# remote settings for Bob's workstation
[Peer]
PublicKey = MMsBeTT9v/7XNWB8a/jSMn9O8olPVNduUwvUPJ6eB14=
AllowedIPs = 10.0.0.4/32

# remote settings for other user devices ...

Another advantage of a hub-and-spoke topology is that you can exercise further fine-grained access control among the hub’s spokes by using the hub’s own firewall. This allows you to apply per-service access-control rules to all the peers in the WireGuard network in one centralized location.

For example, if to the above WireGuard hub you added a VoIP spoke, a fileshare spoke, and a spoke for Alice’s phone, by adding the following [Peer] blocks to the hub’s WireGuard config file:

# remote settings for VoIP server
[Peer]
PublicKey = kAYKIv6gpBDqueaVNOsY8ddKmIC2dnnXJQ0iKzWxfBI=
AllowedIPs = 10.0.0.5/32

# remote settings for fileshare
[Peer]
PublicKey = af24MfZ5LzUedF5WlpK2+O8602g2fmiKO8dYdv8dUyI=
AllowedIPs = 10.0.0.6/32

# remote settings for Alice's phone
[Peer]
PublicKey = QHy/1+olsMKePzg/044wWUunKs/IfWzy4Ub/iJbzJ00=
AllowedIPs = 10.0.0.7/32

You could then use the hub’s firewall to limit Alice’s workstation and Bob’s workstation to only being able to access the mail server and the fileshare; and limit Alice’s phone to only being able to access the mail server and the VoIP server. You’d apply these access control rules using the WireGuard IP addresses of each device (recall as described above, WireGuard’s cryptokey routing allows you to securely bind an IP address to a device).

The access-control settings of the hub’s firewall would look something like this:

Source Host Source IP Destination Host Destination IP Destination Ports

Alice’s workstation

10.0.0.3

Mail server

10.0.0.1

TCP 465 & TCP 993

Alice’s phone

10.0.0.7

Mail server

10.0.0.1

TCP 465 & TCP 993

Bob’s workstation

10.0.0.4

Mail server

10.0.0.1

TCP 465 & TCP 993

Alice’s phone

10.0.0.7

VoIP server

10.0.0.5

TCP 5061 & UDP 10000-20000

Alice’s workstation

10.0.0.3

Fileshare

10.0.0.6

TCP 139, TCP 445, UDP 137, & UDP 138

Bob’s workstation

10.0.0.4

Fileshare

10.0.0.6

TCP 139, TCP 445, UDP 137, & UDP 138

See the Firewall Configuration section of the Zero-Trust Architecture With WireGuard guide for an example nftables ruleset that allows you to apply access-control rules just like this.

Note that the disadvantage of a hub-and-spoke topology is that because the hub can connect to a large number of devices, the hub itself will be an attractive target for an adversary trying to move laterally through your network. So it’s important to make sure that your hubs are well hardened, with up-to-date patches, no extraneous network services, and limited remote access.