Security
Security by design — the layers, what is shipped and what is planned
Security is part of PNeX's DNA: every layer was designed encrypted, isolated and verifiable from day one — including on a Raspberry Pi in a workshop. Each layer below shows its real state; nothing is claimed that is not shipped.
The eight layers
| # | Layer | What it guarantees | State |
|---|---|---|---|
| 1 | Transport | TLS everywhere on one origin, local CA or Let's Encrypt, CA pinned in firmware | Shipped |
| 2 | Device payload | Per-device ChaCha20 frames inside TLS, anti-clone, SHA-256-verified OTA | Shipped |
| 3 | Identity | Dedicated OIDC provider (Rauthy), PKCE, passkeys | Shipped |
| 4 | Authorization | Per-organisation isolation, owner/admin/member/viewer roles, one write source per pin, read-only AI | Shipped |
| 5 | Execution | Sandboxed JavaScript and hermetic Starlark, read-only flow SQL, fenced custom firmware builds | Shipped |
| 6 | Network | Only nginx faces the network (ports 80 and 443); databases and identity stay on the private network | Shipped — production installer; the development stack still publishes database ports |
| 7 | Supply chain | Rust end to end, Wolfi (Chainguard) images, audited dependencies | Shipped |
| 8 | Data at rest | Per-organisation secrets vault, XChaCha20-Poly1305, key ring with rotation | Shipped |
TLS everywhere
nginx terminates TLS for browsers, the native apps (desktop and Android) and devices on a single origin. TLS 1.2 stays enabled because the ESP8266 has no TLS 1.3; small TLS records are negotiated for its 2 KB buffer. See Self-hosting for the local and cloud certificate modes.
The certificate authority of your edge (the local CA, or ISRG Root X1 in cloud mode) is injected into every firmware build. Devices verify the server certificate and its hostname — the WebSocket channel and OTA downloads alike. Renewing the server certificate under the same authority is transparent; changing the authority means a rebuild and reflash (USB or OTA).
Device frame encryption
Inside TLS, every WebSocket frame between a device and the server is encrypted a second time with a key unique to that device:
frame = base64( nonce (12 bytes) ‖ ChaCha20(plaintext) )- Key: 32 random bytes generated when the device is registered, stored with its token and baked into its firmware at build time. It never travels on the wire.
- Nonce: 12 fresh random bytes for every message, in both directions (ChaCha20 as in RFC 7539).
- Scope: telemetry ingestion and the device channel (announce, pin commands, custom metrics and commands, OTA orders). Edge agents use the same framing. OTA images themselves are downloaded over HTTPS and checked by SHA-256 (see Firmware & OTA).
- Size limits: a frame carries at most 4 KB of plaintext on ESP32 and 1 KB on ESP8266; the firmware refuses to send anything larger rather than sending it in clear.
- Wrong key: the server cannot read the frame and answers
decryption_failed; the device never becomes active.
The frames use ChaCha20 without Poly1305: this layer gives confidentiality per device, not authentication. Integrity and server authenticity come from the TLS layer underneath, with the certificate authority pinned in the firmware. A versioned move to an authenticated mode (AEAD) is planned.
Device identity and anti-clone
A device connects with its id and its token. Tokens are unique per device, never reused, and can be deactivated: the server revalidates live sessions about every 10 seconds and closes a session whose token was revoked.
Because identity is baked into the firmware, two boards flashed from the same binary are indistinguishable. The server therefore enforces a first-live-wins lease, held in Valkey and shared by every server instance:
- while the original device is connected, a second connection with the same identity
is rejected (close code
4003); - a clean disconnect frees the lease immediately;
- after a silence longer than the lease (10 seconds by default, two missed heartbeats) the lease expires and a reconnection is accepted.
That last point is the honest limit: once the original is silent, a clone can take its place. Without a per-board hardware identity, PNeX detects and refuses concurrent clones, it cannot prove which physical board is genuine.
Secrets vault
Each organisation has one vault for every secret it uses: notification channel tokens and passwords, credentials of HTTP flow nodes, Wi-Fi passwords and LLM provider keys.
- Encryption: XChaCha20-Poly1305 with a fresh 24-byte nonce on every write. The organisation and the secret id are bound as associated data: a row copied to another organisation or another secret no longer decrypts.
- Key outside the database: the master key ring comes from the environment
(
PNEX_SECRETS_KEYS, generated by the installer). The server refuses to start without it. A database dump alone reveals no secret. - References, not values: a form field that holds a secret stores a typed reference to a vault entry. No secret value appears in deployed flows, in the job queue or between server instances.
- Flow runtime: it receives only references and resolves each one at start-up, and only for secrets used by a flow deployed for that organisation. The master key never reaches the runtime, and function nodes (JavaScript, Starlark) have no access to secrets.
- Write-only: the Secrets page lists every secret with what uses it, never its value. No role can read a value back. Owners and admins create and replace secrets; members can only pick an existing one; viewers see names only. A secret still in use cannot be deleted.
- Rotation: put a new key at the head of the ring and restart, then run the re-encryption from the platform status page. Re-encryption never runs at boot, so instances being upgraded one by one all keep reading the vault. The status page counts secrets still under an old key; remove that key when the count is zero.
Changing a secret value cascades nothing: flows pick it up at their next deploy, and devices keep the Wi-Fi password they were flashed with until you rebuild them.
Out of the vault on purpose: infrastructure secrets (database URL, storage keys, the key ring itself) stay in the environment, and device tokens and frame keys are machine credentials not yet encrypted at rest.
Least-privilege boundaries
- The server never writes pins on its own initiative.
- The AI assistant reads, never writes to devices.
- Flow nodes run read-only SQL, and HTTP fetches are option-gated.
- Custom firmware builds run from a server-generated project, with a source check, a library catalog and an optional network-less sandbox — and the feature is off by default. See Firmware & OTA.
Threat model notes
Device tokens are URL-safe and never re-used across devices. OTA verifies SHA-256 before flashing. The identity provider runs as a separate component (Rauthy), so identity stays manageable and auditable independently of the platform server.

