Build a Mesh VPN with Nebula
Build a fully self-hosted mesh VPN with Nebula: create a CA, sign node certs, configure lighthouses, enforce group-based firewall rules, and run as a systemd service.
Before you start
- ▸At least one VPS or cloud VM with a static public IP and UDP port 4242 open for the lighthouse
- ▸Root or sudo access on every machine joining the mesh
- ▸The tun kernel module available (lsmod | grep tun); load it with modprobe tun if missing
- ▸curl and tar available to download the Nebula release archive
Nebula is a scalable, peer-to-peer mesh VPN developed at Slack and open-sourced in 2019. Unlike WireGuard (which you configure manually or rely on a control plane) or Tailscale (which uses a hosted coordinator), Nebula gives you a fully self-hosted, certificate-authority-based overlay network. Every node authenticates with signed certificates, punches through NAT automatically, and routes directly to peers — no traffic hairpins through a central server. This guide walks through building a working Nebula mesh: creating a CA, signing node certificates, configuring a lighthouse, enforcing firewall rules, and getting nodes talking.
How Nebula Works (30-Second Model)
Nebula has three distinct roles:
- CA (Certificate Authority): An offline key pair you use to sign node certificates. Never runs as a daemon.
- Lighthouse: A publicly reachable node (VPS, cloud VM) that helps peers discover each other's public IPs. It participates in the mesh like any other node but also answers discovery queries.
- Node: Any machine you want on the overlay — your laptops, servers, containers.
Every node gets a private overlay IP (e.g., 10.42.0.0/16) plus a signed certificate. The CA's public key is distributed to every node; that's the only trust anchor.
Prerequisites and Downloads
You need at least one publicly reachable host for the lighthouse (UDP port 4242 open), plus the machines you want to mesh. Download the latest Nebula release from github.com/slackhq/nebula/releases. As of 2024 the current stable series is 1.9.x.
# On each machine (adjust arch: amd64, arm64, etc.)
curl -LO https://github.com/slackhq/nebula/releases/latest/download/nebula-linux-amd64.tar.gz
tar xzf nebula-linux-amd64.tar.gz
sudo install -m 755 nebula nebula-cert /usr/local/bin/
Verify the install:
nebula-cert --version
nebula --version
Step 1: Create the Certificate Authority
Do this on a secure, preferably offline machine. The CA private key signs all node certs — protect it like an SSH root key.
mkdir -p ~/nebula-ca && cd ~/nebula-ca
nebula-cert ca --name "MyMesh CA" --duration 87600h
This produces ca.crt and ca.key. The --duration flag sets validity (87600h = 10 years). Store ca.key offline; you only need it to sign new certificates.
Step 2: Sign Node Certificates
Plan your overlay subnet first. Using 10.42.0.0/16 gives you 65 534 addresses. Assign each node a static overlay IP.
# Lighthouse — overlay IP 10.42.0.1
nebula-cert sign --ca-crt ca.crt --ca-key ca.key \
--name lighthouse01 \
--ip 10.42.0.1/16
# App server
nebula-cert sign --ca-crt ca.crt --ca-key ca.key \
--name appserver01 \
--ip 10.42.0.10/16
# Developer laptop
nebula-cert sign --ca-crt ca.crt --ca-key ca.key \
--name devlaptop \
--ip 10.42.0.50/16 \
--groups dev,laptops
The --groups flag tags a certificate; firewall rules later reference these group names. Each sign invocation produces <name>.crt and <name>.key. Copy each node's cert, its private key, and ca.crt to the corresponding machine — and nowhere else.
# Example: copy to lighthouse (adjust path/user)
scp ca.crt lighthouse01.crt lighthouse01.key user@your-lighthouse:/etc/nebula/
Step 3: Configure the Lighthouse
Create /etc/nebula/config.yml on the lighthouse host. The lighthouse needs a static public IP (or a DNS name) and UDP 4242 open in your cloud firewall.
sudo mkdir -p /etc/nebula
cat | sudo tee /etc/nebula/config.yml << 'EOF'
pki:
ca: /etc/nebula/ca.crt
cert: /etc/nebula/lighthouse01.crt
key: /etc/nebula/lighthouse01.key
lighthouse:
am_lighthouse: true
interval: 60
listen:
host: 0.0.0.0
port: 4242
punchy:
punch: true
firewall:
outbound:
- port: any
proto: any
host: any
inbound:
- port: any
proto: icmp
host: any
- port: 22
proto: tcp
host: any
EOF
Open UDP 4242 in your host firewall. On a system using firewalld (Fedora/RHEL/Rocky):
sudo firewall-cmd --permanent --add-port=4242/udp
sudo firewall-cmd --reload
On Ubuntu/Debian with ufw:
sudo ufw allow 4242/udp
Step 4: Configure Regular Nodes
Each non-lighthouse node needs to know the lighthouse's real public IP. Replace 203.0.113.10 with your actual lighthouse public IP.
cat | sudo tee /etc/nebula/config.yml << 'EOF'
pki:
ca: /etc/nebula/ca.crt
cert: /etc/nebula/appserver01.crt
key: /etc/nebula/appserver01.key
lighthouse:
am_lighthouse: false
interval: 60
hosts:
- "10.42.0.1" # overlay IP of the lighthouse
static_host_map:
"10.42.0.1": ["203.0.113.10:4242"] # real public IP:port
listen:
host: 0.0.0.0
port: 0 # 0 = random ephemeral port
punchy:
punch: true
respond: true
firewall:
outbound:
- port: any
proto: any
host: any
inbound:
- port: any
proto: icmp
host: any
- port: 22
proto: tcp
host: any
- port: 443
proto: tcp
host: any
EOF
Adjust the inbound rules to match what this specific node actually serves. The firewall section is enforced by Nebula itself, independent of the OS firewall — traffic that doesn't match is dropped before it hits the kernel network stack.
Step 5: Run Nebula as a systemd Service
This works identically across Debian/Ubuntu, Fedora/RHEL, and Arch.
cat | sudo tee /etc/systemd/system/nebula.service << 'EOF'
[Unit]
Description=Nebula mesh VPN
Wants=network-online.target
After=network-online.target
[Service]
ExecStart=/usr/local/bin/nebula -config /etc/nebula/config.yml
Restart=on-failure
RestartSec=5s
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadOnlyPaths=/etc/nebula
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now nebula
sudo systemctl status nebula
The service drops to non-root after acquiring the TUN device capability. If you're on a very locked-down system, verify the tun kernel module is loaded: modprobe tun.
Step 6: Firewall Rules with Groups
Nebula's built-in firewall understands certificate groups. Here's a more realistic inbound policy on an app server that allows all nodes in the dev group to reach port 8080, but blocks everyone else:
inbound:
- port: any
proto: icmp
host: any
- port: 8080
proto: tcp
groups:
- dev
- port: 22
proto: tcp
ca_sha: "" # empty = any valid CA-signed cert
host: any
Rules are evaluated top-to-bottom; first match wins. The groups list is an AND condition — a certificate must possess all listed groups to match. Use group (singular) for OR semantics in older releases; 1.6+ supports both.
Verification
Once nebula is running on at least two nodes, verify connectivity with ICMP:
# From appserver01, ping the lighthouse's overlay IP
ping -c 4 10.42.0.1
# Check the nebula TUN interface is up
ip addr show nebula1
# View current peer table (run as root)
nebula-cert print --path /etc/nebula/appserver01.crt
You can also send SIGUSR1 to the nebula process to dump connection state to the journal:
sudo systemctl kill -s USR1 nebula
sudo journalctl -u nebula -n 50
Troubleshooting
- No handshake, connection stuck: Confirm UDP 4242 is reachable from the internet on the lighthouse. Test with
nc -u <lighthouse-public-ip> 4242from a remote host. - Certificate errors on startup: Verify all three files (ca.crt, node.crt, node.key) are on the node and that the cert was signed by that ca.crt. Run
nebula-cert verify --ca ca.crt --crt node.crt. - TUN device not created: Check
modprobe tunand that the running user hasCAP_NET_ADMIN. In containers, you may need--device /dev/net/tun. - Peers can reach lighthouse but not each other: Enable
punchy.respond: trueon all nodes behind NAT. If both peers are behind symmetric NAT, you may need a relay — Nebula 1.6+ supportsrelaynodes for this case.
Nebula vs. Tailscale vs. WireGuard
| Feature | Nebula | Tailscale | WireGuard |
|---|---|---|---|
| Control plane | Self-hosted CA + lighthouses | Hosted (tailscale.com) or Headscale | None (manual) or 3rd-party |
| Auth model | X.509-style certs, groups | SSO / identity provider | Public keys only |
| NAT traversal | Built-in UDP hole punching | Built-in (DERP relay fallback) | Manual or via wg-quick + stunnel |
| Firewall policy | Per-node, cert-group-aware | ACL via admin console | None native; use nftables/iptables |
| Kernel dependency | Userspace (TUN) | Userspace or kernel WG | Kernel module (fast) or wireguard-go |
| Best for | Self-hosted, large multi-site meshes | Quick team VPN, BYOD | Site-to-site, performance-critical |
Nebula's sweet spot is environments where you control the full stack, need cert-based group policies, and want zero dependency on external services. WireGuard will generally outperform it on raw throughput because of the kernel data path. Tailscale is faster to set up but you're trusting their coordination server unless you self-host Headscale.
Frequently asked questions
- Can I have more than one lighthouse?
- Yes, and for production you should. Add multiple entries to static_host_map and lighthouse.hosts on each node. Nodes will query all configured lighthouses; redundancy improves reliability when one is unreachable.
- What happens if the lighthouse goes down after peers have connected?
- Existing peer-to-peer tunnels continue working because Nebula routes directly between nodes after initial discovery. The lighthouse is only needed to establish new connections or reconnect after a roam.
- How do I rotate or revoke a compromised node certificate?
- Issue a new certificate with a new key pair, distribute it to the node, and restart Nebula. There is no built-in CRL in current Nebula versions; revocation is handled by rotating the CA and re-signing all certs, or by removing the node from firewall allow-lists so it can connect but not reach anything.
- Does Nebula work inside Docker or Kubernetes?
- Yes. Run nebula in a privileged container or one with /dev/net/tun mounted and CAP_NET_ADMIN granted. In Kubernetes, a DaemonSet with hostNetwork: false and the tun device mounted is a common pattern for pod-level mesh membership.
- How does Nebula's performance compare to WireGuard?
- Nebula runs entirely in userspace over a TUN interface, so throughput is typically 300–800 Mbps on modern hardware versus WireGuard's kernel-path gigabit-plus speeds. For most mesh use cases (internal APIs, SSH, monitoring) the difference is irrelevant; for bulk data transfer between two fixed sites, WireGuard is the better tool.
Related guides
Common Linux Network Ports Reference
Learn Linux port ranges, read /etc/services, find what's listening with ss and nmap, and apply solid firewall rules to expose or block the right ports.
How to Configure a Static IP on Linux
Configure a static IP on Linux using Netplan, NetworkManager (nmcli), or systemd-networkd across Ubuntu, Fedora, Debian, and Arch with verified steps.
Expose a Service with Cloudflare Tunnel
Expose local services to the internet without port-forwarding using Cloudflare Tunnel. Install cloudflared, create a named tunnel, configure ingress rules, and run as a systemd service.
firewalld Zones and Rich Rules in Practice
Assign interfaces to firewalld zones, open services, write rich rules for source-based and rate-limited policies, and manage runtime vs permanent config.