Skip to content
Search

Nebula Overlay Networking

lastmod 2026-10-27created 2021-09-2558 edits

A scalable overlay networking tool with a focus on performance, simplicity and security

Nebula is an overlay networking tool designed to be fast, secure, and scalable. Connect any number of hosts with on-demand, encrypted tunnels that work across any IP networks and without opening firewall ports.

Features

  • A tool that connects large numbers of nodes spread across multiple clouds/locations.
  • Developed to be simple while focusing on performance and security.
  • Slack initially used IPSec, then developed its own and has been using it for 2 years.
  • A mutually authenticated P2P software-defined network based on the Noise Protocol Framework.

Cloud services support their own security groups, but these are tied to each service and region, and connecting across multiple services is not possible. A solution that merges encryption, security groups, certificates, tunneling, etc. into one to solve this.

  • Supports encrypted connections between hosts
  • Service-agnostic - cloud/data center/personal laptop, doesn't matter
  • High-level traffic filtering
  • Provides strong identity
  • As fast as possible
  • Network changes can be tested

Components of a Nebula network

Lighthouse

In Nebula, a Lighthouse is a Nebula host that keeps track of all the other Nebula hosts and helps them find each other within the Nebula network.

At least one host in the overlay network must be a Lighthouse. Lighthouses discover routes to each other and assist with NAT traversal.

Certificate Authority (CA)

In its simplest form, a Nebula Certificate Authority (CA) consists of two files: the CA certificate and the associated private key.

The CA certificate is distributed to and trusted by every host on the network.

The CA private key should not be distributed, and can be kept offline when it is not being used to add hosts to the Nebula network.

Hosts

A Nebula host is simply a single node in the network (e.g. a server, laptop, phone, tablet).

The Certificate Authority is used to sign the key for each host added to the Nebula network.

The host certificate contains the name, IP address, group memberships, and various other details about the host.

Individual hosts cannot modify their own certificate, because modifying it invalidates the certificate. This lets us trust that a host cannot impersonate another host within the Nebula network. Each host has its own private key, which is used to verify that host's identity when Nebula tunnels are created.

Download and installation

sudo -s
## curl -L https://github.com/slackhq/nebula/releases/download/v1.9.1/nebula-linux-amd64.tar.gz | tar xzf - -C /usr/bin
curl -L https://github.com/slackhq/nebula/releases/download/v1.10.0/nebula-linux-amd64.tar.gz | tar xzf - -C /usr/bin

Installing on Windows

Register the service:

C:\ProgramData\nebula\nebula.exe -service install -config C:\ProgramData\nebula\config.yml

(Just in case) Add firewall rules

They are installed automatically, but use this if you ever need it

# 새로운 방화벽 규칙 추가
## Inbound UDP 규칙 (기본 포트 4242)
New-NetFirewallRule -DisplayName "Nebula Inbound UDP" `
    -Name "Nebula-In-UDP" `
    -Direction Inbound `
    -Protocol UDP `
    -LocalPort 4242 `
    -Action Allow `
    -Profile Any `
    -Description "Allow Nebula overlay network inbound traffic"

## Outbound UDP 규칙 (기본 포트 4242)
New-NetFirewallRule -DisplayName "Nebula Outbound UDP" `
    -Name "Nebula-Out-UDP" `
    -Direction Outbound `
    -Protocol UDP `
    -RemotePort 4242 `
    -Action Allow `
    -Profile Any `
    -Description "Allow Nebula overlay network outbound traffic"

## 방화벽 확인
Get-NetFirewallRule -Name Nebula-In-UDP
Get-NetFirewallRule -Name Nebula-Out-UDP

Creating your first Certificate Authority

Nebula provides built-in encryption of the CA private key since v1.7.0. Unless you plan to store the private key in encrypted storage (e.g.Ansible VaultorAWS Secrets Manager), using the built-in encryption is recommended.
To encrypt the Nebula private key, pass the ``-encrypt`` flag when creating the CA and you will be prompted for a password. Keep it safe. You will be prompted for it every time you sign a host using the encrypted CA key.
Nebula usesAES-256-GCMencryption together withArgon2idfor key derivation.
The defaultArgon2parameters are taken from the "FIRST RECOMMENDED" suggestion of the Argon2 RFC (1 iteration, 4 lanes of parallelism, 2GiB RAM).
To choose your own parameters, use the ``-argon-iterations``, ``-argon-parallelism``, and ``-argon-memory`` CLI flags.

Nebula uses a Certificate Authority to identify individual hosts within a Nebula network. The first step in creating your own Nebula network is to create a Certificate Authority for your organization. When you add a new host to the Nebula network, the Certificate Authority is used to generate credentials proving that the host is trusted by your organization.

./nebula-cert ca -name "Myorganization, Inc"

This creates files named ca.key and ca.cert in the current directory.

The ca.key file is the most sensitive file you will create, because it is the key used to sign certificates for individual Nebula hosts. Store this file in a safe place, preferably with strong encryption.

INFORMATION

By default, this CA is created with a 1-year expiry, and every certificate it signs is valid until 1 second before the CA expires. To ensure continued connectivity, set up an alert or calendar event to rotate the CA and certificates before then!

