nodewire

package
v0.3.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 7, 2026 License: GPL-3.0 Imports: 20 Imported by: 0

README

Private boot-bound TLS

Unwired primitives only; no listener, public route, specialist mode, provisioning service, persistence or enrollment is installed. Credentials are independent of Ether's shared encryption key and public Plex certificate. Only explicit roots are accepted. Errors deliberately omit credentials and upstream policy details.

Identity is the exact cluster identity, persistent cluster.Identity.NodeID, and the process's existing cluster.Node.Incarnation (Postgres generates this once at construction). Never mint another boot for TLS, reuse a previous boot, copy node.json, or interpret a display name as an identity. Fields are bounded ASCII tokens compatible with nodesource and remotetranscode DTOs. The sole URI SAN is ether-node://identity/v1/<cluster>/<node>/<boot>, with no URI aliases, queries, fragments, escapes or extra identities.

An authorized issuer receives an explicit CA certificate/Ed25519 signer, checked Ed25519 CSR, trusted identity, and 1–8 exact DNS/IP SANs. Issue ignores CSR requested extensions and bounds validity to 24 hours and the CA's validity. Hostnames are lowercase ASCII DNS without wildcards; IP SANs are supported. Enrollment must authorize all identity/host values independently of the CSR. NewSnapshot parses/copies leaf/key PEM and explicit CA PEM; no internal mutable TLS config, pool, certificate or private key slice is returned to callers. Configs returned by builders are individually owned by their recipient.

NewGuard requires a live Policy and clock. Policy must check complete current cluster/node/boot membership and revocation; nil/unavailable policy fails closed. Policy can inspect the copied certificate serial for credential revocation. Callbacks run without locks and must be concurrency safe. Caller-controlled placement claims never establish membership. ClientTLS(expected, hostname) checks BOTH a complete expected peer identity and ordinary DNS/IP hostname, chain, EKU and validity. ServerTLS requires verified client certificates and checks cluster membership; the intended caller and operation are additionally checked by consumer authorization. Minimum TLS is 1.2; resumption is disabled.

Replace atomically publishes a snapshot for the SAME local cluster/node/boot. New server handshakes select it automatically. For clients, rebuild the config and HTTP transport after root/client credential rotation and close old idle connections. This deliberate requirement retains ordinary Go TLS verification; there is no InsecureSkipVerify or dynamic trust bypass. Use overlapping explicit root bundles while rotating. Failed snapshot validation leaves the old one intact. Request checks use the latest snapshot, so removed roots, expiry, old boots and revocations fail even on established connections. Checks are point-in-time; policy updates racing admission still require consumer atomic authority checks.

Integration:

  • remotetranscode: Security.Authenticate = guard.RemoteAuthenticate returns the certificate Boot, since Controller is boot-scoped in that DTO. Policy still receives the complete identity and must bind the current controller membership; Security.Authorize must independently check placement/generation/source rights for every operation. Supply ClientTLS to its private HTTP client and the mandatory AdapterOptions.RecheckServer closure described below.
  • nodesource: Options.Peer = guard.SourcePeer returns stable NodeID and Boot, checking current roots/expiry/policy on every call (including reused TLS). This leaf-only API supports direct trusted-CA issuance; for intermediate chains compose guard.Request in a private request wrapper instead. Keep its existing verified-chain handler checks. Supply ClientTLS, BindServer and the mandatory ClientOptions.RecheckServer closure described below to the client.
  • Both outbound consumers require RecheckServer(ctx, state) composed with guard.RecheckServer(ctx, state, expectedIdentity, hostname). The shared outbound transport opens a fresh HTTP/1.1 TLS connection per operation. Ordinary Go chain/hostname/EKU verification and original TLS callbacks run first; current roots, expiry and complete cluster/node/boot policy are checked against that selected connection before HTTP receives it or can transmit credential headers or request bodies. Rejection closes the connection. Keepalive, HTTP/2 and TLS session resumption are disabled, and caller dial/proxy hooks cannot bypass admission. The fixed origin is enforced and consumer clients refuse redirects.
  • Go 1.27 HTTP transport detaches dial cancellation but preserves context values. The wrapper carries the original operation context in a private value and restores it inside the TLS dial hook, with a bounded admission deadline. Policy must cooperate with cancellation. This is a pre-write dial boundary, not a GotConn cancellation assumption. Admission timeout does not impose a new deadline on streaming response bodies; request cancellation and streaming backpressure remain in effect. See the linked transport audit for ordering evidence and real TLS fixtures.
  • After root/client credential rotation, explicitly build a new client and transport from ClientTLS. Old transports still invoke current Guard policy and cannot bypass removed roots, expiry or revocation. Checks are point-in-time: handshake callbacks alone cannot revoke active connections, and outbound admission does not terminate a stream already admitted. Server handlers use Request per operation. Long source copies additionally require live policy through Authority.Verify at their existing cadence; the leaf mapper runs at request admission only. Certificate trust is never placement/source authority.

