Skip to content
Nebula logoNebula logoNebula Docs

Using unsafe routes for access beyond overlay hosts

Hosts on a Nebula network may need to reach devices that cannot run Nebula, such as printers, physical access control systems, or proprietary devices. If such devices are on a subnet reachable by one of the Nebula hosts, that host can act as a subnet gateway, forwarding traffic from other Nebula hosts to the subnet devices.

This guide explains how to configure Nebula’s unsafe_routes feature to route traffic through the subnet gateway to the devices that cannot run Nebula.

NEBULA OVERLAY NETWORK ░ SUBNET (HOME LAN)
░
┌──────────────┐ ░
│ Roaming Host │────┐ ░
└──────────────┘ │ ░
│ ░
┌────────────┐ │ ░
│ Lighthouse │────┤ ░
└────────────┘ │ ░
│ ░
(nebula1) │ ░
┌───┴────────────┐
│ Subnet Gateway │
└───────────┬────┘
░ │ (eth0)
░ │
░ │ ┌─────────┐
░ └────│ Printer │
░ └─────────┘

Read the Quick Start guide to learn how to create a basic Nebula overlay network.

To complete this guide, you will need the following:

  1. A working basic Nebula overlay network with a lighthouse and at least two hosts:
    • A roaming host running Linux, macOS, or Windows that needs access to the subnet devices
    • The subnet gateway running Linux, macOS, or Windows that will forward traffic from the overlay network to the subnet using unsafe_routes
      (This guide only covers configuring a Linux host as the subnet gateway)
  2. CA with the nebula-cert binary able to sign host certificates with additional metadata required for unsafe_routes
  3. The ca.key and ca.crt files for the Nebula overlay network
    • In ca.crt, unsafeNetworks must be either not specified or list the required unsafe routes
  4. Root access to the roaming host
  5. Root access to the subnet gateway

Check your ca.crt to ensure its unsafeNetworks (formerly -subnets) is either not specified or lists the subnets you want to access. In the directory containing your CA cert, run:

Terminal window
nebula-cert print -path ca.crt

If unsafeNetworks is set and does not include the required subnets, you will have to regenerate all certificates from scratch.

Here's an example of a CA generated without specifying unsafeNetworks:

Terminal window
# For more focused output, the -json flag is added and the result is piped to jq
nebula-cert print -json -path ca.crt | jq .details
{
"groups": null,
"isCa": true,
"issuer": "",
"name": "BK Labs",
"networks": null,
"notAfter": "2027-09-23T10:24:58+08:00",
"notBefore": "2026-09-23T10:24:58+08:00",
"unsafeNetworks": null
}

To illustrate how unsafe_routes works in practice, this guide uses an example scenario with the hostnames, IP addresses, and subnets provided below.

This is the overlay network with hosts running Nebula. Let's choose a common overlay network subnet 10.0.0.0/24 with the IP range 10.0.0.1 – 10.0.0.254.

The nebula-roaming host in this example has Internet access but is not on the home LAN.

Overlay host IP Overlay hostname Description
10.0.0.1 lighthouse Nebula lighthouse
10.0.0.5 subnet-gateway Overlay host routing traffic between overlay and subnet
10.0.0.11 nebula-roaming Roaming host that needs access to the printer via subnet-gateway

This is the subnet on which a roaming host needs to access devices. Let's choose a common LAN subnet 192.168.1.0/24 with the IP range 192.168.1.1 – 192.168.1.254.

Subnet device IP Overlay hostname Description
192.168.1.5 subnet-gateway Overlay host routing traffic to subnet devices
192.168.1.25 (none) Printer that cannot run Nebula

With your Nebula overlay network and subnet up and running, add unsafe routes following the steps below.

On the subnet gateway:

On a client host:

Using the setup in Example network, this guide will show you how to configure the roaming host (nebula-roaming, 10.0.0.11) to route traffic through the subnet gateway (subnet-gateway, 10.0.0.5 and 192.168.1.5) to reach the printer on the home LAN from anywhere.

Step 1. Subnet gateway: Add unsafeNetworks to the certificate

Section titled “Step 1. Subnet gateway: Add unsafeNetworks to the certificate”

On your CA host, go to the directory containing your ca.crt and ca.key, then sign a new cert for the subnet gateway with subnets you want to route:

Terminal window
nebula-cert sign -name 'subnet-gateway' -networks '10.0.0.5/24' -unsafe-networks '192.168.1.0/24'

Inspect the new certificate to confirm that unsafeNetworks is now correctly set to the home LAN subnet.

