Connecting to the network
Yggdrasil connects machines through an encrypted IPv6 network carried over their existing connections. Each machine has its own key and Yggdrasil address. Peers provide connections into the network, while services on the machine are reached through the tunnel interface, named ygg0 by default.
Private key
The module expects an age-encrypted file containing the private key in PEM format. Generate a new key for a new node, or reuse the existing key to retain a node's Yggdrasil address.
Creating a new key
Open a shell with Yggdrasil available:
nix-shell -p yggdrasil
Inside that shell, generate a configuration and pass it directly to the key exporter:
yggdrasil -genconf | yggdrasil -useconf -exportkey > private-key.pem
This creates a new private key in private-key.pem. It does not require an existing configuration file. The generated configuration passes through the pipe without being saved separately.
Reusing an existing key
If your old configuration embeds a hexadecimal PrivateKey, export that key with yggdrasil:
yggdrasil -useconffile /path/to/your/old/yggdrasil.conf -exportkey > private-key.pem
Replace the path with an existing Yggdrasil configuration file. This command reads that file; it does not create it. If your old setup already stores the key in a separate PEM file through PrivateKeyPath, use that PEM file directly. yggdrasilctl administers a running daemon and does not provide the key export command.
Encrypting the key
Encrypt the PEM file for the machine's SSH host key:
age -R /etc/ssh/ssh_host_ed25519_key.pub -o private-key.age private-key.pem
Place private-key.age beside the host module that enables Yggdrasil. The plaintext key stays outside the configuration. The Secrets chapter explains recipients and activation-time decryption.
Service declaration
This host module connects to three public peers and exposes TCP port 22 over Yggdrasil:
{ pkgs, ... }:
{
environment.systemPackages = [ pkgs.yggdrasil ];
services.nftables.enable = true;
services.yggdrasil = {
enable = true;
privateKeyFile = ./private-key.age;
peers = [
"tls://5.252.226.123:1338?key=0000000087ee9949eeab56bd430ee8f324cad55abf3993ed9b9be63ce693e18a"
"tls://5.45.109.122:1338?key=000000d80a2d7b3126ea65c8c08fc751088c491a5cdd47eff11c86fa1e4644ae"
"tls://168.119.169.163:443?key=0000006149970f245e6cec43664bce203f2514b60a153e194f31e2b229a1339d"
];
expose.ssh.ports = [ 22 ];
};
}
The peer addresses are explicit host configuration. Each key parameter pins the expected public key of that peer. Copland decrypts the private key during activation and starts Yggdrasil through OpenRC. Changing the encrypted key restarts the service during a switch.
expose.ssh permits connections to an SSH server that is already enabled and listening on IPv6. It does not start that server. Installing pkgs.yggdrasil also makes the command-line tools available in your shell.
Direct peers and listeners
Two machines can connect directly as well as through public peers. For alice, add bob to the existing peer list and enable a listener:
{
services.yggdrasil = {
peers = [ "tls://<bob-public-ipv4>:9002" ];
listen = [ "tls://0.0.0.0:9002" ];
};
services.nftables.allowedTCPPorts = [ 9002 ];
}
On bob, use the reverse connection:
{
services.yggdrasil = {
peers = [ "tls://<alice-public-ipv4>:9002" ];
listen = [ "tls://0.0.0.0:9002" ];
};
services.nftables.allowedTCPPorts = [ 9002 ];
}
Replace the bracketed placeholders with the machines' public IPv4 addresses. These snippets extend the enabled service and firewall from the previous example. Peer lists from separate imported modules are combined; when editing one declaration, append the direct URI to its existing list.
The listener accepts incoming router connections on TCP port 9002. This port belongs to the underlying network connection. expose separately controls access to applications through the Yggdrasil tunnel.
Hostnames
Use hosts to give known Yggdrasil addresses local names:
{
services.yggdrasil.hosts = {
alice = "<alice-yggdrasil-ipv6>";
bob = "<bob-yggdrasil-ipv6>";
};
}
Replace each placeholder with that node's actual Yggdrasil IPv6 address. Copland adds alice.ygg and bob.ygg to the local hosts file. Multiple names may refer to the same address. This mapping provides names for connections and access rules; it does not establish peer connections or assign addresses to nodes.
Service access
Access from named hosts
With the host mapping above, this declaration allows SSH only from alice and HTTPS from any Yggdrasil participant:
{
services.nftables.enable = true;
services.yggdrasil.expose = {
ssh = {
ports = [ 22 ];
allowFrom = [ "alice" ];
};
web = {
ports = [ 443 ];
allowFrom = [ ];
};
};
}
Each entry requires at least one TCP port. allowFrom contains names from services.yggdrasil.hosts, and an unknown name causes configuration evaluation to fail. Omitting allowFrom has the same effect as an empty list: every Yggdrasil participant may connect to those ports.
When expose contains entries, the firewall allows their combined TCP access and drops other incoming tunnel traffic, apart from established connections and the ICMP traffic it normally permits. A global port allowance does not bypass these restrictions. An unrestricted entry for a port does allow everyone through that port, even if another entry restricts it to named hosts. Non-empty expose requires services.nftables.enable = true.
Trusting the tunnel
An empty expose trusts the entire tunnel interface:
{
services.yggdrasil.expose = { };
}
This is the default. With the firewall enabled, all incoming traffic on ygg0 is accepted. To use this mode after configuring individual entries, remove those entries; an empty attribute set in another module does not erase them.
Local peer discovery
Multicast lets nearby machines discover each other without static peer addresses. This complete example enables discovery on all interfaces and uses a fixed TCP port for incoming peer connections:
{
services.nftables.enable = true;
services.yggdrasil = {
enable = true;
privateKeyFile = ./private-key.age;
multicastInterfaces = [ ".*" ];
multicastPort = 9002;
expose.ssh.ports = [ 22 ];
};
}
multicastInterfaces contains regular expressions matched against interface names. For one interface, replace ".*" with an expression such as "^enp1s0$" using your actual interface name. The default empty list disables discovery.
For discovery, Copland permits UDP port 9001 from IPv6 link-local addresses to multicast address ff02::114. A fixed multicastPort also permits incoming TCP connections from IPv6 link-local addresses. These firewall allowances apply across interfaces; the expressions select where Yggdrasil itself discovers peers and listens.
The default multicastPort = 0 selects random TCP ports and adds no TCP allowance. Use a fixed port for incoming peering behind the firewall, as described in the Yggdrasil peering guide. The discovery and peering ports connect routers; expose.ssh in this example allows application traffic on TCP port 22 through the resulting tunnel.
Local administration
Add an existing local user to adminUsers to let that user access the admin socket:
{ pkgs, ... }:
{
environment.systemPackages = [ pkgs.yggdrasil ];
services.yggdrasil.adminUsers = [ "<local-user>" ];
}
Replace <local-user> with an account declared on this machine. After applying the configuration, start a new login session to pick up the group membership. You can then inspect the node and its peer connections:
yggdrasilctl -endpoint=unix:///run/yggdrasil/yggdrasil.sock getSelf
yggdrasilctl -endpoint=unix:///run/yggdrasil/yggdrasil.sock getPeers
rc-service yggdrasil status
getSelf reports the node's Yggdrasil address, and getPeers shows its router connections. Admin access permits control of the daemon through its local socket; it is separate from the remote service access configured with expose.