Networking AWS

Yggdrasil 0.5.15 on AWS User Guide

| Product: Yggdrasil 0.5.15 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and verifying Yggdrasil 0.5.15 on Ubuntu 24.04 from cloudimg on Amazon EC2. It is one version of the cloudimg Hardened WireGuard VPN & VPN, Proxy and Remote Access AMIs listing on AWS Marketplace; you pick it as the software version when you subscribe and launch. The other VPN, proxy and remote access servers in the listing are described on the listing's guide hub.

Yggdrasil is an open-source, end-to-end encrypted IPv6 mesh network. Every node has an ed25519 key, and its permanent IPv6 address (inside 200::/7) is derived from that key, so the address cannot be spoofed. Nodes peer over TCP or TLS links and route for each other in a self-arranging tree, so any node can reach any other node of the mesh by its address, across NAT and across clouds. On this image the EC2 instance is a ready-to-use mesh node that your own machines peer with over TLS on port 9001.

Yggdrasil 0.5.15 is installed from the official release: cloudimg pins the checksums of the upstream release package and of the two binaries it installs (yggdrasil, the node, and yggdrasilctl, its admin tool). The build record and the source offer are in /usr/share/doc/yggdrasil/BUILD-PROVENANCE.txt.

What is on the image:

  • Yggdrasil 0.5.15 (yggdrasil.service): the mesh node, with its address on interface ygg0 and a TLS peering listener on port 9001/tcp
  • An identity of its own: the node key (and so the node's IPv6 address) and a peering password are generated on your instance's first boot; no two instances share them
  • Peering by password: only a node configured with this instance's password can peer with it
  • No public peers: the node joins no network until you add peers
  • A firewall for the mesh: traffic from the mesh never crosses your security group, so inbound traffic on ygg0 is dropped by default except ping (ICMPv6) and replies; you open ports with one command
  • Admin API for root only: yggdrasilctl talks to a local unix socket that only root can use
  • Helpers: yggdrasil-selftest (an end-to-end mesh test with a second node) and yggdrasil-mesh-firewall (the mesh firewall)

Default user: ubuntu · Architecture: x86_64 · Root volume: 8 GiB gp3

Security posture

  • The mesh bypasses the security group. Anything that can route to this node's 200::/7 address through the mesh (every peer, and on a public mesh anyone those peers route for) reaches the instance over ygg0 without crossing your AWS security group. The image therefore ships an nftables table (cloudimg-yggdrasil-mesh-firewall.service, which yggdrasil.service requires) that drops every packet arriving on ygg0 except ICMPv6 (ping, path MTU) and replies to connections this instance opened. SSH and every other service on the instance are unreachable from the mesh until you allow a port (Step 7).
  • No default key, no default password. The node key is the node's identity and address: a shared key would give every instance the same address and let anyone impersonate it. The image ships no configuration at all. First boot runs yggdrasil -genconf on your instance, generates a 32-character peering password, writes /etc/yggdrasil/yggdrasil.conf and /root/yggdrasil-credentials.txt (both readable only by root) and only then starts the node. The upstream package's own install scripts, which would generate a key at build time, are never run.
  • Peering by password. The listener is tls://0.0.0.0:9001?password=<your password>. Yggdrasil 0.5 binds the password into the link handshake, so a node without it cannot complete a peering. cloudimg tested this with a second node: without the password it was refused, with it the peering came up.
  • Off until you turn it on. No peers are configured (the node never contacts anyone until you add a peer), multicast peer discovery is off (VPCs carry no multicast, and discovery would open a link-local listener), node info privacy is on (the node does not tell the mesh its platform or version), and IP forwarding is off.
  • Admin API on a local socket, root only. AdminListen is unix:///var/run/yggdrasil/yggdrasil.sock, inside a runtime directory only root and the service account can enter. There is no admin TCP port. Run yggdrasilctl with sudo.
  • Least privilege. The node runs as the system user yggdrasil with one capability, CAP_NET_ADMIN (to create ygg0 and its routes; port 9001 needs no other), NoNewPrivileges, core dumps off, a read-only file system apart from its runtime directory, no access to home directories, and systemd's system-call and address-family filters. The key file stays root-only on disk; systemd hands the service a private copy at start.
  • No SSRF to instance credentials. yggdrasil.service carries an IPAddressDeny rule for the metadata addresses (169.254.169.254, fd00:ec2::254, 168.63.129.16), so no peer the node is told to dial can reach them; Yggdrasil itself also refuses to dial a link-local address such as 169.254.169.254. The mesh firewall drops anything forwarded to those addresses as well, and a mesh peer cannot route IPv4 (or the VPC) through the node at all. The AMI is registered IMDSv2 only.
  • Fails closed. yggdrasil.service starts only after first boot has written the ready marker, only with the mesh firewall loaded, and only after a check refuses a configuration without a real key, with a TCP admin socket, or with a different interface name. If first boot cannot finish, the node stays stopped and the next boot starts over with a new identity.
  • Per-instance identity. SSH host keys and the machine ID are generated fresh on every instance.

Prerequisites

  1. An AWS account and permission to launch EC2 instances
  2. An EC2 key pair in the Region you launch in
  3. A subnet with outbound internet access, and a public IP address (or Elastic IP) if machines outside AWS will peer with this node

Recommended instance type: m5.large. Yggdrasil is light; traffic you route through the mesh is what needs bandwidth.

Connecting to your instance

Version Operating system SSH user
Yggdrasil 0.5.15 on Ubuntu 24.04 Ubuntu 24.04 LTS ubuntu

Step 1: Launch the instance

Option A: AWS Marketplace console

  1. Open the cloudimg Hardened WireGuard VPN & VPN, Proxy and Remote Access AMIs listing on AWS Marketplace and choose View purchase options, then Subscribe. One subscription covers every version in the listing.
  2. Once the subscription is active, choose Launch your software.
  3. Choose One-click launch from AWS Marketplace (or Launch from EC2 Console for the full launch wizard). In Version, choose Yggdrasil 0.5.15 on Ubuntu 24.04, then choose your Region.
  4. Pick an instance type (m5.large recommended; the software charge is $0.04 per vCPU per hour) and your key pair.
  5. Security group: the listing's default group allows SSH (22) and the peering port 9001/tcp. Restrict SSH to your own IP address, and 9001 to your peers' addresses where you know them (the peering password already refuses any node that does not have it).
  6. Choose a subnet with internet access and a public IP, leave the metadata defaults (the image already requires IMDSv2), then choose Launch.

Option B: AWS CLI

Run this on your own workstation. It creates a security group that allows SSH from your management address and the peering port from your peers, then launches the instance. Replace the AMI ID with the one shown for your Region on the Marketplace launch page, and the VPC, subnet, key pair and address ranges with your own:

SG=$(aws ec2 create-security-group --group-name yggdrasil \
  --description "Yggdrasil mesh node" --vpc-id vpc-0123456789abcdef0 \
  --query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 22 --cidr <your-mgmt-cidr>
aws ec2 authorize-security-group-ingress --group-id "$SG" --protocol tcp --port 9001 --cidr <your-peer-cidr>
aws ec2 run-instances \
  --image-id ami-0123456789abcdef0 \
  --instance-type m5.large \
  --key-name my-key-pair \
  --security-group-ids "$SG" \
  --subnet-id subnet-0123456789abcdef0 \
  --associate-public-ip-address \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=yggdrasil}]'

You do not need --metadata-options: the AMI already makes IMDSv2 required.

Step 2: Connect and read your node details

Connect as ubuntu with the key pair you chose:

ssh -i ~/.ssh/my-key-pair.pem ubuntu@<public-ip>

First boot finishes within a minute of launch. It generates the node key and the peering password, writes the configuration, loads the mesh firewall and starts the node. The details are in /root/yggdrasil-credentials.txt, readable only by root. This command shows everything except the password:

sudo grep -v -e '^yggdrasil.peering_secret=' -e '^yggdrasil.peer_uri=' /root/yggdrasil-credentials.txt
# Yggdrasil 0.5.15 - generated on first boot by yggdrasil-firstboot.service. No other instance shares these.
# peering_secret admits a node to peer with this one; keep this file private. The node's private key is in
# /etc/yggdrasil/yggdrasil.conf (0600 root). To peer another machine, add peer_uri to its Peers list (see the guide):
#   https://www.cloudimg.co.uk/guides/yggdrasil-0-5-15-on-ubuntu-24-04-aws/

yggdrasil.ipv6_address=200:9c57:9796:62e9:4e02:fbaf:1743:404c
yggdrasil.ipv6_subnet=300:9c57:9796:62e9::/64
yggdrasil.public_key=b1d43434ce8b58fe8228745e5fd9f9b42dc5f9b4d22432b35c1fa160052d1351
yggdrasil.private_ipv4=172.31.92.59
yggdrasil.config=/etc/yggdrasil/yggdrasil.conf
yggdrasil.mesh_firewall=sudo yggdrasil-mesh-firewall show
yggdrasil.self_test=sudo yggdrasil-selftest
  • ipv6_address is this node's address on the mesh; other nodes reach the instance at it.
  • ipv6_subnet is a /64 routed to this node; you can hand addresses from it to machines behind the node (Step 9).
  • public_key identifies the node; peers can pin it (Step 8).