nebula-cert print -json -path subnet-gateway.crt | jq .details
{
"groups": null,
"isCa": false,
"issuer": "57903a07e52a8f5464636aeccb1942560324dcd1f6c4f0457d77b00372b5d9f2",
"name": "subnet-gateway",
"networks": [
"10.0.0.5/24"
],
"notAfter": "2027-09-23T10:24:57+08:00",
"notBefore": "2026-09-29T11:24:07+08:00",
"unsafeNetworks": [
"192.168.1.0/24"
]
}

Next, copy the new host cert and key over to the subnet gateway:

Terminal window
# Replace user with your ssh username
scp subnet-gateway.crt subnet-gateway.key user@10.0.0.5:~/

After copying these files, login to your subnet gateway host and replace your existing cert and key files (commonly stored in /etc/nebula) with the updated files. It's also a good practice to make sure that the key file is only readable by root. If you change existing file paths for the cert and key, don't forget to update them in your Nebula config.yml file.

Step 2. Subnet gateway: Configure Nebula firewall

Section titled “Step 2. Subnet gateway: Configure Nebula firewall”

Besides adding the subnet to the cert's unsafeNetworks field, you need to add separate firewall rules on your subnet gateway to regulate inbound traffic to your subnet from other overlay hosts. For this, add an inbound firewall rule with local_cidr for subnet devices and ports that you need to reach.

To access the printer on the home LAN, you need to add an inbound firewall rule to allow TCP port 631 on 192.168.1.0/24 (depending on your printer, you may need to include additional ports). On the subnet gateway, edit the Nebula configuration file (commonly located at /etc/nebula/config.yml) and add the following to existing firewall rules:

firewall:
inbound:
- port: 631
host: any
proto: tcp
local_cidr: 192.168.1.25/32

However, the easiest way to configure the firewall is to allow any port or protocol. In this case, on the subnet gateway, add the following to existing firewall rules:

firewall:
inbound:
- port: any
host: any
proto: any
local_cidr: 192.168.1.0/24

At this point we're finished configuring the Nebula-specific components on the subnet gateway. If Nebula is running, it's a good idea to restart it to validate that the configuration file, cert, and key are set up correctly.

Terminal window
$ sudo nebula -config /etc/nebula/config.yml
# To improve readability, each output line has been redacted as follows:
# time=2026-09-29T04:04:06.476Z level=INFO msg="..." redacted to msg="..."
msg="Firewall rule added" firewallRule="map[caName: caSha: cidr: direction:outgoing endPort:0 groups:[] host:any localCidr: proto:0 startPort:0]"
msg="Firewall rule added" firewallRule="map[caName: caSha: cidr: direction:incoming endPort:0 groups:[] host:any localCidr: proto:0 startPort:0]"
msg="Firewall rule added" firewallRule="map[caName: caSha: cidr: direction:incoming endPort:0 groups:[] host:any localCidr:192.168.1.0/24 proto:0 startPort:0]"
msg="Firewall started" firewallHashes=SHA:bec58c30ae0bd0463ff56a5fe9a75a21dc0253a44e3dcbe00e0d70dfb2fbe782,FNV:1666660931
msg=listening addr=[::]:4242
msg="Main HostMap created" preferredRanges=[]
msg="punchy enabled"
msg="Loaded send_recv_error config" sendRecvError=always
msg="Loaded accept_recv_error config" acceptRecvError=always
msg="Nebula interface is active" interface=nebula1 networks=[10.0.0.5/24] build=1.11.2 udpAddr=[::]:4242 boringcrypto=false
msg="Handshake message sent" vpnAddrs=[10.0.0.1] localIndex=4215839296 remoteIndex=0 udpAddrs=[<lighthouse-public-ip>:4242] initiatorIndex=4215839296 handshake="map[stage:1 style:ix_psk0]"
msg="Handshake message received" vpnAddrs=[10.0.0.1] from=<lighthouse-public-ip>:4242 certName=lighthouse certVersion=2 fingerprint=<...> issuer=<...> initiatorIndex=4215839296 responderIndex=1986196114 handshake="map[stage:2 style:ix_psk0]"

If you see a Handshake message sent to your lighthouse followed by a corresponding received message after a similar set of lines shown above you are good to go.

You can either leave Nebula running in the background or stop it while completing the next step on the subnet gateway.

Step 3. Subnet gateway: Configure traffic routing

Section titled “Step 3. Subnet gateway: Configure traffic routing”

Now, you need to configure traffic routing on Linux. First, allow packet forwarding between network interfaces. Then, configure the firewall to do packet routing and NAT.