To set a custom period for which the CA is valid, pass the -duration XXhXXmXXs flag. For example, -duration 17531h creates a CA valid for just under 2 years.

Building a Nebula network

Establishing a Lighthouse

Nebula Lighthouses let hosts find each other anywhere in the world. A Lighthouse is the only host in a Nebula network whose IP address must not change.

Running a Lighthouse requires very few compute resources, and you can easily use the cheapest option offered by a cloud hosting provider.

If you are not sure which provider to use, many of us have used $5/month DigitalOcean droplets as Lighthouses.

After launching the instance, make sure Nebula UDP traffic (default port udp/4242) can reach the instance over the internet and is not blocked by an inbound firewall.

If you use iptables:

-A INPUT -p udp -m state --state NEW -m udp --dport 4242 -j ACCEPT

Creating Keys and Certificates

Here we assume three hosts: lighthouse1, laptop, and server.

You can name hosts any way you like, including with an FQDN.

You also need to choose a Nebula IP address for each host when creating its certificate. In this example, we create a Nebula network using the subnet 192.168.100.x/24 and assign each host an IP address within this subnet.

This example also shows Nebula Groups, which can later be used to define Traffic Rules in the Nebula Network.

./nebula-cert sign -name "lighthouse1" -ip "192.168.100.1/24"
./nebula-cert sign -name "laptop" -ip "192.168.100.5/24" -groups "laptop,ssh"
./nebula-cert sign -name "server" -ip "192.168.100.9/24" -groups "servers"

Configuring Nebula

INFORMATION

The following examples use the address 198.51.100.1 as the routable IP of a hypothetical lighthouse. When creating your Nebula config files, you must substitute the routable IP of your own lighthouse.

Download Nebula's Example Config and copy it to config-lighthouse.yaml for the Lighthouse and config.yaml for all other hosts:

curl -o config.yml https://raw.githubusercontent.com/slackhq/nebula/master/examples/config.yml
cp config.yml config-lighthouse.yaml
cp config.yml config.yaml

Don't create the files from scratch; copy the example and modify only the parts described below.

Lighthouse configuration (config-lighthouse.yaml)

For the Lighthouse, just make sure am_lighthouse: true is set.

Typically, lighthouses have no entries in static_host_map, because all hosts report themselves to the lighthouse. If you use multiple lighthouses, they generally do not need to know about each other.

static_host_map:

lighthouse:
  am_lighthouse: true

Host configuration (config.yaml)

For individual hosts, make sure the Lighthouse is correctly defined in the static_host_map section and that lighthouse > hosts has been added.

static_host_map:
  '192.168.100.1': ['198.51.100.1:4242']

lighthouse:
  am_lighthouse: false
  interval: 60
  # `hosts`는 이 노드가 보고하고 쿼리해야 하는 Lighthouse 호스트 목록입니다.
  # 중요 1: Lighthouse 노드에서는 비어 있어야 합니다.
  # 중요 2: 이는 LIGHTHOUSES의 실제 라우팅 가능한 IP가 아니라 LIGHTHOUSES의 Nebula IP 여야 합니다.
  hosts:
    - '192.168.100.1'

Firewall configuration

The example config file allows all outbound traffic but only specific connections to each Nebula host. To allow traffic to and from hosts in the network, you need to modify these settings.

firewall:
  outbound:
    # Allow all outbound traffic from this node
    - port: any
      proto: any
      host: any

  inbound:
    # Allow icmp between any nebula hosts
    - port: any
      proto: icmp
      host: any

If traffic is sent over the Nebula Network and you use iptable, you have to configure the firewall on both sides.... what a damn hassle..

By default, with no settings, In/Out bound is DROP:

firewall:
  outbound_action: drop
  inbound_action: drop

If you want to open port 6443/tcp:

firewall:
  inbound:
    - port: 6443
      proto: tcp
      host: any

And if needed: iptables -A INPUT -p tcp -m state --state NEW -m tcp --dport 6443 -j ACCEPT

Running Nebula

Once the config, certificate, and key files have been created, the final step is to copy the appropriate files to each host and start the Nebula network.

Lighthouse

  1. Copy the following files to the Lighthouse: the nebula binary, config-lighthouse.yaml, ca.crt, lighthouse1.crt, and lighthouse1.key Never copy the ca.key file!!!
  2. SSH to your lighthouse.
  3. Create a directory named /etc/nebula on your lighthouse host.
  4. Move the config, certificate, and key files into the appropriate directory.

mv config-lighthouse.yaml /etc/nebula/config.yaml mv ca.crt /etc/nebula/ca.crt mv lighthouse.crt /etc/nebula/host.crt mv lighthouse.key /etc/nebula/host.key

</syntaxhighlight>

  1. Start Nebula: ./nebula -config /etc/nebula/config.yaml

Hosts

In this example, we configure the host named server created above. Substitute the appropriate file names.

  1. Copy the following files to the Host: the nebula binary, config.yaml, ca.crt, server.crt, and server.key Never copy the ca.key file!!!
  2. SSH to the host you've named server.
  3. Create a directory named /etc/nebula on the Nebula host.
  4. Move the configuration, certificates, and key into the appropriate directory.
    • Note that the example config assumes the host certificate and key are named host.crt and host.key, so you will need to rename some files when moving them into the appropriate directory.

mv config.yaml /etc/nebula/config.yaml mv ca.crt /etc/nebula/ca.crt mv server.crt /etc/nebula/host.crt mv server.key /etc/nebula/host.key

</syntaxhighlight>

  1. Start Nebula: ./nebula -config /etc/nebula/config.yaml

Adjusting file permissions

cd /etc/nebula
sudo chmod 600 ca.crt config.yml host.crt host.key
sudo chown root:root ca.crt config.yml host.crt host.key

Verifying it all works

You can now ping other hosts running Nebula (assuming ICMP is allowed). To PING the example lighthouse, run:

ping 192.168.100.1

Relay

INFORMATION

Bottom line: if both hosts trying to communicate are hidden behind a Symmetric NAT, you'll end up having to use this method.

A relay host forwards traffic between two peers. This can be useful when two nodes have trouble communicating with each other directly (e.g. some NATs can make establishing a direct connection between two nodes difficult.)

To act as a relay for other hosts, am_relay: true must be set (default is false).

To use relays, the host must have use_relays: true set (default is true).

Any host can be a relay. It doesn't have to be a Lighthouse. However, like lighthouses, relay nodes must be deployed with a public internet IP and firewall rules that allow Nebula's UDP traffic inbound.

The list of Nebula IPs listed in relays are other hosts that can act as relays and can be used to relay packets to the current host. That is, the IPs in this list must have am_relay: true set in their configs; otherwise they will reject relay requests.

This lets a host specify relays that are "close" to it. For example, if you have some Nebula hosts in a private AWS VPC, you can set up a dedicated relay host that enables connections to peers in that VPC.

You cannot relay through a relay. A host configured as a relay (am_relay: true) cannot specify other hosts as relays in its own config.

Example relay settings:

relay:
  relays:
    - 192.168.100.1
  am_relay: true
  use_relays: true

INFORMATION

The relays addresses are required. Also, the host trying to connect through the relay must have relay settings configured.

iptables

If you have set up an iptables firewall, don't use 0 for the listen.port value in Nebula's config.yml. (Proceeding on the assumption that the documented default of 4242 is used)

Then configure as follows:

# Allow all traffic on Nebula interface
# Nebula 인터페이스의 모든 트래픽 허용
-A INPUT -i nebula1 -j ACCEPT
-A OUTPUT -o nebula1 -j ACCEPT

# Allow Nebula UDP port (lighthouse communication)
# Nebula UDP 포트 허용 (lighthouse와의 통신)
-A INPUT -i wlp4s0 -p udp --dport 4242 -j ACCEPT
-A OUTPUT -o wlp4s0 -p udp --dport 4242 -j ACCEPT

Security issues

How it works

Nebula is a so-called mesh network, in which nodes communicate with each other directly without going through a central gateway. It works by cleverly exploiting properties of networking.

A typical networking flow that happens thousands of times a day looks like this (ridiculously simplified).

How_Nebula_works_-_flow.png

When establishing a connection, the client opens a port for the server's response. Firewalls and routers between the client and server are aware of this process and make sure traffic going back to the client is allowed.

Nebula uses this open port to cleverly get through firewalls and connect clients directly to each other. Special nodes, called Lighthouses in Nebula terminology, are used to let clients discover each other. These special nodes are the only network members that need a publicly reachable IP address. A client first connects to the Lighthouse and opens a local port. However, instead of responding directly, the Lighthouse tells the target client to respond to the newly opened port. The process is repeated from the other side as well.

How_Nebula_works_-_discovery.png

This is a greatly simplified diagram. In reality, a lot more spoofing and trickery goes on for smooth NAT traversal.

Authentication

One big advantage over WireGuard is the way Nebula handles authentication. WireGuard expects the administrator to integrate a new client's public key into the configuration of existing nodes. This is simple in theory, but the approach doesn't scale well, and automatically rolling out new nodes requires additional tooling. Tailscale and co. are a good way to manage this complexity, but at the cost of introducing additional services and abstractions.

Nebula solves this by leveraging public key infrastructure (PKI) concepts. A central certificate authority (CA) signs the clients' certificates. These certificates are used not only for administrative purposes but also for cryptographic purposes. The signature includes not only the node's IP address but also other metadata such as allowed subnets and groups.

Whenever a new client registers with the Lighthouse, it verifies the certificate against the certificate authority and remembers the IP address stored in the certificate. Since the CA itself does not change, the Lighthouse can verify clients created after it was configured, and does not need to adapt to such changes.

My applied settings

See Blackhole#Nebula.

Nebula vs Tailscale

Running as a systemd service

Save the file to /etc/systemd/system/nebula.service:

[Unit]
Description=Nebula VPN
After=network-online.target systemd-resolved.service
Wants=network-online.target systemd-resolved.service

[Service]
ExecStart=/usr/bin/nebula -config /etc/nebula/config.yml
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

Load the service file / enable at boot / start:

sudo systemctl daemon-reload
sudo systemctl enable nebula
sudo systemctl start nebula

config.yml examples

Modify the official sample as the base.

curl -o config.yml https://raw.githubusercontent.com/slackhq/nebula/master/examples/config.yml