The two lines left out are peering_secret (the password) and peer_uri, the complete address your other machines put in their Peers list: tls://<this instance's public IP>:9001?password=<password>. Read them when you set up a peer, on a screen no one else can see:

sudo grep -e '^yggdrasil.peer_uri=' /root/yggdrasil-credentials.txt

Step 3: Confirm the node is running

The node and the mesh firewall are active, the daemon runs unprivileged, and the only listener is the peering port (a dual-stack socket, IPv4 and IPv6):

systemctl is-active yggdrasil.service cloudimg-yggdrasil-mesh-firewall.service
systemctl show -p User -p CapabilityBoundingSet yggdrasil.service
yggdrasil -version
sudo ss -Htlnp 'sport = :9001' | awk '{print $1, $4, $6}'
active
active
CapabilityBoundingSet=cap_net_admin
User=yggdrasil
Build name: yggdrasil
Build version: 0.5.15
LISTEN *:9001 users:(("yggdrasil",pid=9910,fd=5))

yggdrasilctl getSelf shows the node itself, and ygg0 carries its address:

sudo yggdrasilctl getSelf
ip -6 addr show dev ygg0 scope global
┌─────────────────────┬──────────────────────────────────────────────────────────────────┐
│ Build name:         │ yggdrasil                                                        │
│ Build version:      │ 0.5.15                                                           │
│ IPv6 address:       │ 200:9c57:9796:62e9:4e02:fbaf:1743:404c                           │
│ IPv6 subnet:        │ 300:9c57:9796:62e9::/64                                          │
│ Routing table size: │ 1                                                                │
│ Public key:         │ b1d43434ce8b58fe8228745e5fd9f9b42dc5f9b4d22432b35c1fa160052d1351 │
└─────────────────────┴──────────────────────────────────────────────────────────────────┘
14: ygg0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 65535 qdisc fq_codel state UNKNOWN group default qlen 500
    inet6 200:9c57:9796:62e9:4e02:fbaf:1743:404c/7 scope global
       valid_lft forever preferred_lft forever

Step 4: Run the self-test

The shipped self-test proves the mesh end to end without any machine of yours: it starts a second, short-lived Yggdrasil node in an isolated network namespace (as a systemd unit that is itself denied the metadata service), checks that a peering without this instance's password is refused and that one with it comes up, pings this node over the mesh, and checks what a mesh peer cannot reach:

sudo yggdrasil-selftest
ok: yggdrasil.service is active (pid 8214, user yggdrasil)
ok: mesh firewall loaded (inbound on ygg0: ICMPv6 + replies + your allow list; everything else dropped)
ok: admin API: local unix socket only; an unprivileged user is refused
ok: node 200:9c57:9796:62e9:4e02:fbaf:1743:404c (200::/7) on ygg0, matches the credentials file
ok: a peering without this instance's password is refused
ok: a test node (transient unit, metadata denied) peered with the password; ping 200:9c57:9796:62e9:4e02:fbaf:1743:404c over the mesh answers
ok: SSH on 200:9c57:9796:62e9:4e02:fbaf:1743:404c is dropped from the mesh (default deny)
ok: metadata (169.254.169.254, fd00:ec2::254) and this host's VPC address 172.31.92.59 are unreachable from the mesh
ok: IP forwarding is off (IPv4 and IPv6)
ok: metadata: 169.254.169.254 is unreachable from inside yggdrasil.service
SELFTEST_OK

Step 5: Peer another machine with this node

On the machine you want on the mesh (a laptop, a server, another instance), install Yggdrasil from the project's installation instructions, then add this node's peer_uri (from Step 2) to the Peers list of its configuration (/etc/yggdrasil/yggdrasil.conf on Linux) and restart it:

Peers: [
  "tls://<this-instance-public-ip>:9001?password=<peering_secret>"
]
sudo systemctl restart yggdrasil

The other machine dials out to this node, so it needs no inbound port of its own; this node's security group must allow 9001/tcp from it. Keep that machine's configuration file readable only by root: it now holds the password.

Step 6: Check peers

When the other machine has connected, it is listed as an inbound peer with its own mesh address:

sudo yggdrasilctl getPeers
┌────────────────────────┬───────┬─────┬────────────────────────────────────────┬────────┬────────┬───────┬───────┬──────┬────┬────┬──────┬────────────┐
│          URI           │ State │ Dir │               IP Address               │ Uptime │  RTT   │  RX   │  TX   │ Down │ Up │ Pr │ Cost │ Last Error │
├────────────────────────┼───────┼─────┼────────────────────────────────────────┼────────┼────────┼───────┼───────┼──────┼────┼────┼──────┼────────────┤
│ tls://192.0.2.10:40658 │ Up    │ In  │ 200:a6c3:b5bd:c967:c80d:ec16:88ee:3d9e │ 9s     │ 0.71ms │ 0.6KB │ 0.7KB │ -    │ -  │ 0  │ 1    │ -          │
└────────────────────────┴───────┴─────┴────────────────────────────────────────┴────────┴────────┴───────┴───────┴──────┴────┴────┴──────┴────────────┘

From either machine, the other answers on its mesh address (ping -6 200:...). Note that getPeers also prints the full URI of any peer this node dials out to, password included, so share its output with care.

Step 7: The mesh firewall: open a port to the mesh

Everything that arrives over the mesh except ping and replies is dropped, and the counter shows it:

sudo yggdrasil-mesh-firewall show
table inet cloudimg_yggdrasil {
    chain input {
        type filter hook input priority filter - 10; policy accept;
        iifname "ygg0" ct state established,related accept
        iifname "ygg0" meta l4proto ipv6-icmp accept
        iifname "ygg0" counter packets 6 bytes 480 drop comment "cloudimg-mesh-default-deny"
    }

    chain forward {
        type filter hook forward priority filter - 10; policy accept;
        ip daddr { 168.63.129.16, 169.254.169.254 } counter packets 0 bytes 0 drop
        ip6 daddr fd00:ec2::254 counter packets 0 bytes 0 drop
        iifname "ygg0" counter packets 0 bytes 0 drop
    }
}

To let your mesh machines reach a service on this instance, allow its port. For example, SSH over the mesh:

sudo yggdrasil-mesh-firewall allow tcp 22

The allowed ports are kept in /etc/yggdrasil/mesh-firewall.conf (one tcp <port> or udp <port> per line) and survive reboots; sudo yggdrasil-mesh-firewall deny tcp 22 closes the port again. Remember that on a public mesh (Step 10) an allowed port is open to every node of that network, not just yours: combine it with the service's own authentication, or with AllowedPublicKeys (Step 8) and a private mesh.

The forward chain keeps this node an endpoint: nothing from the mesh is forwarded into your VPC or to the instance metadata service, and IP forwarding is off (/etc/sysctl.d/60-cloudimg-yggdrasil.conf).