Typically, packet forwarding is not enabled by default, and the respective kernel parameter is set to 0:

Terminal window
$ sudo sysctl net.ipv4.ip_forward
net.ipv4.ip_forward = 0

You can update this variable at runtime as follows:

Terminal window
$ sudo sysctl -w net.ipv4.ip_forward=1
net.ipv4.ip_forward = 1

Now that packet forwarding is enabled, you need to add a few firewall rules. First, the subnet gateway needs to route packets between Nebula and local network interfaces. Second, it needs to do network address translation (NAT).

Below you can find generic steps to configure the following:

These steps assume ufw is already active (sudo ufw status shows Status: active). If it isn't, allow SSH and Nebula's UDP port first (for example, sudo ufw allow OpenSSH and sudo ufw allow 4242/udp), then run sudo ufw enable. Enabling ufw without these rules can lock you out of the host.

First, allow packet routing between the Nebula interface and the local network interface:
(Replace network interfaces (nebula1, eth0) and subnets with your actual ones)

Terminal window
sudo ufw route allow in on nebula1 out on eth0 from 10.0.0.0/24 to 192.168.1.0/24
sudo ufw reload

Confirm the new rule is active:

Terminal window
$ sudo ufw status
Status: active
To Action From
-- ------ ----
192.168.1.0/24 on eth0 ALLOW FWD 10.0.0.0/24 on nebula1

Next, enable NAT to masquerade outgoing packets.

Open /etc/ufw/before.rules and add the NAT configuration block at the top of the file (before any *filter block):

Terminal window
# NAT table rules
*nat
:POSTROUTING ACCEPT [0:0]
# flush all existing POSTROUTING rules in the nat table to avoid duplicates on reload
-F POSTROUTING
# 10.0.0.0/24 is the Nebula network, 192.168.1.0/24 is the route CIDR
-A POSTROUTING -s 10.0.0.0/24 -d 192.168.1.0/24 -j MASQUERADE
# don't delete the 'COMMIT' line or these nat table rules won't be processed
COMMIT

Reload UFW to apply the NAT configuration:

Terminal window
sudo ufw reload

Make sure the nftables package is installed (sudo apt install nftables on Debian and Ubuntu) and the /etc/nftables/ directory exists (sudo mkdir -p /etc/nftables).

Create a new nftables configuration (e.g., /etc/nftables/nebula.conf). Configure the packet forwarding chain and then the NAT postrouting chain:
(Replace network interfaces (nebula1, eth0) and subnets with your actual ones)

Terminal window
table ip nebula_routing
delete table ip nebula_routing
table ip nebula_routing {
chain forward {
type filter hook forward priority filter; policy accept;
iifname nebula1 oifname eth0 ip saddr 10.0.0.0/24 ip daddr 192.168.1.0/24 counter accept
iifname eth0 oifname nebula1 ip saddr 192.168.1.0/24 ip daddr 10.0.0.0/24 ct state established,related counter accept
}
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
ip saddr 10.0.0.0/24 ip daddr 192.168.1.0/24 counter masquerade
}
}

To load the rules, run:

Terminal window
sudo nft -f /etc/nftables/nebula.conf

To verify that the rules have taken effect, you can run:

$ sudo nft list table nebula_routing
table ip nebula_routing {
chain forward {
type filter hook forward priority filter; policy accept;
iifname "nebula1" oifname "eth0" ip saddr 10.0.0.0/24 ip daddr 192.168.1.0/24 counter packets 6 bytes 504 accept
iifname "eth0" oifname "nebula1" ip saddr 192.168.1.0/24 ip daddr 10.0.0.0/24 ct state established,related counter packets 6 bytes 504 accept
}
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
ip saddr 10.0.0.0/24 ip daddr 192.168.1.0/24 counter packets 2 bytes 168 masquerade
}
}

First, allow packet routing between the Nebula interface and the local network interface:
(Replace network interfaces (nebula1, eth0) and subnets with your actual ones)

Terminal window
sudo iptables -I FORWARD 1 -i nebula1 -o eth0 -s 10.0.0.0/24 -d 192.168.1.0/24 -j ACCEPT
sudo iptables -I FORWARD 2 -i eth0 -o nebula1 -s 192.168.1.0/24 -d 10.0.0.0/24 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT

Next, add the NAT rule to masquerade outgoing traffic:

Terminal window
sudo iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -d 192.168.1.0/24 -j MASQUERADE

Once complete, confirm the forwarding rules are active:

Terminal window
$ sudo iptables -L FORWARD -v -n
Chain FORWARD (policy DROP 135 packets, 10404 bytes)
pkts bytes target prot opt in out source destination
524 76K ACCEPT 0 -- nebula1 eth0 10.0.0.0/24 192.168.1.0/24
1034 1469K ACCEPT 0 -- eth0 nebula1 192.168.1.0/24 10.0.0.0/24 ctstate RELATED,ESTABLISHED

Then verify the NAT rule:

Terminal window
$ sudo iptables -t nat -L POSTROUTING -v -n
Chain POSTROUTING (policy ACCEPT 0 packets, 0 bytes)
pkts bytes target prot opt in out source destination
0 0 MASQUERADE 0 -- * * 10.0.0.0/24 192.168.1.0/24

You may see additional rules listed depending on your host and if other services (such as Docker) have modified iptables.

Step 4. Roaming host: Configure traffic routing

Section titled “Step 4. Roaming host: Configure traffic routing”

The subnet gateway host is set up. We are almost there!

The final step is to configure the overlay network hosts to use unsafe_routes to route the traffic destined for the subnet devices through the subnet gateway.

On nebula-roaming, make sure you have the following in the tun config section:

tun:
drop_local_broadcast: true
drop_multicast: true
unsafe_routes:
- route: 192.168.1.0/24
via: 10.0.0.5

Run Nebula with the new configuration:

Terminal window
$ sudo nebula -config /etc/nebula/config.yml
# To improve readability, each output line has been redacted as follows:
# time=2026-09-29T04:04:06.476Z level=INFO msg="..." redacted to msg="..."
msg="Firewall rule added" firewallRule="map[caName: caSha: cidr: direction:outgoing endPort:0 groups:[] host:any localCidr: proto:0 startPort:0]"
msg="Firewall rule added" firewallRule="map[caName: caSha: cidr: direction:incoming endPort:0 groups:[] host:any localCidr: proto:0 startPort:0]"
msg="Firewall started" firewallHashes=SHA:498215dec4e5687a2353f51c10838c113bd1af35ef72b8e8c9f536986ada5417,FNV:2782948616
msg=listening addr=[::]:4242
msg="Main HostMap created" preferredRanges=[]
msg="punchy enabled"
msg="Loaded send_recv_error config" sendRecvError=always
msg="Loaded accept_recv_error config" acceptRecvError=always
msg="Nebula interface is active" interface=nebula1 networks=[10.0.0.11/24] build=1.11.2 udpAddr=[::]:4242 boringcrypto=false
msg="Added route" route=192.168.1.0/24
msg="Handshake message sent" vpnAddrs=[10.0.0.1] localIndex=2523598458 remoteIndex=0 udpAddrs=[<lighthouse-public-ip>:4242] initiatorIndex=2523598458 handshake="map[stage:1 style:ix_psk0]"
msg="Handshake message received" vpnAddrs=[10.0.0.1] from=<lighthouse-public-ip>:4242 certName=lighthouse certVersion=2 fingerprint=<...> issuer=<...> initiatorIndex=2523598458 responderIndex=1344867621 handshake="map[stage:2 style:ix_psk0]" durationNs=59967190 sentCachedPackets=1

As you can see in msg="Added route", the unsafe route is being used.

Now you should be able to successfully ping any host on the subnet from your roaming host and access subnet devices:

  • Ping working? Great! Go ahead and add that printer, map file shares, and SSH to any host on the home LAN.
  • Ping not working? If both your roaming host and subnet gateway are behind NAT, they might have issues pinging each other. In this case, try using punchy or relay. For details, see NAT traversal and firewalls.

If the subnet gateway uses ufw, ping alone doesn't confirm that forwarding works: ufw's default rules forward ICMP echo even without a ufw route allow rule. To check the routing rules, also connect to a TCP port on a subnet device, for example the printer's IPP port with nc -zv 192.168.1.25 631.

Add the same unsafe_routes section to other overlay hosts that need access to subnet devices.

When you are running Nebula with unsafe_routes the host's OS will forward all traffic destined for the unsafe route to Nebula. This might cause a local IP conflict if your underlay network happens to use the same IP range as the subnet specified in your unsafe_routes configuration.

After you've successfully completed the steps above, you can also access your unsafe routes from a device running Mobile Nebula on Android or iOS. For this, launch the app and configure it as follows:

  1. Tap the site you want to configure
  2. Tap Configuration → Advanced → Unsafe routes
  3. Tap Add a new route and enter the same route and via values that you used in Step 4
  4. Tap Save

If connected, disconnect from the site. Now, connect again and try to access one of the hosts only accessible using unsafe_routes.