The downloaded original looks like this:

# This is the nebula example configuration file. You must edit, at a minimum, the static_host_map, lighthouse, and firewall sections
# Some options in this file are HUPable, including the pki section. (A HUP will reload credentials from disk without affecting existing tunnels)

# PKI defines the location of credentials for this node. Each of these can also be inlined by using the yaml ": |" syntax.
pki:
  # The CAs that are accepted by this node. Must contain one or more certificates created by 'nebula-cert ca'
  ca: /etc/nebula/ca.crt
  cert: /etc/nebula/host.crt
  key: /etc/nebula/host.key
  # blocklist is a list of certificate fingerprints that we will refuse to talk to
  #blocklist:
  #  - c99d4e650533b92061b09918e838a5a0a6aaee21eed1d12fd937682865936c72
  # disconnect_invalid is a toggle to force a client to be disconnected if the certificate is expired or invalid.
  #disconnect_invalid: true

# The static host map defines a set of hosts with fixed IP addresses on the internet (or any network).
# A host can have multiple fixed IP addresses defined here, and nebula will try each when establishing a tunnel.
# The syntax is:
#   "{nebula ip}": ["{routable ip/dns name}:{routable port}"]
# Example, if your lighthouse has the nebula IP of 192.168.100.1 and has the real ip address of 100.64.22.11 and runs on port 4242:
static_host_map:
  "192.168.100.1": ["100.64.22.11:4242"]

# The static_map config stanza can be used to configure how the static_host_map behaves.
#static_map:
  # cadence determines how frequently DNS is re-queried for updated IP addresses when a static_host_map entry contains
  # a DNS name.
  #cadence: 30s

  # network determines the type of IP addresses to ask the DNS server for. The default is "ip4" because nodes typically
  # do not know their public IPv4 address. Connecting to the Lighthouse via IPv4 allows the Lighthouse to detect the
  # public address. Other valid options are "ip6" and "ip" (returns both.)
  #network: ip4

  # lookup_timeout is the DNS query timeout.
  #lookup_timeout: 250ms

lighthouse:
  # am_lighthouse is used to enable lighthouse functionality for a node. This should ONLY be true on nodes
  # you have configured to be lighthouses in your network
  am_lighthouse: false
  # serve_dns optionally starts a dns listener that responds to various queries and can even be
  # delegated to for resolution
  #serve_dns: false
  #dns:
    # The DNS host defines the IP to bind the dns listener to. This also allows binding to the nebula node IP.
    #host: 0.0.0.0
    #port: 53
  # interval is the number of seconds between updates from this node to a lighthouse.
  # during updates, a node sends information about its current IP addresses to each node.
  interval: 60
  # hosts is a list of lighthouse hosts this node should report to and query from
  # IMPORTANT: THIS SHOULD BE EMPTY ON LIGHTHOUSE NODES
  # IMPORTANT2: THIS SHOULD BE LIGHTHOUSES' NEBULA IPs, NOT LIGHTHOUSES' REAL ROUTABLE IPs
  hosts:
    - "192.168.100.1"

  # remote_allow_list allows you to control ip ranges that this node will
  # consider when handshaking to another node. By default, any remote IPs are
  # allowed. You can provide CIDRs here with `true` to allow and `false` to
  # deny. The most specific CIDR rule applies to each remote. If all rules are
  # "allow", the default will be "deny", and vice-versa. If both "allow" and
  # "deny" IPv4 rules are present, then you MUST set a rule for "0.0.0.0/0" as
  # the default. Similarly if both "allow" and "deny" IPv6 rules are present,
  # then you MUST set a rule for "::/0" as the default.
  #remote_allow_list:
    # Example to block IPs from this subnet from being used for remote IPs.
    #"172.16.0.0/12": false

    # A more complicated example, allow public IPs but only private IPs from a specific subnet
    #"0.0.0.0/0": true
    #"10.0.0.0/8": false
    #"10.42.42.0/24": true

  # EXPERIMENTAL: This option may change or disappear in the future.
  # Optionally allows the definition of remote_allow_list blocks
  # specific to an inside VPN IP CIDR.
  #remote_allow_ranges:
    # This rule would only allow only private IPs for this VPN range
    #"10.42.42.0/24":
      #"192.168.0.0/16": true

  # local_allow_list allows you to filter which local IP addresses we advertise
  # to the lighthouses. This uses the same logic as `remote_allow_list`, but
  # additionally, you can specify an `interfaces` map of regular expressions
  # to match against interface names. The regexp must match the entire name.
  # All interface rules must be either true or false (and the default will be
  # the inverse). CIDR rules are matched after interface name rules.
  # Default is all local IP addresses.
  #local_allow_list:
    # Example to block tun0 and all docker interfaces.
    #interfaces:
      #tun0: false
      #'docker.*': false
    # Example to only advertise this subnet to the lighthouse.
    #"10.0.0.0/8": true

  # advertise_addrs are routable addresses that will be included along with discovered addresses to report to the
  # lighthouse, the format is "ip:port". `port` can be `0`, in which case the actual listening port will be used in its
  # place, useful if `listen.port` is set to 0.
  # This option is mainly useful when there are static ip addresses the host can be reached at that nebula can not
  # typically discover on its own. Examples being port forwarding or multiple paths to the internet.
  #advertise_addrs:
    #- "1.1.1.1:4242"
    #- "1.2.3.4:0" # port will be replaced with the real listening port

  # EXPERIMENTAL: This option may change or disappear in the future.
  # This setting allows us to "guess" what the remote might be for a host
  # while we wait for the lighthouse response.
  #calculated_remotes:
    # For any Nebula IPs in 10.0.10.0/24, this will apply the mask and add
    # the calculated IP as an initial remote (while we wait for the response
    # from the lighthouse). Both CIDRs must have the same mask size.
    # For example, Nebula IP 10.0.10.123 will have a calculated remote of
    # 192.168.1.123
    #10.0.10.0/24:
      #- mask: 192.168.1.0/24
      #  port: 4242

