Skip to content

Security & Trust Model

Security Overview

  • Bootstrap tokens are one-time-use with a short TTL. They are deleted from disk after successful registration.
  • Private keys are generated during registration and stored in /var/lib/plexd/ on Linux, and under the platform data directory on macOS and Windows (see Platform defaults). They never leave the node.
  • Control plane communication is TLS-encrypted (HTTPS). The agent validates the server certificate. Every SSE event is additionally signed with the control plane's Ed25519 key and verified by the agent before processing.
  • Mesh traffic is encrypted end-to-end via WireGuard.
  • Compromised nodes can be force-removed from the control plane, triggering key rotation across all affected peers.
  • Hook integrity is enforced via SHA-256 checksums computed at discovery, monitored via inotify, and re-verified before every execution against the digest pinned at first discovery — there is no checksum on the wire, and the inotify watcher deliberately cannot move that pin. Mismatches block execution and trigger alerts.
  • Binary verification - plexd reports its own SHA-256 checksum at registration and with every heartbeat. The control plane compares against known-good checksums per version.

Key Exchange and Trust Model

plexd uses WireGuard's Noise_IKpsk2 handshake with static Curve25519 key pairs. The control plane acts as a trusted key distribution center but never has access to private keys. All events from the control plane (peer changes, policy updates, action requests, key rotations) are signed with an Ed25519 signing key and verified by the agent before processing.

Trust Chain

Bootstrap Token (one-time, short TTL)


   Control Plane  ──── Trust anchor: distributes public keys, PSKs,
        │               and its own Ed25519 signing public key

        ├──► Signing Key ──── Verifies all SSE events + session JWTs


   Node Identity  ──── Public key bound to node ID and mesh IP


   Peer Tunnels   ──── WireGuard E2E encryption (private key stays local)

Phase 1: Registration

During registration the node generates its Curve25519 key pair locally. Only the public key is sent to the control plane. The private key never leaves the node.

Phase 2: Tunnel Setup

The client configures the local WireGuard interface using the registration response:

  1. Create WireGuard interface (plexd0)
  2. Assign mesh IP and set private key
  3. Add each peer with its public key, endpoint, allowed IPs, and PSK
  4. Run STUN discovery and report the node's public endpoint to the control plane
  5. Receive NAT-discovered endpoints of peers and update WireGuard accordingly

Phase 3: Steady State

The control plane pushes peer and key updates via SSE. Every SSE event is signed with the control plane's Ed25519 signing key. The client verifies the signature before applying any change.

Signed Event Envelope:

Every SSE event is wrapped in a signed envelope. The signature covers the canonical JSON serialization of all fields except signature itself (i.e. id, type, scope, key_id, issued_at, and payload):

json
{
  "id": "evt_d4e5f6",
  "type": "node_state_updated",
  "scope": "node:n_abc123",
  "key_id": "did:web:plexsphere.com#key-2026-01",
  "issued_at": "2026-01-15T10:30:00Z",
  "payload": { "revision": 42 },
  "signature": "base64-encoded-ed25519-signature"
}

Verification on every event:

  1. Select the signing key by the envelope's key_id — the current signing key, or the previous key during a rotation grace window — and verify the Ed25519 signature over the canonical JSON of all fields except signature, using the control plane's signing public key (received during registration).
  2. Check issued_at staleness (max 5 minutes) and reject timestamps too far in the future.
  3. If any check fails, reject the event and log it; the event is skipped and never dispatched.

This ensures that even if the TLS connection is compromised (e.g. through a rogue proxy or certificate authority), events cannot be forged. Duplicate or reordered deliveries are harmless because the reconciler's authoritative state pull, not the event payload, is the source of truth.

Action dispatch is the one case where that distinction cuts both ways. A dispatch is read only from the executions block of the NSK-authenticated state pull, so no event payload — forged, replayed, or reordered — can make a node run an action. But the state response itself carries no application-layer signature: unlike an SSE envelope, it is protected by TLS alone. An attacker able to present a certificate the node's system trust store accepts — a misissued certificate, a rogue CA, or a deployment that set tls_insecure_skip_verify — can therefore inject an executions entry naming any registered action. Signing the executions block is a control-plane contract change and is not yet available; until it is, treat the TLS trust path (and never enabling tls_insecure_skip_verify outside a lab) as the boundary that protects remote execution.

SSE Events:

SSE EventClient Action
node_state_updatedTrigger a state reconcile: refresh the local node state cache (metadata, data entries) and notify Node API consumers
policy_updatedTrigger a reconcile and update local firewall rules
bridge_config_updatedTrigger a bridge reconcile (bridge mode)
peer_endpoint_changed / peer_key_rotatedTrigger a state reconcile — peer topology is applied from the authoritative state pull, not the event payload
action_requestTrigger a state reconcile — the payload is opaque; action dispatches are delivered in the executions block of the authoritative state pull, never by the event (see Remote Actions and Hooks)
session_setupTrigger a state reconcile — the payload is opaque; the session is provisioned from the sessions block of the authoritative state pull, never from the event (see Secure Access Tunneling)
session_revokedTrigger a state reconcile — the payload is opaque; the session leaving the sessions block is what tears the listener down, so the reconcile's drain does the revocation
rotate_keysGenerate new Curve25519 keypair and initiate key rotation (see Phase 4: Key Rotation)
signing_key_rotatedUpdate the control plane's signing public key, selected by key_id (see Signing Key Rotation)

