You added a relay host, put its Nebula address in a config, and the two peers still will not talk. That happens because a relay path needs three separate statements to agree, and only one of them lives on the machine you probably edited. I have chased this twice: once where the forwarding host had never been given the relay role, and once where the settings were written on the wrong side of the connection. The sequence below checks the role, the advertisement and the permission in that order, and each step has a command that tells you whether to continue.

Map the three roles before you edit a config

Nebula gives every host a job, and the jobs are not inherited. A lighthouse answers where-is-this-peer questions so hosts can find each other. A relay forwards traffic between two peers that have no working direct path. An ordinary host does neither. The three settings that matter live on three different machines, which is why a client config that looks complete can leave both peers waiting.

# on the discovery host
lighthouse:
  am_lighthouse: true
  hosts: []

# on every other host, point at the discovery host's Nebula address
lighthouse:
  hosts:
    - 10.42.0.1

One symptom separates the two failure classes. If both peers list each other and the log shows handshake attempts that never complete, discovery is working and the data path is blocked by NAT or by a firewall. If a peer never learns the other address at all, the lighthouse or its static host map is the problem, and relay settings cannot repair that.

Confirm which config each host actually loaded before editing anything else. A unit that reads a different path than the file you edited is a common and embarrassing cause of "the change did nothing":

systemctl cat nebula | grep ExecStart
nebula -version
journalctl -u nebula -n 20 --no-pager

The version line matters on a mixed fleet. Relay handling, punching behavior and accepted config keys are not identical across releases, so do not assume an older build understands a section it was not written for. Align the hosts on the same minor series, then run nebula -test -config /etc/nebula/config.yml on each host and compare the output before you debug a transport the code does not implement.

Enable the forwarding role on the relay host

The relay role is one flag on the forwarding host, and it is separate from being a lighthouse. A host with discovery enabled and relay forwarding disabled is still only a discovery service. On the relay:

relay:
  am_relay: true

That flag declares intent; it does not create reachability. A relay has to be reachable from both peers, which in practice means a public address, inbound UDP on the Nebula port open in the host firewall and in any cloud security group, and no double NAT in front of it. Check the listening socket, then watch for the peer's packets arriving:

ss -lunp | grep nebula
tcpdump -ni any udp port 4242 and host 198.51.100.7

Two limits save time later. A relay cannot be reached through another relay, so a chain of relay names in a config is not a substitute for giving the forwarding host a usable path. And a relay forwards packets; it does not grant permission, so the destination's own inbound rules still decide whether the relayed flow is allowed. The role and its requirements are set out in the relay configuration documentation.

Advertise the relay from the destination, not the client

This is the reversal that eats the most hours. The relay list belongs on the host you are trying to reach, because it tells other peers which relays may be used to reach that host. Putting it on the client that starts the connection does nothing for the direction the client needs:

# on the DESTINATION host: relays others may use to reach me
relay:
  relays:
    - 10.42.0.5

Peers report that list along with their addresses when they check in with the lighthouse, so the advertisement propagates through discovery rather than through a direct exchange. If the destination cannot reach a lighthouse, its relay list never spreads and a correctly configured relay stays idle. That failure looks exactly like a broken relay and is not one.

Give the destination a bootstrap path in the same pass. The static host map ties a Nebula address to a public endpoint so the first handshake has somewhere to go, and it needs an entry for the relay if the destination is to accept relayed traffic from hosts it cannot reach directly:

static_host_map:
  "10.42.0.5": ["203.0.113.10:4242"]

Let the connecting peer use the relay

The initiating side needs its own permission. relay.use_relays is on by default, so the case worth hunting is an explicit false, often copied from a template written before the relay feature existed or left behind by an earlier attempt to force direct paths:

relay:
  use_relays: true

Permission on this side is not the only gate. The destination's inbound rules still apply to the relayed packet, and the source they match is the identity inside the tunnel rather than the relay's own address. When the relayed handshake completes and the application still fails, read the destination's log for the source it recorded before you rewrite a rule.

NAT traversal settings interact with all of this. punchy.punch helps two peers open a direct path, and when it succeeds there is no relay traffic to observe; that is success, not a missing relay. A pair that works intermittently usually means punching succeeds some of the time, so test that pair several times instead of drawing a conclusion from one attempt.

Verify with the pair that failed, then reload

Test the two hosts that actually failed, with the application they need. A successful ping from your workstation to the relay's own service proves nothing about forwarding between two other peers, and it is the most common false positive in this whole procedure.

journalctl -u nebula -f | grep -iE "relay|handshake"
ping -c 3 10.42.0.9

Reload instead of restarting, and confirm the running process picked up the file you edited. A restart tears down every tunnel and makes the next test harder to read, while a SIGHUP keeps the process and makes it re-read configuration:

sudo systemctl kill -s HUP nebula
# same signal when nebula runs in the foreground
kill -HUP $(pidof nebula)

systemctl reload only works when the local unit defines ExecReload, and the unit that ships with the project does not, so systemctl kill -s HUP is the step that succeeds on a stock install. Follow the reload with a journal read for the config path the process opened, so you know the new file was read rather than the one you assumed was loaded.

Relay settings and NAT punching both act on the path; neither acts on identity. If the pair still fails its handshake once am_relay, relays and use_relays are confirmed in the running config, check that both hosts present certificates issued for the names they are configured with and that the CA is the same on both sides. Keep the certificate and firewall configuration fixed while you change one transport setting at a time, because a certificate edit and a relay edit that land in the same test tell you nothing about which one moved the handshake. A name or CA mismatch fails in the same place as a relay that was never found, which is why this counter-check belongs in the same pass.

Then confirm each statement sits in the file it belongs to:

  • am_relay: true on the forwarding host, and that host reachable on UDP from both sides.
  • relays: naming that host's Nebula address on the destination.
  • use_relays: true on the peer that starts the connection.
  • Inbound rules on the destination still allowing the real source after the path changes.

Barruulee wal qabatan