# Port Nebula will be listening on. The default here is 4242. For a lighthouse node, the port should be defined,
# however using port 0 will dynamically assign a port and is recommended for roaming nodes.
listen:
  # To listen on both any ipv4 and ipv6 use "::"
  host: 0.0.0.0
  port: 4242
  # Sets the max number of packets to pull from the kernel for each syscall (under systems that support recvmmsg)
  # default is 64, does not support reload
  #batch: 64
  # Configure socket buffers for the udp side (outside), leave unset to use the system defaults. Values will be doubled by the kernel
  # Default is net.core.rmem_default and net.core.wmem_default (/proc/sys/net/core/rmem_default and /proc/sys/net/core/rmem_default)
  # Maximum is limited by memory in the system, SO_RCVBUFFORCE and SO_SNDBUFFORCE is used to avoid having to raise the system wide
  # max, net.core.rmem_max and net.core.wmem_max
  #read_buffer: 10485760
  #write_buffer: 10485760
  # By default, Nebula replies to packets it has no tunnel for with a "recv_error" packet. This packet helps speed up reconnection
  # in the case that Nebula on either side did not shut down cleanly. This response can be abused as a way to discover if Nebula is running
  # on a host though. This option lets you configure if you want to send "recv_error" packets always, never, or only to private network remotes.
  # valid values: always, never, private
  # This setting is reloadable.
  #send_recv_error: always

# Routines is the number of thread pairs to run that consume from the tun and UDP queues.
# Currently, this defaults to 1 which means we have 1 tun queue reader and 1
# UDP queue reader. Setting this above one will set IFF_MULTI_QUEUE on the tun
# device and SO_REUSEPORT on the UDP socket to allow multiple queues.
# This option is only supported on Linux.
#routines: 1

punchy:
  # Continues to punch inbound/outbound at a regular interval to avoid expiration of firewall nat mappings
  punch: true

  # respond means that a node you are trying to reach will connect back out to you if your hole punching fails
  # this is extremely useful if one node is behind a difficult nat, such as a symmetric NAT
  # Default is false
  #respond: true

  # delays a punch response for misbehaving NATs, default is 1 second.
  #delay: 1s

  # set the delay before attempting punchy.respond. Default is 5 seconds. respond must be true to take effect.
  #respond_delay: 5s

# Cipher allows you to choose between the available ciphers for your network. Options are chachapoly or aes
# IMPORTANT: this value must be identical on ALL NODES/LIGHTHOUSES. We do not/will not support use of different ciphers simultaneously!
#cipher: aes

# Preferred ranges is used to define a hint about the local network ranges, which speeds up discovering the fastest
# path to a network adjacent nebula node.
# This setting is reloadable.
#preferred_ranges: ["172.16.0.0/24"]

# sshd can expose informational and administrative functions via ssh. This can expose informational and administrative
# functions, and allows manual tweaking of various network settings when debugging or testing.
#sshd:
  # Toggles the feature
  #enabled: true
  # Host and port to listen on, port 22 is not allowed for your safety
  #listen: 127.0.0.1:2222
  # A file containing the ssh host private key to use
  # A decent way to generate one: ssh-keygen -t ed25519 -f ssh_host_ed25519_key -N "" < /dev/null
  #host_key: ./ssh_host_ed25519_key
  # Authorized users and their public keys
  #authorized_users:
    #- user: steeeeve
      # keys can be an array of strings or single string
      #keys:
        #- "ssh public key string"
  # Trusted SSH CA public keys. These are the public keys of the CAs that are allowed to sign SSH keys for access.
  #trusted_cas:
    #- "ssh public key string"

# EXPERIMENTAL: relay support for networks that can't establish direct connections.
relay:
  # Relays are a list of Nebula IP's that peers can use to relay packets to me.
  # IPs in this list must have am_relay set to true in their configs, otherwise
  # they will reject relay requests.
  #relays:
    #- 192.168.100.1
    #- <other Nebula VPN IPs of hosts used as relays to access me>
  # Set am_relay to true to permit other hosts to list my IP in their relays config. Default false.
  am_relay: false
  # Set use_relays to false to prevent this instance from attempting to establish connections through relays.
  # default true
  use_relays: true

