Key Storage
plexd stores cryptographic keys and identity material in a single directory on the node filesystem. All key files are written atomically via fsutil.WriteFileAtomic and restricted to owner-only access. This page documents every key type, its purpose, storage format, lifecycle, and the security properties that protect it.
Overview
During registration, the control plane provisions the node with several cryptographic artifacts. These are persisted to data_dir so that the node can rejoin the mesh, decrypt secrets, and verify SSE events across restarts. The stored keys are:
- Curve25519 private key -- mesh encryption (WireGuard)
- Pre-Shared Keys (PSK) -- per-peer-pair post-quantum defense layer
- Node Secret Key (NSK) -- AES-256-GCM secret decryption
- Ed25519 signing public key -- SSE event and JWT verification
- Bootstrap token -- one-time registration credential (deleted after use)
Storage Location
All key material is stored under data_dir, which defaults to /var/lib/plexd/. The directory is created with mode 0700 if it does not already exist.
| Setting | Default | Description |
|---|---|---|
data_dir | /var/lib/plexd | Root directory for identity and key files |
The directory is also referenced by registration.data_dir and node_api.data_dir, which are propagated from the top-level data_dir at runtime. See the Configuration Reference for details.
Key Types
| File | Key Type | Algorithm | Purpose | Received From |
|---|---|---|---|---|
private_key | Curve25519 private key | X25519 (WireGuard) | Mesh traffic encryption and authentication | Generated locally during registration |
| (in-memory, per peer) | Pre-Shared Key (PSK) | 256-bit symmetric | Post-quantum defense layer for each peer pair | Control plane, delivered with peer configuration |
node_secret_key | Node Secret Key (NSK) | AES-256-GCM | Decrypts secret values fetched from the control plane | Control plane, issued during registration |
signing_public_key | Ed25519 public key | Ed25519 | Verifies SSE event signatures and session JWT tokens | Control plane, issued during registration |
identity.json | Node identity metadata | N/A | Stores node_id, mesh_ip, and signing_public_key | Control plane registration response |
| (bootstrap token file) | Bootstrap token | N/A | One-time authentication for initial registration | Provisioned externally (file, env var, or metadata service) |
Curve25519 Private Key
The Curve25519 keypair is generated locally by the node during registration (internal/registration). The private key is base64-encoded and written to data_dir/private_key. The corresponding public key is sent to the control plane as part of the registration request; it is not stored separately on disk.
WireGuard uses this keypair for all mesh traffic encryption. When keys are rotated, a new keypair is generated, the new public key is uploaded via POST /v1/keys/rotate, and the control plane returns an updated peer list.
Pre-Shared Keys (PSK)
PSKs are 256-bit symmetric keys assigned per peer pair by the control plane. They are delivered as part of the Peer structure (field psk, base64-encoded) and applied to the WireGuard interface at configuration time. PSKs provide a post-quantum defense layer: even if Curve25519 is broken in the future, an attacker who did not also compromise the PSK cannot decrypt captured traffic.
PSKs are not written to individual files on disk. They are part of the peer configuration that the WireGuard subsystem applies to the kernel interface. They are refreshed whenever the control plane distributes updated peer lists (e.g., after key rotation or reconciliation).
Node Secret Key (NSK)
The NSK is an AES-256 symmetric key used with GCM mode to decrypt secret values fetched from the control plane (GET /v1/nodes/{node_id}/secrets/{key}). It is received during registration and stored as a raw string in data_dir/node_secret_key.
The NSK can be rotated:
- Together with mesh keys -- when the control plane signals
rotate_keysvia heartbeat response or SSE event, the full key rotation flow (including NSK refresh) is triggered. - Independently -- the control plane can rotate the NSK without rotating the Curve25519 mesh keypair.
After rotation, the control plane re-encrypts all secrets with the new NSK. The old NSK is overwritten atomically on disk.
Ed25519 Signing Public Key
The control plane's Ed25519 signing public key is used by the node for two purposes:
- SSE event signature verification -- every
SignedEnvelopereceived on the SSE stream is verified against this key before dispatch (see Event Verification). - Session JWT validation -- session tokens presented during secure access tunneling are validated using the same key.
The key is stored as a raw string in data_dir/signing_public_key and also recorded in identity.json. On load, the file value takes precedence if it differs from the JSON field (a warning is logged).
During signing key rotation, the control plane sends a signing_key_rotated SSE event containing:
type SigningKeys struct {
Current string `json:"current"`
Previous string `json:"previous,omitempty"`
TransitionExpires *time.Time `json:"transition_expires,omitempty"`
}Both the current and previous keys are held in memory for the transition period (TransitionExpires), allowing events signed with either key to pass verification. Once the transition expires, only the current key is used.
Bootstrap Token
The bootstrap token is a one-time credential used to authenticate the initial registration request. It can be sourced from a file (/etc/plexd/bootstrap-token), an environment variable (PLEXD_BOOTSTRAP_TOKEN), a direct value, or a cloud metadata service.
The token is deleted from disk immediately after successful registration to prevent reuse:
// 10. Delete token file if applicable.
if tokenResult.FilePath != "" {
if err := os.Remove(tokenResult.FilePath); err != nil {
r.logger.Warn("failed to delete token file", ...)
}
}File Permissions
All key files are written with mode 0600 (owner read/write only). The data_dir directory itself is created with mode 0700. Files are written atomically using fsutil.WriteFileAtomic, which writes to a temporary file first and then renames it into place, ensuring readers never observe a partially-written file.
| Path | Mode | Contents |
|---|---|---|
data_dir/ | 0700 | Key storage directory |
data_dir/identity.json | 0600 | node_id, mesh_ip, signing_public_key (JSON) |
data_dir/private_key | 0600 | Base64-encoded Curve25519 private key |
data_dir/node_secret_key | 0600 | Raw AES-256 Node Secret Key |
data_dir/signing_public_key | 0600 | Raw Ed25519 public key string |
The plexd process should run as a dedicated plexd user. No other user or group needs read access to these files.
Key Lifecycle
Generation and Initial Storage
- Bootstrap token is provisioned out-of-band (file, env var, metadata).
- Node generates a Curve25519 keypair locally.
- Node calls
POST /v1/nodes/registerwith the bootstrap token and public key. - Control plane responds with
node_id,mesh_ip,node_secret_key,signing_public_key, and peer list (including PSKs). SaveIdentitywritesidentity.json,private_key,node_secret_key, andsigning_public_keyatomically with0600permissions.- Bootstrap token file is deleted from disk.
Rotation
Key rotation can be triggered in two ways:
- Heartbeat response -- the control plane sets
rotate_keys: truein theHeartbeatResponse, causing the node to trigger reconciliation which performs the rotation. - SSE events -- specific events signal individual key rotations.
Mesh Key Rotation (rotate_keys)
- Control plane signals rotation via heartbeat or
rotate_keysSSE event. - Node generates a new Curve25519 keypair.
- Node calls
POST /v1/keys/rotatewith the new public key. - Control plane responds with updated peers (new PSKs included).
- Node updates the WireGuard interface and overwrites
private_keyon disk.
NSK Rotation
The NSK is rotated together with mesh keys or independently via the control plane. After rotation:
- The new NSK is written atomically to
data_dir/node_secret_key, overwriting the old value. - The control plane re-encrypts all secrets with the new NSK.
- Secrets are fetched in real-time (not cached as plaintext), so no historical ciphertext accumulates on-node.
Signing Key Rotation (signing_key_rotated)
- Control plane sends a
signing_key_rotatedSSE event (signed with the current key). - The event payload contains the new
currentkey, thepreviouskey, and atransition_expirestimestamp. - The node's
EventVerifieris updated to accept signatures from both keys for the transition period. - After the transition expires, only the new key is used for verification.
Deletion
- Bootstrap token: deleted immediately after successful registration.
- All identity files: removed during explicit deregistration (
plexd deregister), which also deletes the token file if present.
Security Considerations
- File permissions: all key files use
0600; the data directory uses0700. Only the plexd process owner can read key material. - Atomic writes:
fsutil.WriteFileAtomicensures that a crash during a write never leaves a corrupted or partial key file on disk. - No plaintext secret caching: secrets are decrypted in memory on demand via the NSK. No plaintext secret values are persisted to disk.
- One-time bootstrap token: the token is deleted after registration, limiting the window for token theft and replay.
- Transition period for signing keys: during rotation, both old and new signing keys are accepted, preventing event verification failures during rollover. After the transition expires, only the new key is trusted.
- PSK as post-quantum layer: even if the Curve25519 key exchange is compromised (e.g., by a future quantum computer), the per-peer PSK provides an additional symmetric encryption layer that protects captured traffic.
- NSK rotation invalidates old ciphertext: after NSK rotation, secrets encrypted with the old key cannot be decrypted by the node. The control plane re-encrypts with the new NSK.
- TLS on all control plane communication: key material is exchanged over TLS-protected channels, mitigating interception during registration and key rotation.
Source Reference
| Component | Source |
|---|---|
| Identity persistence | internal/registration/identity.go |
| Registration flow | internal/registration/registrar.go |
| Atomic file writes | internal/fsutil/atomic.go |
| Signing key rotation handler | cmd/plexd/cmd/up.go |
| WireGuard peer key rotation | internal/wireguard/handler.go |
| Key rotate API endpoint | internal/api/endpoints.go |
| Event types | internal/api/envelope.go |
| Signing key types | internal/api/types.go |