Unresolved throughput limitation: every HEAD, execution RPC and source range read, including each 64 KiB read, pays a fresh TCP/TLS handshake. GPU/production throughput acceptance has not been demonstrated. Connection reuse requires a separately audited pre-write admission design; these primitives remain unwired.

Provisioning, issuer custody, root bundle installation/bootstrap/adoption, authorized CSR enrollment and rotation scheduling remain host responsibilities. There is no automatic CA generation through the shared database. Existing clusterconfig.Join consumes single-use tokens before writing local files. This package does not reuse those tokens or solve idempotent Kubernetes enrollment. No CA key belongs in notifications, logs or shared encryption material. Deployment must supply clock synchronization and conservative policy expiration bounds.

Documentation

Overview

Package nodewire supplies private, boot-bound mTLS primitives. It starts no services.

Index

Constants

View Source
const MaxLifetime = 24 * time.Hour

Variables

View Source
var ErrDenied = errors.New("nodewire: peer denied")
View Source
var ErrInvalid = errors.New("nodewire: invalid identity or credentials")

Functions

func Issue

func Issue(caDER []byte, caKey ed25519.PrivateKey, csrDER []byte, id Identity, hosts []string, now time.Time, lifetime, clockSkew time.Duration) ([]byte, error)

Issue signs only a checked Ed25519 CSR with an explicitly supplied Ed25519 CA. CSR extensions/names are never copied; enrollment must authorize id and hosts independently before calling Issue. Returns leaf DER, never a CA private key. Backdating covers clock corrections without relaxing verification. The useful lifetime is capped so the entire encoded validity span stays within MaxLifetime.

func NewBoot

func NewBoot() (string, error)

NewBoot is available for runtimes without an incarnation generator. Existing cluster runtimes must supply their already-generated membership incarnation.

Types

type Guard

type Guard struct {
	// contains filtered or unexported fields
}

Guard atomically replaces immutable credentials/trust. Policy is mandatory and called outside locks on each check, including operations on reused connections.

func NewGuard

func NewGuard(s *Snapshot, p Policy, now func() time.Time) (*Guard, error)

func (*Guard) BindServer

func (g *Guard) BindServer(expected Identity, hostname string) func(tls.ConnectionState) error

BindServer is also usable as nodesource.ClientOptions.BindServer. That hook alone runs at handshake: use RecheckServer on every subsequent operation.

func (*Guard) ClientTLS

func (g *Guard) ClientTLS(expected Identity, hostname string) (*tls.Config, error)

ClientTLS returns a fresh independent config for an explicit hostname AND expected identity. Rebuild the transport after root/client credential rotation.

func (*Guard) RecheckServer

func (g *Guard) RecheckServer(ctx context.Context, cs tls.ConnectionState, expected Identity, hostname string) error

func (*Guard) RemoteAuthenticate

func (g *Guard) RemoteAuthenticate(r *http.Request) (string, error)

RemoteAuthenticate maps the existing remotetranscode boot-only Controller DTO. Its separate Security.Authorize callback remains mandatory.

func (*Guard) Replace

func (g *Guard) Replace(s *Snapshot) error

Replace preserves this process identity. Use a new Guard for a new boot.

func (*Guard) Request

func (g *Guard) Request(r *http.Request) (Identity, error)

Request rechecks current trust, expiry and live membership for every request.

func (*Guard) ServerTLS

func (g *Guard) ServerTLS() *tls.Config

ServerTLS selects current credentials/trust per new handshake. Request remains required: neither handshake policy nor TLS session state is current authority.

func (*Guard) SourcePeer

func (g *Guard) SourcePeer(c *x509.Certificate) (nodesource.Peer, error)

SourcePeer fits nodesource.Options.Peer and rechecks current direct-CA trust, validity and live policy on each invocation. That API supplies only a leaf, so intermediate-CA chains must instead use Request at a private HTTP wrapper and an explicitly composed peer mapper. Authority.Verify remains mandatory, including membership/revocation checks during long byte copies.

type Identity

type Identity struct{ Cluster, NodeID, Boot string }

Identity binds the shared cluster, persistent node ID and unique process boot. Boot must be the membership incarnation, never a separately generated TLS boot.

func CertificateIdentity

func CertificateIdentity(c *x509.Certificate) (Identity, error)

CertificateIdentity rejects all extra URI identities and noncanonical aliases.

type Policy

type Policy func(context.Context, Identity, *x509.Certificate) error

Policy must consult current membership/incarnation and revocation (including serial deny lists if used). Unavailable authority MUST return an error. It grants no placement/source rights; consumers still authorize each operation.

type Snapshot

type Snapshot struct {
	// contains filtered or unexported fields
}

Snapshot owns all parsed credentials and roots. No mutable internals escape.

func NewSnapshot

func NewSnapshot(local Identity, certPEM, keyPEM, rootsPEM []byte, now time.Time) (*Snapshot, error)

NewSnapshot accepts a leaf/key PEM and a bounded explicit CA bundle. No system roots, peer-supplied anchors, shared-secret key derivation or CA generation.

Directories

Path Synopsis
Package outbound provides the private per-operation TLS admission boundary.
Package outbound provides the private per-operation TLS admission boundary.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL