Configuration Reference
plexd reads its configuration from a YAML file (default on Linux: /etc/plexd/config.yaml, overridable with --config; see Platform defaults for macOS and Windows). The file is optional: when no file exists at that path, plexd continues with an empty configuration — defaults apply, and CLI flags and environment variables supply the rest — and logs a single warn-level message naming the path it did not find, so a mistyped --config stays visible. A file that exists but cannot be read, a file that is empty, and a file that is not valid YAML, are startup errors that name the path. An empty file is rejected rather than treated as an absent one: a truncated write or a ConfigMap key that rendered to nothing is a broken configuration, not a decision to run without one.
The configuration a command runs on is assembled in four steps:
- Unmarshal the YAML into an
AgentConfigstruct — or start from an empty struct when the file is absent (ParseConfig()ininternal/agent/config.go) - ApplyDefaults for every zero-valued field
- Merge overrides — CLI flag values, then the
PLEXD_*environment overrides - Validate the merged configuration (returns the first error encountered)
Because validation runs on the merged result, a required value may come from any layer: plexd up --api https://api.example.com starts with no config file at all, and with a file that omits api.base_url.
Precedence order (highest to lowest):
- CLI flags (
--api,--mode,--log-level) applyEnvOverrides()—PLEXD_*environment variables (see Environment Variables)- Global env vars via
envOrDefault()(PLEXD_CONFIG,PLEXD_LOG_LEVEL,PLEXD_API,PLEXD_MODE) - Values in the YAML config file
ApplyDefaults()for zero-valued fields
Running without a config file
plexd can run entirely from flags and environment variables. For a fresh registration the file-less minimum is PLEXD_API, PLEXD_PROJECT_ID, PLEXD_RESOURCE_HANDLE, and PLEXD_BOOTSTRAP_TOKEN — or the equivalent --api, --project-id, and --resource-handle flags plus the token. Every other field falls back to its default.
Fields that have no flag or environment override remain reachable only through the file, which stays the primary configuration surface for host installs.
One default is inverted on this path: actions.enabled comes up false without a config file, where a file that omits the actions block leaves it true. actions.enabled is the only switch that stops the control plane from executing actions and hooks on the node, so a file that has gone missing — a deleted ConfigMap, a mistyped --config, a broken mount — must not silently enable it. A deliberately file-less deployment that wants action execution sets PLEXD_ACTIONS_ENABLED=true.
For the full list of overrides, see Environment Variables.
Platform defaults
plexd resolves its configuration, data and runtime directories from the host it runs on. Every value below is a default: --config, PLEXD_CONFIG, the YAML keys and the PLEXD_* overrides replace it, and they take precedence on every platform.
| Path | Linux | macOS | Windows |
|---|---|---|---|
Configuration file (--config, PLEXD_CONFIG) | /etc/plexd/config.yaml | /Library/Application Support/plexd/config.yaml | %ProgramData%\plexd\config.yaml |
data_dir | /var/lib/plexd | /Library/Application Support/plexd/data | %ProgramData%\plexd\data |
registration.token_file | /etc/plexd/bootstrap-token | /Library/Application Support/plexd/bootstrap-token | %ProgramData%\plexd\bootstrap-token |
actions.hooks_dir | /etc/plexd/hooks | /Library/Application Support/plexd/hooks | %ProgramData%\plexd\hooks |
node_api.socket_path | /var/run/plexd/api.sock | /var/run/plexd/api.sock | \\.\pipe\plexd |
Runtime directory (plexd install) | /var/run/plexd | /var/run/plexd | %ProgramData%\plexd\run |
Binary (plexd install) | /usr/local/bin/plexd | /usr/local/bin/plexd | %ProgramFiles%\plexd\plexd.exe |
| Service definition | /etc/systemd/system/plexd.service | /Library/LaunchDaemons/com.plexsphere.plexd.plist | SCM service plexd (no file) |
| Service log | journald | /Library/Logs/plexd/plexd.log | Application Event Log, source plexd |
%ProgramData% is the ProgramData environment variable, which Windows sets for every process including services. plexd falls back to C:\ProgramData when it is unset or empty. %ProgramFiles% is read the same way, with C:\Program Files as its fallback.
macOS resolves the system locations only, with no per-user fallback under ~/Library. The CLI reaches the node API socket without knowing who started the daemon, so a per-user runtime directory would send plexd status looking for a socket a root daemon never created. An unprivileged macOS run sets --config and data_dir itself.
The sections below show the Linux value in their Default column.
Top-Level Fields
| Field | Type | Default | Description |
|---|---|---|---|
mode | string | node | Operating mode: node or bridge. Bridge mode enables bridge-specific subsystems when bridge.enabled: true. |
log_level | string | info | Log level: debug, info, warn, error |
data_dir | string | /var/lib/plexd (Linux) | Directory for persistent agent data. Propagated to registration.data_dir and node_api.data_dir at runtime. Per platform; see Platform defaults. |
api
Control plane HTTP client configuration.
| Field | Type | Default | Description |
|---|---|---|---|
base_url | string | — (required) | Control plane API base URL, e.g. https://api.plexsphere.com. Required in the merged configuration — the value may come from the file, --api, or PLEXD_API. |
tls_insecure_skip_verify | bool | false | Disable TLS certificate verification. WARNING: Only for development/testing. |
connect_timeout | duration | 10s | Maximum time to wait for a TCP connection |
request_timeout | duration | 30s | Maximum time for a complete HTTP request/response cycle. On the SSE event stream it bounds only the setup: the wait for the response headers, and the problem body of a non-2xx answer. It does not cut an open stream. |
sse_idle_timeout | duration | 90s | Maximum idle time on the SSE stream before reconnecting. It is the only bound on an open stream: any byte resets it, the control plane's keep-alive comments included. Must not be negative. |
sse_reprobe_interval | duration | 10m | How often pull-only delivery re-probes the SSE endpoint after the control plane descoped it. Must be at least 1s when set. |
Source: internal/api/config.go
registration
Node registration and bootstrap authentication.
| Field | Type | Default | Description |
|---|---|---|---|
project_id | string | — | Platform project UUID to register into. Required for fresh registration (validated at registration time, not config parse). |
resource_handle | string | — | Platform Resource handle to bind to. Required for fresh registration. |
requested_resource_id | string | — | Optional resource ID override used when substrate naming differs from the platform handle. |
token_file | string | /etc/plexd/bootstrap-token (Linux) | Path to the bootstrap token file. Per platform; see Platform defaults. |
token_env | string | PLEXD_BOOTSTRAP_TOKEN | Environment variable name for the bootstrap token |
token_value | string | — | Direct token value override |
use_metadata | bool | false | Enable the cloud metadata service (IMDS) as a fallback source for registration inputs |
metadata_token_path | string | /plexd/bootstrap-token | Metadata key path for the bootstrap token (e.g. IMDS) |
metadata_timeout | duration | 2s | Maximum time to wait for a metadata service response |
max_retry_duration | duration | 5m | Maximum duration to retry registration |
data_diris propagated from the top-leveldata_dirat runtime. It does not appear in the YAML.
Source: internal/registration/config.go
heartbeat
Periodic heartbeat reporting to the control plane.
| Field | Type | Default | Description |
|---|---|---|---|
interval | duration | 30s | Heartbeat send interval. It also paces the handshake report (POST /v1/nodes/{node_id}/handshakes). Keep it at or below the Domain's reachability heartbeat interval: the control plane floors its edge thresholds at 180s plus the Domain's interval, so a longer local interval makes mesh edges read stale between reports. |
node_idis set at runtime from the registration identity. It does not appear in the YAML.
Source: internal/agent/heartbeat.go
reconcile
State reconciliation loop.
| Field | Type | Default | Description |
|---|---|---|---|
interval | duration | 60s | Time between reconciliation cycles. Minimum: 1s. |
Source: internal/reconcile/config.go
node_api
Local node API server (Unix socket and optional HTTP).
| Field | Type | Default | Description |
|---|---|---|---|
socket_path | string | /var/run/plexd/api.sock (Linux, macOS) | Unix socket path; on Windows the named pipe name. Per platform; see Platform defaults. |
http_enabled | bool | false | Enable the optional HTTP listener |
http_listen | string | 127.0.0.1:9100 | HTTP listen address |
http_token_file | string | — | Path to the HTTP bearer token file |
debounce_period | duration | 5s | Debounce period for coalescing events |
shutdown_timeout | duration | 5s | Maximum time to wait for graceful shutdown |
data_diris propagated from the top-leveldata_dirbyApplyDefaults();secret_auth_enabledis set at runtime byplexd up. Neither appears in the YAML.
Source: internal/nodeapi/config.go
health
Dedicated listener for the unauthenticated Kubernetes probe endpoints /healthz and /readyz. It is independent of node_api, so a probe never needs a credential.
| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Run the health listener. plexd up starts it before registration begins, so a probe during a slow first registration gets an answer instead of a refused connection. On by default because the shipped DaemonSet probes both endpoints unconditionally: an omitted health block would leave the probe target unbound, and the resulting liveness failures restart the container in a loop that tears down the WireGuard interface and the firewall chain every time. Set it to false only when nothing probes the node. Also settable as PLEXD_HEALTH_ENABLED. |
listen | string | "127.0.0.1:9101" | Listen address. The default binds loopback deliberately: the endpoints are unauthenticated, and under hostNetwork: true a wildcard bind answers on every node NIC and — once the mesh is up — to every WireGuard peer. The kubelet probes from the host network namespace, which is the namespace plexd listens in, so a probe with host: 127.0.0.1 reaches a loopback-bound listener. Widening this to ":9101" is an explicit opt-in; adjust the probes' host to match. Also settable as PLEXD_HEALTH_LISTEN, which is what a Pod-network deployment running without a config file uses — there the kubelet dials the Pod IP and a loopback-bound listener answers nothing. |
/healthz answers 200 for as long as the process serves requests — it reports liveness, not control-plane reachability. /readyz answers 200 once all readiness conditions hold; otherwise it answers 503 naming the first unmet one, in the order the agent establishes them:
| Condition | 503 body when unmet |
|---|---|
| The node holds a registered identity | not ready: registration pending |
| The WireGuard interface is up and the deny-by-default firewall baseline is installed | not ready: data plane not configured |
| The WireGuard interface is still present and up | not ready: data plane lost |
| The control plane accepts the node's credential | not ready: node credential refused |
| Event delivery to the control plane is working | not ready: event delivery stopped or not ready: event delivery degraded |
| Every long-running subsystem is still running | not ready: subsystem stopped |
WireGuard setup failure is non-fatal — the agent logs a warning and keeps running — so the data-plane condition is what keeps such a node out of rotation instead of letting it report ready without a tunnel. On Kubernetes this also stops a DaemonSet rolling update from marching across the fleet when a kernel or capability regression breaks WireGuard everywhere.
The data plane is re-checked rather than latched at startup, so an interface deleted or brought down hours later takes the node out of rotation — and puts it back once the interface returns, without a restart. A background poller runs the check every 5 seconds and probes read its last verdict; the endpoints are unauthenticated, so running the check per request would let any caller that reaches the port drive one interface-table dump per GET. The check covers the interface only: a firewall baseline flushed by another actor on the node stays undetected.
not ready: subsystem stopped means a goroutine that should run for the life of the process exited before shutdown — the reconciler or the node API server. Nothing restarts them and /healthz stays 200 by design, so readiness is the only signal; the log line names which subsystem, the response body never does.
For the delivery condition, streaming and pull_only both count as a working path and degraded_polling does not. not ready: event delivery stopped is separate: it means the SSE goroutine exited for good — a permanent failure or a rejected node secret — which the delivery mode alone cannot express, because the reconnect engine returns without ever transitioning it.
not ready: node credential refused means the heartbeat parked after three refused heartbeats: the Node was removed from the control plane or its credential revoked (see Credential Park). It is checked before event delivery because the SSE stream stops on the same refusal, and not ready: event delivery stopped would otherwise hide the cause. It clears when a heartbeat probe is accepted again. A stream that stopped is not restarted then, so the node answers not ready: event delivery stopped until a restart, while the reconciler's pull keeps it converged.
Two consequences are worth knowing before you alert on readiness: the mode only turns to degraded_polling after the SSE polling-fallback window (default 5m), so a node whose stream just broke still reports ready until the window elapses; and a node reports 200 in the short window between its data plane coming up and its first SSE connect. A restart that loads a persisted identity is ready without a fresh registration round-trip.
For the delivery modes themselves, see Reading a Node's Event Delivery Mode.
Source: internal/health/config.go
actions
Remote action execution and hook management.
| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Enable action execution. An omitted key leaves it true; an explicit false disables execution and survives defaulting. Without a config file it defaults to false — see Running without a config file. |
hooks_dir | string | /etc/plexd/hooks (Linux) | Directory containing hook scripts. Per platform; see Platform defaults. |
max_concurrent | int | 5 | Maximum number of concurrent actions. Minimum: 1. |
max_action_timeout | duration | 10m | Maximum duration for a single action. Minimum: 10s. |
max_output_bytes | int64 | 1048576 (1 MiB) | Maximum output size per action in bytes. Minimum: 1024. |
Upgrading from a release that used the zero-value heuristic
Earlier releases derived enabled from the other fields: ApplyDefaults() set it to true only when max_concurrent, max_action_timeout, and max_output_bytes were all zero. A config file that set any one of them without an enabled key therefore ran with action execution off.
That heuristic is gone. enabled now means what it says, so a file of that shape comes up with action execution on after the upgrade, with no config change. If those nodes are meant to stay off, add an explicit actions.enabled: false before rolling the binary forward. plexd up reports the effective value as actions_enabled in its startup log line, and config.dump reports it under actions.enabled.
Source: internal/actions/config.go
upgrade
Release download and Sigstore bundle verification for the service.upgrade action.
| Field | Type | Default | Description |
|---|---|---|---|
release_base_url | string | https://github.com/plexsphere/plexd/releases/download | Base URL of the release download channel. Assets are fetched from {release_base_url}/{tag}/{asset}. Air-gapped setups point this at a mirror. |
signing_identity_regexp | string | ^https://github\.com/plexsphere/plexd/\.github/workflows/release\.yml@refs/tags/v.+$ | Regexp the signing certificate's SAN must match. Compiled at validation; a malformed pattern fails with upgrade: config: compile signing_identity_regexp: .... |
signing_issuer | string | https://token.actions.githubusercontent.com | Exact OIDC issuer the signing certificate must carry. |
trusted_root_path | string | — (empty) | Path to a Sigstore trusted root JSON file. Empty uses the embedded Sigstore public-good trusted root. |
Source: internal/upgrade/config.go
integrity
Binary and hook integrity verification.
| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Enable integrity verification. Defaults to true when verify_interval is zero. |
binary_path | string | — | Path to the plexd binary to verify |
hooks_dir | string | — | Directory containing hook scripts to verify. When empty, mirrors actions.hooks_dir. |
verify_interval | duration | 5m | Interval between integrity verification runs. Minimum: 30s. |
watch_enabled | bool | true | Enable inotify file watching. When enabled, file changes in hooks_dir trigger immediate checksum recomputation. |
Source: internal/integrity/config.go
metrics
System metrics collection and reporting.
| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Enable metrics collection. Defaults to true via zero-value heuristic. |
collect_interval | duration | 15s | Interval between collection cycles. Minimum: 5s. |
report_interval | duration | 60s | Interval between reporting to the control plane. Minimum: 10s. Must be >= collect_interval. |
batch_size | int | 100 | Maximum number of metric points per report batch. Minimum: 1. |
local_endpoint.url | string | — | HTTPS URL for a local metrics endpoint. Must use https:// scheme. Empty means not configured. |
local_endpoint.secret_key | string | — | Authentication credential for the local endpoint. Required when url is set. Redacted in config dumps. |
local_endpoint.tls_insecure_skip_verify | bool | false | Disable TLS certificate verification for the local endpoint. |
For a step-by-step guide to configuring local endpoints, see Setting Up Local Endpoint Delivery.
Source: internal/metrics/config.go
log_fwd
Log collection and forwarding.
| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Enable log forwarding. Defaults to true via zero-value heuristic. |
collect_interval | duration | 10s | Interval between collection cycles. Minimum: 5s. |
report_interval | duration | 30s | Interval between reporting to the control plane. Must be >= collect_interval. |
batch_size | int | 200 | Maximum number of log entries per report batch. Minimum: 1. |
file_patterns | []string | — | Glob patterns for file-based log collection, e.g. ["/var/log/app/*.log"]. The daemon's own log is collected by the platform log source, so listing it here forwards every one of its lines twice. |
filter.min_severity | string | — | Drop log entries below this severity level (syslog severities: emerg, alert, crit, err, warning, notice, info, debug) |
filter.include_units | []string | — | Only pass entries matching one of these unit names. The unit name is the systemd unit on Linux (plexd.service for the daemon) and plexd on macOS and Windows. |
filter.exclude_units | []string | — | Drop entries matching any of these unit names. Takes precedence over include_units. The unit name is the systemd unit on Linux (plexd.service for the daemon) and plexd on macOS and Windows. |
local_endpoint.url | string | — | HTTPS URL for a local log forwarding endpoint. Must use https:// scheme. Empty means not configured. |
local_endpoint.secret_key | string | — | Authentication credential for the local endpoint. Required when url is set. Redacted in config dumps. |
local_endpoint.tls_insecure_skip_verify | bool | false | Disable TLS certificate verification for the local endpoint. |
For a step-by-step guide to configuring local endpoints, see Setting Up Local Endpoint Delivery.
Source: internal/logfwd/config.go, internal/logfwd/filter.go
audit_fwd
Audit event collection and forwarding.
| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Enable audit forwarding. Defaults to true via zero-value heuristic. |
collect_interval | duration | 5s | Interval between collection cycles. Minimum: 1s. |
report_interval | duration | 15s | Interval between reporting to the control plane. Must be >= collect_interval. |
batch_size | int | 500 | Maximum number of audit entries per report batch. Minimum: 1. |
local_endpoint.url | string | — | HTTPS URL for a local audit forwarding endpoint. Must use https:// scheme. Empty means not configured. |
local_endpoint.secret_key | string | — | Authentication credential for the local endpoint. Required when url is set. Redacted in config dumps. |
local_endpoint.tls_insecure_skip_verify | bool | false | Disable TLS certificate verification for the local endpoint. |
For a step-by-step guide to configuring local endpoints, see Setting Up Local Endpoint Delivery.
Source: internal/auditfwd/config.go
wireguard
WireGuard interface and peer management. See WireGuard Tunnel Management.
| Field | Type | Default | Description |
|---|---|---|---|
interface_name | string | plexd0 | Name of the WireGuard interface. On macOS the kernel names the device utunN; this value still keys the device, its UAPI socket (wg show plexd0) and the log lines, and a value of the form utunN requests that unit. See macOS controller. On Windows the value names the Wintun adapter itself, and its GUID is derived from it: see Windows controller |
listen_port | int | 51820 | UDP listen port for WireGuard |
mtu | int | 0 | MTU for the WireGuard interface. 0 sizes the interface to the uplink: plexd up measures the uplink's path MTU with the STUN servers in nat.stun_servers at startup and every 10 minutes, and sets the MTU to the path MTU minus 80 bytes, between 1280 and 1420. A positive value pins the MTU and turns detection off; 1420 pins the default plexd used before detection. Detection also needs nat.enabled. See MTU detection. |
edge_psk | bool | true | Fetch each edge's PSK from the control plane and program it on the peer. An omitted key means enabled; an explicit false is preserved. Takes effect only when the WireGuard interface came up. See Edge PSKs. |
persistent_keepalive | duration | 25s | Keepalive interval written on every mesh peer. It holds open the NAT mapping a node opened by dialing out, and endpoint selection depends on it to try a newly programmed endpoint. 0s turns the keepalive off, clears one an earlier run left on the device, and turns endpoint selection off, so every peer keeps the first entry of its dial order. Must be a whole number of seconds between 0s and 65535s. See Endpoint selection. |
WireGuard needs the same PSK on both ends of a tunnel, and the NAT relay forwards the WireGuard packets end to end, so an edge between a plexd that installs edge PSKs and an older plexd fails, direct and relayed, until both ends match. During a rolling upgrade, set edge_psk: false on the upgraded nodes, and set it back to true (or remove the key) once every node of the Domain runs a plexd that installs edge PSKs. A node with the setting off and its WireGuard interface up logs edge psk disabled by wireguard.edge_psk when plexd up starts. See Turning edge PSKs off.
Source: internal/wireguard/config.go
nat
STUN-based NAT traversal. See NAT Traversal via STUN.
| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Enable NAT traversal. false also turns mesh MTU detection off, so the WireGuard interface comes up at 1420 unless wireguard.mtu pins another value (see MTU detection). Defaults to true via zero-value heuristic: a nat block that sets none of stun_servers, refresh_interval, timeout and min_report_interval is read as enabled, so enabled: false alone keeps detection on. Set one of those fields beside it (see Config). |
stun_servers | []string | (built-in list) | List of STUN server addresses (host:port) |
refresh_interval | duration | 60s | Interval between STUN binding refreshes. Minimum: 10s. |
timeout | duration | 5s | Per-server STUN request timeout |
min_report_interval | duration | 10s | Floor on the endpoint report cadence derived from the control plane's stale_after. A short server-side deadline can otherwise drive STUN queries and endpoint reports above refresh_interval; raise this to bound how far the control plane may accelerate the loop. Must be positive. |
local_candidates | bool | true | Publish the node's local interface addresses, joined with wireguard.listen_port, as endpoint candidates. An omitted key means enabled; an explicit false is preserved. Takes effect only when the WireGuard interface came up, and does not depend on enabled. Peers still receive this node's peer-observed endpoints, which come from other nodes' handshake reports (see Peer-observed endpoints). See Local endpoint candidates. |
candidate_interfaces | []string | [] (every eligible interface) | Interface names to publish candidates from. A listed name is published even when its name marks it as virtual (docker0, br-...). An empty or whitespace-only entry fails validation, also when enabled is false. |
local_candidates and candidate_interfaces are read from the nat block only: plexd up does not read them from peer_exchange. The candidate publisher reports on its own cadence, which follows stale_after, refresh_interval and min_report_interval as the endpoint report does. Peers on the same network reach a published candidate only when UDP wireguard.listen_port is open on that local address.
Source: internal/nat/config.go
peer_exchange
Peer endpoint discovery and exchange. See Peer Endpoint Exchange.
Embeds nat.Config — inherits all NAT fields.
Source: internal/peerexchange/config.go
policy
Network policy enforcement and firewall rules. See Network Policy Enforcement.
| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Enable policy enforcement. An omitted key means enabled; an explicit false is preserved. Also settable as PLEXD_POLICY_ENABLED, which is how a deployment without a config file reaches the opt-out below. |
chain_name | string | plexd-mesh | Name of the nftables chain on Linux; on macOS the chain's comment line in the pf anchor, on Windows the prefix of the WFP filter names |
Policy enforcement needs CAP_NET_ADMIN. On macOS it needs root and a pf main ruleset that references anchor "com.apple/*", which Apple's /etc/pf.conf does; on Windows it needs Administrator. With enforcement on, plexd up probes the backend before it registers and exits if the probe fails — a node that cannot install the deny-by-default baseline must not spend its one-shot bootstrap token first. The failure names the capability and this opt-out:
plexd up: firewall baseline pre-flight: policy enforcement needs CAP_NET_ADMIN,
grant it to the container or set policy.enabled: false to run this node without
enforcement: policy: preflight: policy: nftables: probe: netlink receive:
operation not permittedOn macOS and Windows that failure names the platform's own requirement instead, with the hints policy enforcement needs root and a pf main ruleset that references anchor "com.apple/*" and policy enforcement needs Administrator; see pf & WFP Firewall Controllers.
Grant the capability (see Kubernetes Deployment) or set enabled: false — or, without a file to set it in, PLEXD_POLICY_ENABLED=false. There is no middle setting: a node told to enforce that cannot enforce fails closed rather than joining the mesh unfiltered.
Migration: macOS and Windows had no firewall backend before this release, so
policy.enabled: true— the default — was a silent no-op there. Both platforms now enforce: a node without root (macOS) or Administrator (Windows) fails the pre-flight instead of starting, and a privileged node also filters traffic addressed to itself, which the Linux forward hook never did. Setpolicy.enabled: falsebefore rolling the binary forward to keep the previous behaviour, or add rules for the mesh services the node hosts.
Migration:
enabledwas previously resolved by a zero-value heuristic that guessed intent fromchain_name. Apolicyblock naming a chain without anenabledkey was read as disabled — such a node ran unenforced and now comes up enforcing, which on a host withoutCAP_NET_ADMINmeans it fails the pre-flight instead of starting. A block setting onlyenabled: falsewas forced back on and now takes effect as written. Spellenabledout to be certain of either.
Source: internal/policy/config.go
tunnel
Secure tunnel access for services. See Secure Access Tunneling.
| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Enable secure access tunneling |
max_sessions | int | 10 | Maximum concurrent tunnel sessions |
default_timeout | duration | 30m | Default session timeout |
ssh_listen_addr | string | — | SSH server listen address. If empty, the SSH server is not started. |
host_key_dir | string | — | Directory for the host key (defaults to data_dir) |
ssh_sessions_enabled | bool | true | Serve mediated ssh sessions (Linux and macOS). false settles ssh entries with a warning while tcp sessions keep working. A Mac upgraded to this release serves ssh sessions unless it sets ssh_sessions_enabled: false. See SSH Sessions |
session_signing_public_key | string | — | Standard-base64 Ed25519 key the session helper verifies ssh session tokens against. It wins over the key the helper pins from identity.json into /etc/plexd/session-signing-key on its first run; set it after a Domain signing-key rotation. Read only by Linux's socket-activated session helper; unused on macOS. A malformed value fails validation even with enabled: false. See Trust anchor |
Source: internal/tunnel/config.go
bridge
Gateway bridge mode operation. Active when mode: bridge and bridge.enabled: true. See Bridge Mode.
| Field | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Enable bridge mode |
access_interface | string | eth1 | Access-side network interface. The kernel name on Linux and macOS (eth1, en1); on Windows the adapter's friendly name, as Get-NetAdapter lists it (Ethernet) |
access_subnets | []string | — | Subnets routable via the access interface |
enable_nat | bool | true | Enable NAT masquerading on the access interface. On macOS the masquerade is a nat rule in plexd's pf anchor; on Windows a WinNAT object scoped to the mesh prefix, which translates mesh-sourced traffic only. See pf & WFP Firewall Controllers |
relay_enabled | bool | false | Enable UDP relay for NAT traversal |
relay_listen_port | int | 51821 | Relay UDP listen port |
max_relay_sessions | int | 100 | Maximum concurrent relay sessions |
session_ttl | duration | 5m | Relay session TTL |
ingress_enabled | bool | false | Enable public ingress |
max_ingress_rules | int | 20 | Maximum ingress rules |
ingress_dial_timeout | duration | 10s | Timeout for dialing backend targets |
user_access_enabled | bool | false | Enable user access VPN integration |
user_access_interface_name | string | wg-access | WireGuard interface for user access |
user_access_listen_port | int | 51822 | User access WireGuard listen port |
max_access_peers | int | 50 | Maximum user access peers |
user_access_provider_type | string | — | External VPN provider for user access: tailscale, netbird, or empty (disabled) |
auth_key_env | string | — | Environment variable name containing the auth key for the user access provider (e.g. PLEXD_TAILSCALE_AUTH_KEY) |
site_to_site_enabled | bool | false | Enable site-to-site VPN |
site_to_site_interface_prefix | string | wg-s2s- | Prefix for site-to-site WireGuard interfaces |
site_to_site_listen_port | int | 51823 | Base listen port for site-to-site tunnels |
max_site_to_site_tunnels | int | 10 | Maximum site-to-site tunnels |
tunnel_providers | []string | — | Tunnel provider types for site-to-site: ipsec, openvpn. Empty means WireGuard-only. |
acme_enabled | bool | false | Enable ACME TLS certificate management |
acme_cache_dir | string | — | Directory for ACME certificate caching |
acme_allowed_hosts | []string | — | Hostnames allowed for ACME certificates |
acme_email | string | — | ACME account contact email |
acme_directory_url | string | — | Override ACME directory URL (default: Let's Encrypt production) |
Source: internal/bridge/config.go
Complete Annotated Example
# /etc/plexd/config.yaml — Complete example with all sections
mode: node # "node" or "bridge" (default: node)
log_level: info # debug | info | warn | error (default: info)
data_dir: /var/lib/plexd # persistent data directory (default: /var/lib/plexd)
api:
base_url: https://api.plexsphere.com # required
tls_insecure_skip_verify: false
connect_timeout: 10s
request_timeout: 30s
sse_idle_timeout: 90s
sse_reprobe_interval: 10m
registration:
# project_id: "" # platform project UUID (required for registration)
# resource_handle: "" # platform resource handle (required for registration)
# requested_resource_id: "" # optional resource id override
token_file: /etc/plexd/bootstrap-token
token_env: PLEXD_BOOTSTRAP_TOKEN
# token_value: "" # direct token override
use_metadata: false
metadata_token_path: /plexd/bootstrap-token
metadata_timeout: 2s
max_retry_duration: 5m
heartbeat:
interval: 30s # also paces the handshake report
reconcile:
interval: 60s # min: 1s
node_api:
socket_path: /var/run/plexd/api.sock
http_enabled: false
http_listen: 127.0.0.1:9100
# http_token_file: ""
debounce_period: 5s
shutdown_timeout: 5s
health:
enabled: true # dedicated /healthz + /readyz listener (default: true)
listen: "127.0.0.1:9101" # loopback: the endpoints are unauthenticated
actions:
enabled: true
hooks_dir: /etc/plexd/hooks
max_concurrent: 5 # min: 1
max_action_timeout: 10m # min: 10s
max_output_bytes: 1048576 # min: 1024 (1 MiB)
upgrade:
release_base_url: https://github.com/plexsphere/plexd/releases/download
signing_identity_regexp: '^https://github\.com/plexsphere/plexd/\.github/workflows/release\.yml@refs/tags/v.+$'
signing_issuer: https://token.actions.githubusercontent.com
# trusted_root_path: "" # empty = embedded Sigstore public-good root
integrity:
enabled: true
# binary_path: "" # path to plexd binary
# hooks_dir: "" # empty = mirrors actions.hooks_dir
verify_interval: 5m # min: 30s
watch_enabled: true
metrics:
enabled: true
collect_interval: 15s # min: 5s
report_interval: 60s # min: 10s, >= collect_interval
batch_size: 100 # min: 1
# local_endpoint:
# url: https://metrics.local:9090/ingest
# secret_key: local-metrics-token
# tls_insecure_skip_verify: false
log_fwd:
enabled: true
collect_interval: 10s # min: 5s
report_interval: 30s # >= collect_interval
batch_size: 200 # min: 1
# file_patterns:
# - /var/log/app/*.log
# filter:
# min_severity: warning
# include_units:
# - sshd.service
# - plexd # the daemon itself on macOS and Windows
# exclude_units:
# - cron.service
# local_endpoint:
# url: https://logs.local:9090/ingest
# secret_key: local-logs-token
# tls_insecure_skip_verify: false
audit_fwd:
enabled: true
collect_interval: 5s # min: 1s
report_interval: 15s # >= collect_interval
batch_size: 500 # min: 1
# local_endpoint:
# url: https://audit.local:9090/ingest
# secret_key: local-audit-token
# tls_insecure_skip_verify: false
wireguard:
interface_name: plexd0
listen_port: 51820
# mtu: 0 # 0 = sized to the uplink; a positive value pins it
# edge_psk: true # false during a rolling upgrade, until every node installs edge PSKs
# persistent_keepalive: 25s # 0s turns the keepalive and endpoint selection off
nat:
enabled: true # false also turns mesh MTU detection off
# stun_servers: # default built-in list
refresh_interval: 60s # min: 10s
timeout: 5s
min_report_interval: 10s # floor on the stale_after-driven cadence
# local_candidates: true # false publishes no local endpoint candidates
# candidate_interfaces: [] # empty = every eligible interface; read from nat only
peer_exchange:
enabled: true
refresh_interval: 60s
timeout: 5s
policy:
enabled: true # needs CAP_NET_ADMIN (Linux), root (macOS) or Administrator (Windows); false to run unenforced
chain_name: plexd-mesh
tunnel:
enabled: true
max_sessions: 10
default_timeout: 30m
# ssh_listen_addr: "" # empty = SSH server not started
# host_key_dir: "" # defaults to data_dir
# ssh_sessions_enabled: true # false refuses mediated ssh sessions (Linux, macOS)
# session_signing_public_key: "" # empty = the session helper pins identity.json's key (Linux socket-activated helper only; unused on macOS)
# bridge: # uncomment for bridge mode
# enabled: true
# access_interface: eth1
# access_subnets:
# - 10.0.0.0/24
# enable_nat: false
# relay_enabled: false
# ingress_enabled: false
# user_access_enabled: false
# site_to_site_enabled: false
# acme_enabled: falseSee Also
- Environment Variables Reference — All
PLEXD_*environment variable overrides - CLI Reference — Command-line interface and global flags
- Architecture and Concepts — Subsystem map and startup lifecycle