Phase 4: Key Rotation

Key rotation is triggered by the control plane - either on a schedule, by admin action, or in response to a compromised node. The rotate_keys signal arrives as a heartbeat flag or as a signed SSE event; the SSE event is verified before processing. The node stages a fresh keypair before submitting its public key, and the control plane replies with a rotation receipt (rotation_id, kid, wrap_key_version) rather than a peer list. The node swaps its private key only after that receipt, then picks up the new peers and PSKs on the next state pull. A repeated signal within five minutes of the last committed rotation is skipped, so a rotate_keys flag the control plane keeps set until it observes the new key cannot rekey the node on every heartbeat; a rotation whose key is already staged is always resubmitted.

When a node is force-removed from the control plane, all peers that had a tunnel to the compromised node receive a peer_deregistered event followed by fresh PSKs for their remaining peer pairs.

Signing Key Rotation

The control plane's Ed25519 signing key (used for SSE event signatures and session JWTs) can be rotated independently of WireGuard mesh keys. During rotation, both the old and the new key are valid for a transition period.

The signing_key_rotated event is signed with the current (old) key, which the node already trusts. This creates a chain of trust - each key vouches for its successor.

Pre-Shared Keys (PSK)

Each peer pair uses a unique PSK generated by the control plane and distributed to both peers. PSKs provide:

  • Post-quantum resistance: An additional symmetric key layer on top of the Curve25519 ECDH, protecting against future quantum attacks on elliptic-curve cryptography.
  • Defense in depth: Even if the Curve25519 key exchange is compromised, the PSK layer prevents decryption.

PSKs are rotated together with the main key pairs and whenever a peer is removed from the mesh.

Threat Model

ScenarioImpactMitigation
Control plane compromisedAttacker has signing key - can forge SSE events and inject malicious peersPSK layer for mesh traffic; signing key rotation to limit exposure window; admin-side integrity monitoring; nodes log all applied events for forensic analysis
Node compromisedAttacker has private key and NSK of one nodeForce-remove node, trigger key rotation + PSK refresh on all affected peers; rotate NSK to prevent decryption of future secrets; secrets are not cached so no plaintext on disk to exfiltrate
Bootstrap token stolenAttacker could register a rogue nodeOne-time-use + short TTL limits the attack window
MITM during registrationCould intercept public key exchange and signing keyTLS + server certificate validation on all control plane communication
MITM on SSE streamCould inject forged events (peer changes, action requests, key rotations)Ed25519 signature verification on every event; TLS as first layer; forged events rejected without valid signature
Signing key compromisedAttacker can forge SSE events until key is rotatedSigning key rotation via signing_key_rotated event (signed with current key); transition period for graceful rollover
Session token stolenAttacker could execute scoped actions on target nodeShort TTL, node-bound, scoped action list, revocation on session end
Unauthorized local action executionSSH user runs actions without permissionRequires valid session JWT; --local restricted to root and logged as emergency
Unauthorized secret access (local)Attacker on node reads secrets via socket or K8s SecretLinux/macOS: socket requires root or plexd-secrets; Windows: pipe requires an elevated Administrator or LocalSystem token; K8s Secrets contain only NSK-encrypted ciphertext; decryption requires plexd API with valid bearer token + live control plane
NSK compromisedAttacker could decrypt secret ciphertext from K8s Secrets or intercepted responsesNSK rotation invalidates old key; secrets are fetched in real-time so no historical ciphertext accumulates on-node; control plane re-encrypts with new NSK

Network Requirements

plexd requires the following network connectivity. All control plane communication is outbound-initiated from the node.

Node Mode

DirectionProtocolPortDestinationPurpose
OutboundTCP/443-Control plane APIRegistration, heartbeat, observability, log/audit forwarding, callbacks
OutboundTCP/443-Control plane SSEReal-time event stream (persistent connection)
OutboundUDP/3478, UDP/19302-STUN serversNAT type discovery, public endpoint detection
Inbound/OutboundUDP/5182051820Mesh peersWireGuard encrypted mesh traffic (P2P)

Bridge Mode (additional)

DirectionProtocolPortDestinationPurpose
InboundUDP/5182051820NAT relay clientsWireGuard relay for nodes behind symmetric NAT
InboundTCP/443443Public internetPublic ingress (if ingress.enabled)
InboundUDP/5182151821User access clientsWireGuard user access (if configured)
OutboundUDP/varies-Site-to-site peersVPN tunnels to external networks

Note: Nodes behind NAT do not need any inbound port forwarding. STUN discovery and relay fallback handle NAT traversal automatically.