Step 8: Pin your peers' keys (optional)

The peering password admits any node that knows it. To admit only specific machines, list their public keys (their yggdrasilctl getSelf shows them) in AllowedPublicKeys in /etc/yggdrasil/yggdrasil.conf, then restart the node (Step 11 shows how to edit the file safely):

"AllowedPublicKeys": ["<peer-public-key-1>", "<peer-public-key-2>"],

AllowedPublicKeys limits who may peer with this node; it is not a firewall for traffic routed over the mesh. The mesh firewall (Step 7) is.

Step 9: Route the node's subnet (optional)

Each node owns a /64 (ipv6_subnet in Step 2). Machines behind this node can use addresses from it, which requires turning IP forwarding on and allowing that traffic in the mesh firewall's forward chain. This image keeps forwarding off by design, so a mesh peer can never use the instance as a router into your VPC. If you need subnet routing, add your own nftables rules for exactly the traffic you mean to forward, keep the metadata drops, and change /etc/sysctl.d/60-cloudimg-yggdrasil.conf deliberately.

Step 10: Joining the public Yggdrasil network (optional)

Out of the box this node and your peers form a private mesh. To reach the public Yggdrasil network, add one or more public peers from the community public peers list to Peers, keeping the password-protected listener for your own machines:

"Peers": ["tls://<public-peer-host>:<port>"],

On the public network every node can reach this node's address. The mesh firewall still drops unsolicited inbound traffic, but any port you allow (Step 7) becomes reachable from the whole network. Leave node info privacy on.

Step 11: Configuration changes

The configuration is JSON in /etc/yggdrasil/yggdrasil.conf (root only). It holds the node's private key, so never print it or copy it off the instance. This shows the settings without the key and with the password masked:

sudo python3 -c 'import json,re; c=json.load(open("/etc/yggdrasil/yggdrasil.conf")); c.pop("PrivateKey"); c["Listen"]=[re.sub("password=[^&]*","password=<hidden>",u) for u in c["Listen"]]; print(json.dumps(c, indent=2))'
{
  "Peers": [],
  "InterfacePeers": {},
  "Listen": [
    "tls://0.0.0.0:9001?password=<hidden>"
  ],
  "AdminListen": "unix:///var/run/yggdrasil/yggdrasil.sock",
  "MulticastInterfaces": [],
  "AllowedPublicKeys": [],
  "IfName": "ygg0",
  "IfMTU": 65535,
  "NodeInfoPrivacy": true,
  "NodeInfo": {},
  "GroupPassword": ""
}

Edit it with sudo nano /etc/yggdrasil/yggdrasil.conf, keep it valid JSON, then apply with sudo systemctl restart yggdrasil. The start check (Step 12) refuses a configuration that is not JSON, has no key, puts the admin API on a TCP port, or renames the interface (the mesh firewall is keyed to ygg0); journalctl -u yggdrasil says why.

Step 12: How the fail-closed start works

yggdrasil.service runs only when first boot's ready marker exists and only together with the mesh firewall, every start runs a check as root first, and the files that hold the key and the password are readable only by root:

systemctl cat yggdrasil.service | grep -E '^(ConditionPathExists|Requires|ExecStartPre|LoadCredential|ExecStart|User|CapabilityBoundingSet)='
systemctl show -p IPAddressDeny yggdrasil.service
sudo stat -c '%a %U:%G %n' /root/yggdrasil-credentials.txt /etc/yggdrasil/yggdrasil.conf /run/yggdrasil
Requires=cloudimg-yggdrasil-mesh-firewall.service
ConditionPathExists=/var/lib/cloudimg/yggdrasil-ready
User=yggdrasil
ExecStartPre=+/usr/local/sbin/yggdrasil-preflight
LoadCredential=yggdrasil.conf:/etc/yggdrasil/yggdrasil.conf
ExecStart=/usr/local/bin/yggdrasil -useconffile %d/yggdrasil.conf
CapabilityBoundingSet=CAP_NET_ADMIN
IPAddressDeny=168.63.129.16/32 169.254.169.254/32 fd00:ec2::254/128
600 root:root /root/yggdrasil-credentials.txt
600 root:root /etc/yggdrasil/yggdrasil.conf
750 yggdrasil:yggdrasil /run/yggdrasil

