Files
mesh-host/internal/identity/overlay.go
T
jschoubben 7283924a35 Take over the found tunnel: its key, its port, its peers; stop it, never flush
On an adopted machine the private network takes the predecessor's tunnel
over in place (hq ADR 0105). Genesis finds the one interface up besides the
mesh's own, settles the hub's port and the mesh's range on it, and skips
ADR 0100's non-overlap check for a range that is now the tunnel's; a
--hub-port or --overlay-range that disagrees is refused naming the tunnel's.

At enrolment the found interface's private key becomes this node's overlay
key — the one credential the mesh takes rather than mints — stored where a
generated one is stored, never printed and never sent; the tunnel (port,
address, range, peers) travels with the keys so the mesh composes from it
before the first declaration.

The interface's service may say what it takes over. Before the mesh's unit
starts, the found configuration is kept like any held file and the found
unit is stopped and disabled; nothing is flushed, and an interface still up
after its unit stopped refuses the takeover rather than half-working. The
report says what was carried: interface, port, range, peer count, taken or
not, and where the original was kept.
2026-09-23 23:26:35 +02:00

76 lines
3.4 KiB
Go

package identity
import (
"crypto/ecdh"
"crypto/rand"
"encoding/base64"
"fmt"
)
// The node's key on the private network, which is a different key from the one that says who it
// is — and deliberately so.
//
// novox/hq 08-connectivity: each node generates its own keypair, the private half never leaves
// the machine, and the public half is published to the mesh. That means the control plane
// computes a peer graph it cannot itself impersonate: it knows every public key and holds no
// private one, so it can say who may talk to whom without being able to pretend to be any of them.
//
// Separate from the identity keypair because they are verified by different things at different
// times — the identity signs messages to the mesh, this one encrypts traffic between nodes — and
// a key used for two purposes is one rotation away from breaking the other.
// OverlayKey is a Curve25519 keypair, which is what WireGuard uses.
type OverlayKey struct {
// Public is what travels. Base64, which is the form WireGuard configuration files use, so it
// is carried the way it will be written rather than converted at the last moment.
Public string `json:"public"`
// Private never leaves this machine. It is written to a file of its own that the interface
// configuration points at, so the control plane can compose that configuration without ever
// holding this.
Private string `json:"private"`
}
// GenerateOverlayKey makes this node's keypair for the private network.
func GenerateOverlayKey() (OverlayKey, error) {
private, err := ecdh.X25519().GenerateKey(rand.Reader)
if err != nil {
return OverlayKey{}, fmt.Errorf("cannot generate this node's overlay key: %w", err)
}
return OverlayKey{
Public: base64.StdEncoding.EncodeToString(private.PublicKey().Bytes()),
Private: base64.StdEncoding.EncodeToString(private.Bytes()),
}, nil
}
// OverlayKeyFrom makes this node's overlay key from a private key it did not generate: the found
// tunnel's, on an adopted node whose private network takes that tunnel over (novox/hq ADR 0105).
// The one case where the mesh takes a credential it did not mint. From here on it is stored and
// sealed exactly as a generated one — in the identity file and the key file, readable by root
// alone — and the mesh receives only the public half, derived here from the private one so the
// two cannot disagree.
func OverlayKeyFrom(privateBase64 string) (OverlayKey, error) {
raw, err := base64.StdEncoding.DecodeString(privateBase64)
if err != nil {
return OverlayKey{}, fmt.Errorf("the found tunnel's private key is not base64: %w", err)
}
private, err := ecdh.X25519().NewPrivateKey(raw)
if err != nil {
return OverlayKey{}, fmt.Errorf("the found tunnel's private key is not a Curve25519 key: %w", err)
}
return OverlayKey{
Public: base64.StdEncoding.EncodeToString(private.PublicKey().Bytes()),
Private: base64.StdEncoding.EncodeToString(private.Bytes()),
}, nil
}
// OverlayKeyPath is where the private half lives: a file of its own, referenced by the interface
// configuration rather than embedded in it.
//
// That separation is what lets the mesh compose the configuration. WireGuard's `PostUp` can set a
// private key from a file, so the declaration the control plane sends names this path and carries
// no secret — and the file it names was written by the node, from a key nothing else ever saw.
func OverlayKeyPath(statePath string) string {
return dirOf(statePath) + "/overlay.key"
}