# Configure the private interface. Note: addr is baked into the nebula certificate
tun:
  # When tun is disabled, a lighthouse can be started without a local tun interface (and therefore without root)
  disabled: false
  # Name of the device. If not set, a default will be chosen by the OS.
  # For macOS: if set, must be in the form `utun[0-9]+`.
  # For NetBSD: Required to be set, must be in the form `tun[0-9]+`
  dev: nebula1
  # Toggles forwarding of local broadcast packets, the address of which depends on the ip/mask encoded in pki.cert
  drop_local_broadcast: false
  # Toggles forwarding of multicast packets
  drop_multicast: false
  # Sets the transmit queue length, if you notice lots of transmit drops on the tun it may help to raise this number. Default is 500
  tx_queue: 500
  # Default MTU for every packet, safe setting is (and the default) 1300 for internet based traffic
  mtu: 1300

  # Route based MTU overrides, you have known vpn ip paths that can support larger MTUs you can increase/decrease them here
  routes:
    #- mtu: 8800
    #  route: 10.0.0.0/16

  # Unsafe routes allows you to route traffic over nebula to non-nebula nodes
  # Unsafe routes should be avoided unless you have hosts/services that cannot run nebula
  # NOTE: The nebula certificate of the "via" node *MUST* have the "route" defined as a subnet in its certificate
  # `mtu`: will default to tun mtu if this option is not specified
  # `metric`: will default to 0 if this option is not specified
  # `install`: will default to true, controls whether this route is installed in the systems routing table.
  # This setting is reloadable.
  unsafe_routes:
    #- route: 172.16.1.0/24
    #  via: 192.168.100.99
    #  mtu: 1300
    #  metric: 100
    #  install: true

  # On linux only, set to true to manage unsafe routes directly on the system route table with gateway routes instead of
  # in nebula configuration files. Default false, not reloadable.
  #use_system_route_table: false

# TODO
# Configure logging level
logging:
  # panic, fatal, error, warning, info, or debug. Default is info and is reloadable.
  #NOTE: Debug mode can log remotely controlled/untrusted data which can quickly fill a disk in some
  # scenarios. Debug logging is also CPU intensive and will decrease performance overall.
  # Only enable debug logging while actively investigating an issue.
  level: info
  # json or text formats currently available. Default is text
  format: text
  # Disable timestamp logging. useful when output is redirected to logging system that already adds timestamps. Default is false
  #disable_timestamp: true
  # timestamp format is specified in Go time format, see:
  #     https://golang.org/pkg/time/#pkg-constants
  # default when `format: json`: "2006-01-02T15:04:05Z07:00" (RFC3339)
  # default when `format: text`:
  #     when TTY attached: seconds since beginning of execution
  #     otherwise: "2006-01-02T15:04:05Z07:00" (RFC3339)
  # As an example, to log as RFC3339 with millisecond precision, set to:
  #timestamp_format: "2006-01-02T15:04:05.000Z07:00"

#stats:
  #type: graphite
  #prefix: nebula
  #protocol: tcp
  #host: 127.0.0.1:9999
  #interval: 10s

  #type: prometheus
  #listen: 127.0.0.1:8080
  #path: /metrics
  #namespace: prometheusns
  #subsystem: nebula
  #interval: 10s

  # enables counter metrics for meta packets
  #   e.g.: `messages.tx.handshake`
  # NOTE: `message.{tx,rx}.recv_error` is always emitted
  #message_metrics: false

  # enables detailed counter metrics for lighthouse packets
  #   e.g.: `lighthouse.rx.HostQuery`
  #lighthouse_metrics: false

# Handshake Manager Settings
#handshakes:
  # Handshakes are sent to all known addresses at each interval with a linear backoff,
  # Wait try_interval after the 1st attempt, 2 * try_interval after the 2nd, etc, until the handshake is older than timeout
  # A 100ms interval with the default 10 retries will give a handshake 5.5 seconds to resolve before timing out
  #try_interval: 100ms
  #retries: 20

  # query_buffer is the size of the buffer channel for querying lighthouses
  #query_buffer: 64

  # trigger_buffer is the size of the buffer channel for quickly sending handshakes
  # after receiving the response for lighthouse queries
  #trigger_buffer: 64