An unprivileged user cannot use the admin API:

yggdrasilctl getSelf 2>&1 | tail -1
2026/11/01 06:13:20 Fatal error: dial unix /var/run/yggdrasil/yggdrasil.sock: connect: permission denied

Step 13: Generate a new node identity

To replace the node key (and so the node's address) and the peering password, for example after a peer machine holding the password is lost, run first boot again. It shreds the old configuration, mints a new identity and restarts the node; every peer then needs the new peer_uri:

sudo systemctl stop yggdrasil.service
sudo rm -f /var/lib/cloudimg/yggdrasil-firstboot.done /var/lib/cloudimg/yggdrasil-ready
sudo systemctl restart yggdrasil-firstboot.service
systemctl is-active yggdrasil.service
sudo grep -c '^yggdrasil.ipv6_address=2' /root/yggdrasil-credentials.txt
active
1

Step 14: Logs

journalctl -u yggdrasil shows the node's start-up (its public key and address, which are not secret) and each peer connecting or disconnecting. Yggdrasil never logs the private key or the peering password:

sudo journalctl -u yggdrasil.service -n 8 --no-pager -o cat
2026/11/01 06:09:57 Your IPv6 subnet is 300:9c57:9796:62e9::/64
2026/11/01 06:09:57 UNIX admin socket listening on /var/run/yggdrasil/yggdrasil.sock
2026/11/01 06:09:57 TLS listener started on [::]:9001
2026/11/01 06:09:57 Interface name: ygg0
2026/11/01 06:09:57 Interface IPv6: 200:9c57:9796:62e9:4e02:fbaf:1743:404c/7
2026/11/01 06:09:57 Interface MTU: 65535
2026/11/01 06:11:34 Connected inbound: 200:a6c3:b5bd:c967:c80d:ec16:88ee:3d9e@192.0.2.10:40658, source 192.0.2.9:9001
2026/11/01 06:11:44 Disconnected inbound: 200:a6c3:b5bd:c967:c80d:ec16:88ee:3d9e@192.0.2.10:40658, source 192.0.2.9:9001

Step 15: Security recommendations

  • Keep SSH open only to your management addresses, and port 9001 only to your peers' addresses where you know them.
  • Treat the peering password like a password: put it only on machines you control, keep their configuration files at mode 0600, and generate a new identity (Step 13) if one is exposed.
  • Open mesh ports (Step 7) only for services that authenticate their users, and pin peer keys (Step 8) for a private mesh.
  • Leave IP forwarding off unless you deliberately route the node's subnet.
  • Leave unattended security upgrades on. Yggdrasil is two binaries in /usr/local/bin; cloudimg ships updated versions as new versions of this listing.
  • Security notes for 0.5.15: govulncheck reports no vulnerability reachable from Yggdrasil's code. The release's Go modules carry three advisories in code Yggdrasil does not use: GO-2026-6866 and GO-2026-6860 (the HTML parser in golang.org/x/net) and GO-2026-5932 (the unmaintained golang.org/x/crypto/openpgp package, which Yggdrasil does not import).

Support and licensing

Yggdrasil is open-source software by the Yggdrasil Network project and contributors, distributed under LGPL-3.0 with a linking exception that grants additional permissions (the GNU Lesser General Public License v3.0, plus an exception that lets you convey a combined work that links to Yggdrasil without providing the Minimal Corresponding Source or installation information). The licence text and cloudimg's build record, with the source offer for the unmodified upstream 0.5.15 release (also available from support@cloudimg.co.uk), are in /usr/share/doc/yggdrasil/. cloudimg provides the packaging, the per-instance identity, the mesh firewall and the hardening described in this guide.

cloudimg is not affiliated with or endorsed by the Yggdrasil Network project. Yggdrasil is used here only to identify the software.

For help with deployment or configuration, email support@cloudimg.co.uk or visit cloudimg support.