$linuxjunkies
>

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.

IntermediateUbuntuDebianFedoraArch12 min readUpdated June 7, 2026

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> 4242 from 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 tun and that the running user has CAP_NET_ADMIN. In containers, you may need --device /dev/net/tun.
  • Peers can reach lighthouse but not each other: Enable punchy.respond: true on all nodes behind NAT. If both peers are behind symmetric NAT, you may need a relay — Nebula 1.6+ supports relay nodes for this case.

Nebula vs. Tailscale vs. WireGuard

FeatureNebulaTailscaleWireGuard
Control planeSelf-hosted CA + lighthousesHosted (tailscale.com) or HeadscaleNone (manual) or 3rd-party
Auth modelX.509-style certs, groupsSSO / identity providerPublic keys only
NAT traversalBuilt-in UDP hole punchingBuilt-in (DERP relay fallback)Manual or via wg-quick + stunnel
Firewall policyPer-node, cert-group-awareACL via admin consoleNone native; use nftables/iptables
Kernel dependencyUserspace (TUN)Userspace or kernel WGKernel module (fast) or wireguard-go
Best forSelf-hosted, large multi-site meshesQuick team VPN, BYODSite-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.

tested on:Ubuntu 24.04Fedora 40Debian 12Arch rolling

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