# Nebula security group configuration
firewall:
  # Action to take when a packet is not allowed by the firewall rules.
  # Can be one of:
  #   `drop` (default): silently drop the packet.
  #   `reject`: send a reject reply.
  #     - For TCP, this will be a RST "Connection Reset" packet.
  #     - For other protocols, this will be an ICMP port unreachable packet.
  outbound_action: drop
  inbound_action: drop

  # Controls the default value for local_cidr. Default is true, will be deprecated after v1.9 and defaulted to false.
  # This setting only affects nebula hosts with subnets encoded in their certificate. A nebula host acting as an
  # unsafe router with `default_local_cidr_any: true` will expose their unsafe routes to every inbound rule regardless
  # of the actual destination for the packet. Setting this to false requires each inbound rule to contain a `local_cidr`
  # if the intention is to allow traffic to flow to an unsafe route.
  #default_local_cidr_any: false

  conntrack:
    tcp_timeout: 12m
    udp_timeout: 3m
    default_timeout: 10m

  # The firewall is default deny. There is no way to write a deny rule.
  # Rules are comprised of a protocol, port, and one or more of host, group, or CIDR
  # Logical evaluation is roughly: port AND proto AND (ca_sha OR ca_name) AND (host OR group OR groups OR cidr) AND (local cidr)
  # - port: Takes `0` or `any` as any, a single number `80`, a range `200-901`, or `fragment` to match second and further fragments of fragmented packets (since there is no port available).
  #   code: same as port but makes more sense when talking about ICMP, TODO: this is not currently implemented in a way that works, use `any`
  #   proto: `any`, `tcp`, `udp`, or `icmp`
  #   host: `any` or a literal hostname, ie `test-host`
  #   group: `any` or a literal group name, ie `default-group`
  #   groups: Same as group but accepts a list of values. Multiple values are AND'd together and a certificate would have to contain all groups to pass
  #   cidr: a remote CIDR, `0.0.0.0/0` is any.
  #   local_cidr: a local CIDR, `0.0.0.0/0` is any. This could be used to filter destinations when using unsafe_routes.
  #      Default is `any` unless the certificate contains subnets and then the default is the ip issued in the certificate
  #      if `default_local_cidr_any` is false, otherwise its `any`.
  #   ca_name: An issuing CA name
  #   ca_sha: An issuing CA shasum

  outbound:
    # Allow all outbound traffic from this node
    - port: any
      proto: any
      host: any

  inbound:
    # Allow icmp between any nebula hosts
    - port: any
      proto: icmp
      host: any

    # Allow tcp/443 from any host with BOTH laptop and home group
    - port: 443
      proto: tcp
      groups:
        - laptop
        - home

    # Expose a subnet (unsafe route) to hosts with the group remote_client
    # This example assume you have a subnet of 192.168.100.1/24 or larger encoded in the certificate
    - port: 8080
      proto: tcp
      group: remote_client
      local_cidr: 192.168.100.1/24

Lighthouse config.yml

pki:
  ca: /etc/nebula/ca.crt
  cert: /etc/nebula/host.crt
  key: /etc/nebula/host.key

static_host_map:

lighthouse:
  am_lighthouse: true
  interval: 60

listen:
  host: 0.0.0.0
  port: 4242

punchy:
  punch: true

cipher: aes

relay:
  #relays:
    #- 192.168.100.1
    #- <other Nebula VPN IPs of hosts used as relays to access me>
  am_relay: true
  use_relays: true

tun:
  disabled: false
  dev: nebula1
  drop_local_broadcast: false
  drop_multicast: false
  tx_queue: 500
  mtu: 1300

logging:
  level: info
  format: text

firewall:
  outbound_action: drop
  inbound_action: drop

  conntrack:
    tcp_timeout: 12m
    udp_timeout: 3m
    default_timeout: 10m

  outbound:
    - port: any
      proto: any
      host: any

  inbound:
    - port: any
      proto: icmp
      host: any

Hosts config.yml

pki:
  ca: /etc/nebula/ca.crt
  cert: /etc/nebula/host.crt
  key: /etc/nebula/host.key

static_host_map:
  '192.168.100.1': ['api.your.run:4242']

lighthouse:
  am_lighthouse: false
  interval: 60
  hosts:
    - "192.168.100.1"

listen:
  # To listen on both any ipv4 and ipv6 use "::"
  host: 0.0.0.0
  port: 0

punchy:
  punch: true

cipher: aes

relay:
  am_relay: false
  use_relays: true

tun:
  disabled: false
  dev: nebula1
  drop_local_broadcast: false
  drop_multicast: false
  tx_queue: 500
  mtu: 1300

logging:
  level: info
  format: text

firewall:
  outbound_action: drop
  inbound_action: drop

  conntrack:
    tcp_timeout: 12m
    udp_timeout: 3m
    default_timeout: 10m

  outbound:
    - port: any
      proto: any
      host: any

  inbound:
    - port: any
      proto: icmp
      host: any

    # 일부 서버에서 HTTPS 호스팅을 위해 필요.
    - port: 443
      proto: tcp
      host: any

    # kubectl 에서 사용하는 6443 포트
    - port: 6443
      proto: tcp
      host: any

Troubleshooting

Cannot connect after restarting the service on a specific Node

Given the following:

  • NodeA - my current local PC
  • NodeB - server PC

If you changed NodeB's settings and restarted the service, and suddenly ping from NodeA to NodeB stops working... restart NodeA too!!!!! Presumably the cause is that UDP is not connection-oriented. <- needs confirmation. Anyway, restart the service on all related nodes.

create Wintun interface failed, create TUN device failed: Error creating interface: Access is denied

