Published article

In a mesh of your own devices, trust has to be earned on both sides

The moment one of your devices can ask another to run an LLM or read your data, you have an attack surface inside your own home. How Aurora's mesh earns trust on both sides: WebRTC transport, Bluetooth-style pairing, and intersection RBAC.

In a mesh of your own devices, trust has to be earned on both sides
Published July 10, 2026Updated July 11, 2026
  • aurora
  • distributed
  • mesh
  • security
  • networking

The moment you let one of your devices ask another to run an LLM, transcribe audio, or read from your database, you create an attack surface inside your own home. A compromised Raspberry Pi should not walk straight into your desktop's data. So the interesting question for a private AI mesh is not how devices talk. It is how a device proves it is allowed to.

This is the third piece in my series on Aurora's distributed mesh. The first covered why a single device is a ceiling. The second covered the architecture that lets a request run on another machine as a config change. This one covers the part that makes any of it safe to turn on: the transport, the pairing, and the access control.

The transport: WebRTC, and why not the obvious alternatives

For peer-to-peer communication Aurora uses WebRTC DataChannels. I chose them over WebSockets, raw sockets, a VPN, and heavier P2P stacks like libp2p, ZeroMQ, or gRPC. Four reasons drove the call:

  • End-to-end encryption by default. DataChannels run over DTLS-SRTP. Even the signaling server used for the initial handshake cannot read what flows between peers. Encryption is the only mode, not a setting you remember to enable.
  • NAT traversal built in. ICE with STUN/TURN handles the part most people underestimate, getting two devices on different networks to find each other, without port forwarding, public IPs, or a VPN to babysit.
  • Battle-tested. WebRTC carries video calls for billions of people. The cryptography and the NAT machinery are mature and well understood, which is what you want underneath a security boundary.
  • Low latency. SCTP over DTLS is tuned for real-time, which matters when a person waits on the other end of "Hey Aurora."

The only external dependency in the design is a STUN/TURN signaling server, and it sees the minimum: an IP address per peer trying to connect. It stores nothing and never sees the encrypted traffic in transit. You can host it yourself for almost nothing, or use a public signaling network. Either way it cannot read your data. Lightweight MQTT signaling handles room auto-generation and encrypted (AEAD) peer presence, so devices announce themselves without leaking who is online to anyone watching.

The data crossing these channels is JSON-RPC carrying the same typed contracts services use internally, so a remote call has the same shape as a local one.

The auth gate: guilty until paired

When a peer first connects over a DataChannel, Aurora does not trust it. The RTCClient enforces an auth gate: for an unauthenticated peer, only authentication-related messages pass through, and the gate drops everything else. An anonymous peer can call exactly four methods, PairingStart, PairingConnect, PairingExchange, and Login, the minimum surface needed to become trusted and not one method beyond it.

A timing problem hides here. Pairing needs a human to approve the new device, and humans are slow. A naive connection timeout would kill the handshake while you walk over to approve it. So the gate runs an intelligent timeout. A heartbeat loop, roughly every 5 seconds, extends the deadline while a pairing flow is genuinely in progress, then lets it expire when nothing is happening. The connection stays open exactly as long as it should.

Pairing: the Bluetooth model

Joining a mesh should feel like pairing headphones, not configuring a firewall. So it does:

  1. The new device requests to pair and shows a 6-digit code.
  2. You verify that code from an already-trusted device.
  3. As the admin you approve it, and at that moment you scope its permissions to what it needs.
  4. Aurora issues a bearer token scoped to the permissions you granted.

No passwords cross the network. No pre-shared keys to rotate and lose. Just a short code you confirm in person, so a human establishes trust on purpose, once.

Bilateral trust: A approving B is not B trusting A

This is the design choice I hold most firmly. Most systems treat trust as unilateral, where one side opens a door and the other walks through. In a mesh where any "trusted" device might later be compromised, that is a liability. So Aurora's pairing is bilateral and two-phase.

In phase 1 the initiator reaches the responder. PairingStart carries the remote peer's ID, the responder side approves, and PairingExchange issues a token. In phase 2 the responder pairs back automatically. A _reverse_pairing() step runs over the same DataChannel so the relationship becomes mutual instead of one-directional.

Each side establishes trust on its own terms. Instance A deciding to trust Instance B does not enroll A into B's circle of trust. Both sides run the flow, and both sides set their own permissions. Under the hood this consolidates into the auth tables. Aurora keeps users, devices, and tokens in sync with mesh_peers through outbound foreign-key synchronization, and issues a JWT during the exchange.

RBAC: the bus topic is the permission

Aurora gates every action, and the model stays uniform: a bus topic and a permission are the same string. There are 195+ validated permission strings, and they nest by granularity, from a broad Auth.* down to Auth.use versus Auth.manage, down to a single operation like Auth.PairingApprove. That use-versus-manage split is the contract's method_type from the previous article doing real work, since reading from a service belongs to a different permission class than administering it.

In practice you hand out the right amount of access:

  • A living-room speaker gets TTS.* and STT.*, so it can listen and speak, but not DB.*. It never touches your data.
  • Your phone gets Orchestrator.*, chat.send, and Scheduler.*, so it can ask questions and set reminders.
  • A friend's device you paired only to share compute gets TTS.Request and nothing else. It can use your TTS engine and stays blind to everything else.

The rule that ties it together: effective permissions are the intersection of what the user is allowed and what the token is scoped for. Even a leaked token cannot exceed the user behind it, and a generously-scoped user still cannot act through a narrowly-scoped token.

Administration runs as its own set of gated methods, ApproveMeshPeer, DenyMeshPeer, ListMeshPeers, and RemoveMeshPeer. A permission change (UpdateMeshPeerPermissions) propagates through those same outbound foreign keys into the auth tables, so revoking a device revokes it everywhere instead of leaving a stale grant behind. Enforcement lives in a dedicated ACL layer in front of the gateway, on the same topic-as-permission model.

How you would verify this

A security model is only as good as the tests that try to break it, so the evaluation targets the boundaries directly. Confirm the auth gate rejects unauthorized RPC attempts. Confirm the pairing timeout and heartbeat loop behave under a deliberately slow human approval. Put the DataChannel under Wireshark or tcpdump and confirm the DTLS encryption on the wire. Check RBAC by granting and denying per method_type to prove that use and manage really are separate doors.

The shape of trust in a system you own

Pull the pieces together and a philosophy falls out. Encryption is the default. New devices stay untrusted until a human confirms a code in person. Trust is mutual and scoped, never unilateral and never blanket. Permissions are the intersection of identity and token, so the system fails closed. And the only outside party, the signaling server, cannot read anything that matters.

None of this is specific to voice assistants. Contract-based services, an auth gate on the transport, bilateral pairing, and intersection-based RBAC give you a template for any distributed AI workload you want to keep private while still spreading it across more than one machine.

That closes this series on Aurora's distributed mesh: the why, the architecture, and the trust model. The mesh works today, with pairing and granular RBAC, selective service sharing with capacity limits, transparent routing with fallback, and a system that runs in any topology from a single process to a P2P mesh. Next comes reach: mobile clients, a web UI, better on-device LLMs, and OAuth for MCP.

Aurora is free and open source. If you work on distributed systems security and would have drawn any of these boundaries differently, that is exactly the conversation I am hoping to have.

Originally published on LinkedIn.

Related projects: public writing