Skip to content

Registration

The internal/registration package handles node self-registration and bootstrap authentication with the Plexsphere control plane. It resolves a one-time bootstrap token, generates a Curve25519 keypair, registers with the control plane, persists the resulting identity, and manages auth token lifecycle.

Config

Config holds registration parameters passed to the Registrar constructor. Config loading is the caller's responsibility.

FieldTypeDefaultDescription
DataDirstringData directory for identity files (required)
TokenFilestring/etc/plexd/bootstrap-tokenPath to bootstrap token file
TokenEnvstringPLEXD_BOOTSTRAP_TOKENEnvironment variable for bootstrap token
TokenValuestringDirect token value override
UseMetadataboolfalseEnable cloud metadata token source
MetadataTokenPathstring/plexd/bootstrap-tokenMetadata key path for bootstrap token
MetadataTimeouttime.Duration2sTimeout for metadata service requests
HostnamestringHostname override (default: os.Hostname)
Metadatamap[string]stringOptional metadata for registration request
MaxRetryDurationtime.Duration5mMaximum retry duration for transient errors
go
cfg := registration.Config{
    DataDir: "/var/lib/plexd",
}
cfg.ApplyDefaults() // sets TokenFile, TokenEnv, MetadataTokenPath, MetadataTimeout, MaxRetryDuration
if err := cfg.Validate(); err != nil {
    log.Fatal(err) // DataDir is required
}

TokenResolver

Resolves the bootstrap token by checking sources in priority order. The first non-empty result wins.

Source Priority

  1. Direct valueConfig.TokenValue
  2. FileConfig.TokenFile (content trimmed of whitespace)
  3. Environment variableos.Getenv(Config.TokenEnv) (trimmed)
  4. Metadata service — via MetadataProvider interface (only if Config.UseMetadata is true)

Token Validation

  • Non-empty
  • Maximum 512 bytes
  • Printable ASCII only (bytes 0x20–0x7E)

TokenResult

FieldTypeDescription
ValuestringThe resolved token value
FilePathstringNon-empty if token was read from a file

FilePath is used by the Registrar to delete the token file after successful registration.

go
resolver := registration.NewTokenResolver(&cfg, nil) // nil = no metadata provider
result, err := resolver.Resolve(ctx)
if err != nil {
    // error lists all attempted sources
}

MetadataProvider

Pluggable interface for cloud-specific token resolution.

go
type MetadataProvider interface {
    ReadToken(ctx context.Context) (string, error)
}

The concrete implementation IMDSProvider reads tokens from cloud instance metadata services. See Cloud-Init Deployment Reference for details.

GenerateKeypair

Generates a Curve25519 keypair for WireGuard mesh encryption.

  • Private key: 32 random bytes from crypto/rand, clamped per Curve25519 spec
  • Public key: derived via curve25519.X25519(privateKey, Basepoint)
  • Private key never leaves the node and is never logged
go
keypair, err := registration.GenerateKeypair()
if err != nil {
    log.Fatal(err)
}
pubKeyBase64 := keypair.EncodePublicKey() // standard base64, 44 characters

Keypair

FieldTypeDescription
PrivateKey[]byte32-byte clamped Curve25519 key
PublicKey[]byte32-byte derived public key

NodeIdentity

Holds the registration identity of a node after successful enrollment.

FieldTypeJSON TagPersisted To
NodeIDstring"node_id"identity.json
MeshIPstring"mesh_ip"identity.json
SigningPublicKeystring"signing_public_key"identity.json + signing_public_key
PrivateKey[]byte"-" (excluded)private_key (base64)
NodeSecretKeystring"-" (excluded)node_secret_key

Data Directory Layout

{data_dir}/
├── identity.json        (0600) — NodeID, MeshIP, SigningPublicKey
├── private_key          (0600) — base64-encoded Curve25519 private key
├── node_secret_key      (0600) — bearer token for post-registration API calls
└── signing_public_key   (0600) — control plane signing public key
  • Directory created with 0700 permissions if missing
  • All files written atomically (temp file + fsync + rename)
  • PrivateKey and NodeSecretKey use json:"-" tags to prevent accidental JSON serialization

SaveIdentity / LoadIdentity

go
// Persist after registration
err := registration.SaveIdentity("/var/lib/plexd", identity)

// Load on restart
identity, err := registration.LoadIdentity("/var/lib/plexd")
if errors.Is(err, registration.ErrNotRegistered) {
    // no identity files — need to register
}

ErrNotRegistered

Sentinel error returned by LoadIdentity when identity files are absent from the data directory.

go
var ErrNotRegistered = errors.New("registration: node is not registered")

Supports errors.Is matching:

go
if errors.Is(err, registration.ErrNotRegistered) {
    // proceed with fresh registration
}

Registrar

Orchestrates the complete registration lifecycle: check existing identity, resolve token, generate keypair, register with retries, persist identity, clean up token file, and set auth token.

Constructor

go
func NewRegistrar(client *api.ControlPlane, cfg Config, logger *slog.Logger) *Registrar
  • Applies config defaults
  • Logger tagged with component=registration
  • Optional: call SetMetadataProvider, SetCapabilities, SetClock after construction

Register

go
func (r *Registrar) Register(ctx context.Context) (*NodeIdentity, error)

Orchestration flow:

  1. Load existing identity — if valid, set auth token and return (idempotent)
  2. Corrupt identity — log warning, proceed with fresh registration
  3. Resolve bootstrap token — via TokenResolver
  4. Generate Curve25519 keypair
  5. Resolve hostnameConfig.Hostname or os.Hostname()
  6. Set bootstrap token as authclient.SetAuthToken(token)
  7. POST /v1/register with retry — exponential backoff on transient errors
  8. Build NodeIdentity from response + private key
  9. Persist identity atomically to data directory
  10. Delete token file if token was file-based (failure logged, not fatal)
  11. Set node_secret_key as authclient.SetAuthToken(nsk)

Retry Logic

Registration retries on transient failures using api.ClassifyError for error classification.

Error TypeAction
Network errors / 5xxRetry with exponential backoff
429 Rate LimitedRespect Retry-After header
401 UnauthorizedFail immediately (invalid bootstrap token)
403 ForbiddenFail immediately
409 ConflictFail immediately (hostname registered)
400 Bad RequestFail immediately

Backoff parameters (consistent with internal/api/ReconnectEngine):

ParameterValue
Base interval1s
Multiplier2x
Max interval60s
Jitter±25%
TimeoutConfig.MaxRetryDuration (default 5m)

IsRegistered

go
func (r *Registrar) IsRegistered() bool

Returns true if valid identity files exist in Config.DataDir.

Usage Example

go
// Create control plane client
cpClient, err := api.NewControlPlane(api.Config{
    BaseURL: "https://api.plexsphere.com",
}, "1.0.0", slog.Default())
if err != nil {
    log.Fatal(err)
}

// Create registrar
reg := registration.NewRegistrar(cpClient, registration.Config{
    DataDir:  "/var/lib/plexd",
    Hostname: "node-01",
    Metadata: map[string]string{"region": "us-east-1"},
}, slog.Default())

// Run registration (idempotent — skips if already registered)
identity, err := reg.Register(ctx)
if err != nil {
    log.Fatalf("registration failed: %v", err)
}

log.Printf("registered as %s with mesh IP %s", identity.NodeID, identity.MeshIP)
// Control plane client now has node_secret_key set as auth token

Auth Token Lifecycle

PhaseAuth Token Value
Before registrationBootstrap token
During POST /v1/registerBootstrap token (Bearer)
After registrationNodeSecretKey
On restart (cached)NodeSecretKey from disk