PS C:\ProgramData\nebula> .\nebula.exe -config .\config.yml time="2025-12-07T12:10:04+09:00" level=info msg="Firewall rule added" firewallRule="map[caName: caSha: direction:outgoing endPort:0 groups:[] host:any ip: localIp: proto:0 startPort:0]" time="2025-12-07T12:10:04+09:00" level=info msg="Firewall rule added" firewallRule="map[caName: caSha: direction:incoming endPort:0 groups:[] host:any ip: localIp: proto:1 startPort:0]" time="2025-12-07T12:10:04+09:00" level=info msg="Firewall rule added" firewallRule="map[caName: caSha: direction:incoming endPort:48888 groups:[] host:any ip: localIp: proto:6 startPort:48888]" time="2025-12-07T12:10:04+09:00" level=info msg="Firewall started" firewallHashes="SHA:879e15158ed6c75529330465079d421033576cf2b5f016a246f859b9cb737d3e,FNV:1848223226" 2025/12/07 12:10:04 Failed to create private namespace: 액세스가 거부되었습니다. (Code 0x00000005) 2025/12/07 12:10:04 Failed to take device installation mutex: 액세스가 거부되었습니다. (Code 0x00000005) 2025/12/07 12:10:04 Failed to create private namespace: 액세스가 거부되었습니다. (Code 0x00000005) 2025/12/07 12:10:04 Failed to take device installation mutex: 액세스가 거부되었습니다. (Code 0x00000005) time="2025-12-07T12:10:04+09:00" level=error msg="Failed to get a tun/tap device" error="create Wintun interface failed, create TUN device failed: Error creating interface: Access is denied."

Run it with administrator privileges.

server misbehaving

Looking at the Nebula server log:

journalctl -u nebula.service -n 50

The output is as follows:

Feb 05 07:58:40 zer0-hp-zbook nebula[1989]: time="2026-02-05T07:58:40+09:00" level=info msg="Firewall rule added" firewallRule="map[caName: caSha: direction:outgoing endPort:0 groups:[] host:any ip: proto:0 startPort:0]"
Feb 05 07:58:40 zer0-hp-zbook nebula[1989]: time="2026-02-05T07:58:40+09:00" level=info msg="Firewall rule added" firewallRule="map[caName: caSha: direction:incoming endPort:0 groups:[] host:any ip: proto:1 startPort:0]"
Feb 05 07:58:40 zer0-hp-zbook nebula[1989]: time="2026-02-05T07:58:40+09:00" level=info msg="Firewall rule added" firewallRule="map[caName: caSha: direction:incoming endPort:22 groups:[] host:any ip: proto:6 startPort:22]"
Feb 05 07:58:40 zer0-hp-zbook nebula[1989]: time="2026-02-05T07:58:40+09:00" level=info msg="Firewall rule added" firewallRule="map[caName: caSha: direction:incoming endPort:48888 groups:[] host:any ip: proto:6 startPort:48888]"
Feb 05 07:58:40 zer0-hp-zbook nebula[1989]: time="2026-02-05T07:58:40+09:00" level=info msg="Firewall started" firewallHash=e76a23d282b34774ab83db2d92ba97746c7e4196b17d5f748f25aec679b96133
Feb 05 07:58:40 zer0-hp-zbook nebula[1989]: time="2026-02-05T07:58:40+09:00" level=info msg="Main HostMap created" network=192.168.88.210/24 preferredRanges="[]"
Feb 05 07:58:40 zer0-hp-zbook nebula[1989]: time="2026-02-05T07:58:40+09:00" level=info msg="UDP hole punching enabled"
Feb 05 07:58:40 zer0-hp-zbook nebula[1989]: time="2026-02-05T07:58:40+09:00" level=error msg="Static host address could not be parsed" entry=1 error="lookup api.yourdomain.com on 127.0.0.53:53: server misbehaving" vpnIp=192.168.88.1
Feb 05 07:58:40 zer0-hp-zbook systemd[1]: nebula.service: Main process exited, code=exited, status=1/FAILURE
Feb 05 07:58:40 zer0-hp-zbook systemd[1]: nebula.service: Failed with result 'exit-code'.
Feb 05 07:58:40 zer0-hp-zbook systemd[1]: nebula.service: Scheduled restart job, restart counter is at 5.
Feb 05 07:58:40 zer0-hp-zbook systemd[1]: nebula.service: Start request repeated too quickly.
Feb 05 07:58:40 zer0-hp-zbook systemd[1]: nebula.service: Failed with result 'exit-code'.
Feb 05 07:58:40 zer0-hp-zbook systemd[1]: Failed to start nebula.service - Nebula VPN.

It's a DNS resolution failure. Check DNS#How to specify the DNS address directly. If you started it as a daemon, DNS lookup is failing because systemd-resolved is not ready yet at boot time, or Nebula starts before the network is up. After 5 restarts it hits the rate limit and fails completely.

Add a network dependency to the systemd service:

[Unit]
After=network-online.target systemd-resolved.service
Wants=network-online.target systemd-resolved.service

Adding RestartSec=5 as well also fixes the problem of restarts happening too quickly:

[Service]
RestartSec=5

nebula certificate for this host is expired

"Could net load client cert" is printed along with the error "nebula certificate for this host is expired". This happens because the certificate is not valid.

In most cases, this happens because the time differs between the signing machine (CA/lighthouse side) and the machine where the certificate is installed.

Compare the times.

## 서버의 시간 확인
date -u
timedatectl

## 인증서의 실제 UTC 값 확인 / check cert validity window in UTC
nebula-cert print -path /etc/nebula/host.crt -json | jq '.details | {notBefore, notAfter}'

The certificate contents are printed as follows:

{
  "notBefore": "2026-08-05T17:24:17+09:00",
  "notAfter": "2034-05-28T13:41:45+09:00"
}

The machine's (server's) time must fall within the validity period.

See also

Favorite site

author Zer0 sitemap