Both sites use the same LAN range, and the overlap is not what actually breaks the design. The hub must choose a site using only the destination address, and two identical destinations give it nothing to choose with. Renumbering a working office to satisfy a tunnel is usually a worse trade than giving each published service a distinct address the tunnel can carry. The design below publishes one HTTPS service per site and nothing else.

Why the overlap is not the real problem

WireGuard forwards by destination: the AllowedIPs of a peer decide which peer receives a packet. Two peers cannot both advertise 10.10.7.0/24 without one of them winning every lookup. That is a property of the peer table rather than of the tunnel, which is why the fix here is an address plan and not a routing trick.

The plan is to stop advertising the LAN at all. Advertise a per-service alias instead, one address per site, and let each site's gateway translate that alias to its real application.

The user routes two unique addresses, never sees the ambiguous prefix, and nothing in either office changes. This also states the boundary honestly: you are publishing one HTTPS service from each site, not merging two networks. The rules that follow stay small because the statement is narrow. A wider statement would need wider rules, which is how these designs get complicated.

Give each site an alias the user can route

Choose addresses from a range that conflicts with nothing on the user's machine, the hub or either LAN. Here the hub is 10.66.0.1, the user is 10.66.0.10, the gateways are 10.66.0.21 and 10.66.0.22, and the two aliases are 10.77.1.20 and 10.77.2.20. The hub publishes each alias through the peer that owns it, so an outgoing packet addressed to one alias can only leave through the matching site:

[Interface]
Address = 10.66.0.1/32
ListenPort = 51820
PrivateKey = HUB_KEY

[Peer]
PublicKey = USER_PUBLIC_KEY
AllowedIPs = 10.66.0.10/32

[Peer]
PublicKey = SITE_A_PUBLIC_KEY
AllowedIPs = 10.66.0.21/32, 10.77.1.20/32

[Peer]
PublicKey = SITE_B_PUBLIC_KEY
AllowedIPs = 10.66.0.22/32, 10.77.2.20/32

The user side needs only the two aliases, which keeps the client configuration free of both LAN prefixes. Nothing in that configuration mentions either real LAN:

[Interface]
Address = 10.66.0.10/32
PrivateKey = USER_KEY

[Peer]
PublicKey = HUB_PUBLIC_KEY
Endpoint = hub.example.net:51820
AllowedIPs = 10.77.1.20/32, 10.77.2.20/32
PersistentKeepalive = 25

Each gateway advertises the hub and the user source, nothing else, so a site never learns the other site's LAN. Keepalive maintains a NAT mapping on the site uplink; it does not authorise traffic, and the firewall still decides what is admitted. Copying one gateway's configuration to the other is fine as long as the private key, the interface address and the alias all change.

What the hub must permit between two of its peers

The hub forwards packets between two of its own peers, which is a forward-chain decision rather than an input decision. Enable IPv4 forwarding through the platform's normal mechanism, then permit exactly the pairs the design needs.

The rule below allows the user to one alias and one port, and permits return traffic through connection tracking. It drops everything else in the forward path, which is the point:

nft add table inet hub_transit
nft add chain inet hub_transit forward '{ type filter hook forward priority 0 ; policy drop ; }'
nft add rule inet hub_transit forward ct state established,related accept
nft add rule inet hub_transit forward iifname wg0 oifname wg0 tcp dport 443 ip saddr 10.66.0.10 ip daddr 10.77.1.20 accept

Two details are worth stating because they cause quiet failures. An accept in one base chain does not override a drop in another, so merge this with the existing policy rather than loading it alongside. And the source address identifies the device, not the person; keep application authentication separate from network admission.

Where an alias becomes a real address

Site A receives a request for 10.77.1.20, rewrites the destination to the real application, and rewrites the source to its own LAN address so the reply can return without a route to the tunnel. Doing both translations in one place keeps the rest of the network ignorant of the alias. The rules below are the entire mechanism:

nft add table ip site_a_nat
nft add chain ip site_a_nat pre '{ type nat hook prerouting priority -100 ; policy accept ; }'
nft add chain ip site_a_nat post '{ type nat hook postrouting priority 100 ; policy accept ; }'
nft add rule ip site_a_nat pre iifname wg0 tcp dport 443 ip saddr 10.66.0.10 ip daddr 10.77.1.20 dnat to 10.10.7.20:443
nft add rule ip site_a_nat post iifname wg0 oifname lan0 tcp dport 443 ip saddr 10.66.0.10 ip daddr 10.10.7.20 snat to 10.10.7.2

Site A's forward policy must then admit the translated flow from the tunnel interface to the LAN interface, plus the return direction. The forward hook sees the destination after destination translation and before the source rewrite, which is why the rule names the real application address. Read those two hooks in that order whenever a rule appears to match nothing. A forward rule written against the alias can never match, because the alias no longer exists at that hook.

Site B repeats this with its own alias and its own key. It must not reuse Site A's private key, because two peers sharing one identity is a configuration error that is hard to see and easy to create by copying a file. Keep both gateway documents identical except for the values that must differ, so the difference stands out in review. A copied file is the usual origin of that fault.

The source rewrite has a cost. The application sees the gateway as its client, so its logs no longer show which user connected. Keep the gateway audit trail, and do not weaken the application login to compensate for a network identity that translation removed.

How to prove both published names answer

Give each alias a DNS name and configure the application and certificate for that name, because translation does not rewrite redirects or embedded absolute URLs. Use one name per site so a certificate mismatch can never be mistaken for a routing fault.

Test from the real client, confirm which site answered, and test an unauthorised port as well. Use new connections after any NAT change, since established state keeps the old mapping:

wg show wg0 allowed-ips
sudo nft list ruleset
curl -sS -o /dev/null -w "%{http_code}\n" https://site-a.example.net/

An empty or unexpected alias in the first command means the peer selection is wrong before any firewall rule matters. If the alias is right and the name still fails, the problem has moved to DNS and certificates. Once both names answer and the third port stays closed, the design does what it claims: two narrowly published services, not two merged LANs.

Ngaahi tokoni fekau'aki