Skip to content

This page may contain unreviewed machine generated text. Use with care.

Pages

Trust document and keys

The document naming who may join the mesh, and the key files nodes authenticate with.

The trust document names every node that may join the mesh, its endpoints, and the key it signs with. Every node reads it from auth.trust. A handshake succeeds only between nodes the document names, each proving it holds the key of the block that lists it.

The format is the registry.json of ranet: a JSON array of organizations.

Format

[
  {
    "organization": "example",
    "public_key": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA...\n-----END PUBLIC KEY-----",
    "nodes": [
      {
        "common_name": "gateway",
        "endpoints": [
          {
            "serial_number": "0",
            "address_family": "ip4",
            "address": "192.0.2.1",
            "port": 13000
          },
          {
            "serial_number": "1",
            "address_family": "ip6",
            "address": "gateway.example.net",
            "port": 13000
          }
        ],
        "remarks": {}
      }
    ]
  }
]

Organization

FieldMeaning
organizationThe organization's name, the first half of every node name
public_keyThe Ed25519 public key every node of this block signs with, as a PEM PUBLIC KEY block
nodesThe nodes of this organization

An organization may appear in more than 1 block, each with its own key. A node is verified with the key of the block that lists it.

Node

FieldMeaning
common_nameThe node's name within its organization
endpointsThe addresses peers dial this node at
remarksAny JSON value. ranet3 reads and ignores it

The node's full name is organization/common_name, written as configuration describes.

Endpoint

FieldMeaning
serial_numberThe endpoint's name within its node, the part after :
address_familyip4 or ip6
addressA literal address, a hostname, or null for an endpoint nobody dials
portThe UDP port peers dial
  • A literal address of another family is never dialed.
  • A hostname is resolved on every dial, filtered to the endpoint's family. On Linux ranet3 resolves it with Go's own resolver, from /etc/hosts and the nameservers /etc/resolv.conf lists, and asks no NSS module, so a name only such a module answers, a .local name through nss-mdns among them, does not resolve. Its query to a nameserver outside cap.border.overlay, one in a subnet another node announces included, carries the underlay's mark where the process may set it, so where cap.table is written it ends at row L1 or L2 of where a lookup ends and never waits on a session that only the name can dial. A nameserver on the host itself, such as systemd-resolved's 127.0.0.53, takes the query at row L1, and its own query upstream carries no mark, so it can wait on such a session. A query to a nameserver inside the overlay carries none, whatever zone follows its address, and where cap.table is written ends as rows L1 to L5 give. A link-local nameserver lies outside an overlay of the mesh's own prefixes, so its query carries the mark where the process may set it. Sharing a host describes the mark.
  • An endpoint with no address, an empty one, or a name holding :, / or a space is never dialed. Its node can still dial out and be answered.
  • Peers dial the port listed here. Keep it equal to the node's own link.port, which ranet3 does not check.

What is refused

The whole document is refused, and the daemon does not start or reload, when:

  • It is not a JSON array of organizations, or carries a field this page does not list.
  • Anything follows the array.
  • An organization has no name, or its public_key is not an Ed25519 key in PEM.
  • A node has no common_name, or its name appears twice in 1 organization, across all of that organization's blocks.
  • An endpoint has no serial_number, a port of 0, or an address_family other than ip4 and ip6.
  • 2 endpoints of 1 node share a serial_number.

How a node uses it

At startup and on every reload, a node checks itself against the document:

  • The document must name node.
  • The public half of auth.key must be the key the document holds for the node.
  • Each link.endpoints entry must be an endpoint the document lists for the node, in the same family.
  • Every node and endpoint auth.aliases names must be in the document. One it lacks refuses the start, and a reload logs it instead.

A reload reads the document again. It closes every session whose peer the new document drops or lists under a different key. Reloading describes the rest. ranet3 status shows how many organization blocks and nodes the document holds.

IDs on the wire

On the wire, each side of a handshake names an endpoint in an ID payload. A ranet3 node names itself by an X.501 distinguished name of O, CN and serialNumber (ID_DER_ASN1_DN), unless auth.aliases.local gives it another ID toward that peer, and a responder sends back the ID asked for, as below. The other side reads the endpoint the ID names, looks it up in the document and verifies the signature with that node's key (RFC 7427 with Ed25519, RFC 8420). The ID only selects the endpoint, and the key always comes from the document. An initiator that refuses the responder's ID or signature says so with AUTHENTICATION_FAILED in the exchange that follows, and a ranet3 responder then ends the session it had opened.

An ID names an endpoint when it is:

  • a DN of O, CN and serialNumber
  • an ID_KEY_ID whose octets spell the endpoint as organization/name:serial
  • an ID_IPV4_ADDR, ID_IPV6_ADDR, ID_FQDN, ID_RFC822_ADDR or ID_KEY_ID that an entry of auth.aliases.remote matches, as the configuration reference describes

Any other ID is refused with AUTHENTICATION_FAILED: one no entry matches, an address ID whose length is not that of its family, and an ID of another type. So is an initiator whose ID names a node and no endpoint, such as a DN without serialNumber. An address ID is never compared with the address the packet came from.

An initiator asks for the endpoint it dials in IDr: by its DN where auth.aliases.remote names it by no ID, by the 1 ID that names it, and not at all where several do, since it cannot know which one the responder holds. It accepts only an IDr that names the endpoint it dialed.

A responder answers as an endpoint the initiator asked for, or refuses with AUTHENTICATION_FAILED. It never answers as another one.

  • An IDr that names an endpoint, by its DN, by a key ID spelling it or through this node's own auth.aliases.remote, is answered as that endpoint when it is one of this node's endpoints of the family the request arrived in, and refused otherwise. This holds even where auth.aliases.local presents the same ID.
  • With no IDr, or one that names no endpoint and holds an ID auth.aliases.local presents to some peer, the responder answers as its 1 endpoint of the family the request arrived in.
  • Any other IDr is refused.

The responder sends back the ID asked for, a DN in its own encoding. With no IDr it sends the ID auth.aliases.local presents to the initiator, or else its DN. Each end records which ID named its peer in an ike.id.resolved event, as debugging describes.

Families

A local endpoint dials only remote endpoints of its own family, so a responder takes a handshake only where the families agree. Once the initiator's signature verifies, the responder refuses with AUTHENTICATION_FAILED:

  • an initiator whose ID names an endpoint the document lists in the other family than the one the request arrived in
  • an IDr naming one of this node's endpoints of the other family
  • a request it would answer by family when this node's link.endpoints holds none or more than 1 entry of that family

A node with 1 IPv4 endpoint therefore refuses a request that reaches it over IPv6, with or without an IDr. A request from an IPv4-mapped address counts as IPv4. A node with 2 endpoints in 1 family still answers as either of them a peer that asks for it in IDr, as a ranet3 peer does unless its auth.aliases.remote names that endpoint by several IDs.

Key files

auth.key is the node's private key: an Ed25519 key in a PKCS8 PEM PRIVATE KEY block. Every node of an organization block holds the same key.

OpenSSL writes both halves:

openssl genpkey -algorithm ed25519 -out key.pem
openssl pkey -in key.pem -pubout -out pub.pem
jq -Rs . pub.pem    # the public key as a JSON string for public_key

Keep it private

The private key authenticates every node of its organization. Store it with mode 0600, outside any version-controlled tree, and pass it to the node by path. The NixOS and nix-darwin modules refuse a settings.auth.key in the Nix store.

The trust document holds no secret, but it lists every node's address. The repository's .gitignore excludes every registry.json except the synthetic fixture in internal/registry/testdata.

Updated