‹remote-access

remote-access

all docs

Remote access — architecture & security

How a browser anywhere reaches your NAT'd home node — by passkey, encrypted endpoint-to-endpoint, with oneaurica as a blind relay that only ever forwards ciphertext. The key that tunnel terminates against is pinned by the name directory, not authenticated (§5, "the honest floor, part 2"), so this is confidentiality against the relay, not end-to-end encryption to a verified peer.

Built on [iroh](https://iroh.computer) 1.0.0-rc.1. The transport is the new piece; the passkey/capability auth and the mesh organ already existed.


1. The players (topology)

graph LR

subgraph DEV["Your device — anywhere"]

PAGE["Browser tab<br/>origin = access host"]

SW["Service Worker<br/>fetch intercept + zcap jar"]

WASM["wasm iroh client<br/>relay-only endpoint"]

end

subgraph CF["Cloudflare edge"]

CFE["TLS termination<br/>WebSocket proxy"]

end

subgraph ONE["oneaurica · DO App Platform"]

RELAY["iroh-relay (dolphin-app)<br/>BLIND — ciphertext only"]

REG["registry.resolve<br/>name → key"]

SHELL["app shell + wasm assets"]

end

subgraph HOME["Home node — behind NAT"]

ORG["mesh organ<br/>public door (relay-only)"]

BR["bridge<br/>HTTP over bi-stream"]

WEB["web door :8080<br/>passkey verify · issues zcap"]

end

PAGE -->|loads shell + wasm| SHELL

PAGE --- SW

PAGE --- WASM

PAGE -->|resolve name| REG

WASM ==>|"wss · E2E QUIC inside"| CFE

CFE ==> RELAY

ORG ==>|"wss · outbound dial"| CFE

RELAY -.->|routes ciphertext| ORG

ORG --> BR --> WEB

• Browser — loads a thin app shell from the access host. A Service Worker

intercepts the page's requests; an in-page wasm iroh client holds one relay-only connection to the home node (the socket lives in the page, not the SW, because SWs get evicted when idle).

• oneaurica — runs the iroh-relay (on App Platform behind Cloudflare), serves

the app shell + wasm assets, and a registry that maps a name (amit-home) to a node key. It never holds a private key, a passkey, or any plaintext.

• Home node — the mesh organ dials *out* to the relay and holds a relay-only

public door open; arriving streams bridge to the loopback web door, which runs the existing passkey verification and issues sessions.


2. Control flow — establishing the connection

sequenceDiagram

autonumber

participant H as Home node (organ)

participant RL as iroh-relay (blind)

participant B as Browser (wasm)

participant RG as registry

Note over H,RL: at server start

H->>RL: open wss, register public door under its key

Note over B,RG: when you open the shell

B->>RG: resolve "amit-home"

RG-->>B: endpoint_id + relay_url

B->>RL: dial endpoint_id over wss

RL->>H: route the connection request

B->>H: QUIC + TLS1.3 raw-public-key handshake

H->>B: mutual auth — the keypair *is* the EndpointId

Note over B,H: E2E session established · relay forwarded only ciphertext

The home node is unreachable inbound (NAT), but as a relay client it holds an outbound wss connection open, so the relay can route a browser to it. Neither side ever learns the other's IP — the browser path is relay-only by construction (clear_ip_transports), so no direct address is ever offered.


3. Data flow — tunnelling one request

sequenceDiagram

autonumber

participant P as page fetch()

participant SW as Service Worker

participant W as wasm client

participant RL as relay (blind)

participant O as organ bridge

participant D as web door :8080

P->>SW: fetch (same-origin)

SW->>SW: serialize HTTP/1.1 · attach Cookie from jar

SW->>W: request bytes (MessageChannel)

W->>RL: write over the encrypted iroh bi-stream

RL->>O: forward ciphertext

O->>D: open loopback TCP, replay the request

D-->>O: HTTP response

O-->>W: encrypted bytes back

W-->>SW: response bytes

SW->>SW: parse · capture Set-Cookie into the jar

SW-->>P: Response (cookie stripped)

Every same-origin request the page makes — navigations, fetch, form posts — rides this one pipe. The home node's existing pages work unchanged: it just sees ordinary HTTP/1.1 arriving on its loopback web door.


4. Events flow — the passkey login ceremony

sequenceDiagram

autonumber

participant SE as Secure Enclave

participant B as Browser page

participant T as Tunnel (SW + wasm + relay)

participant H as Home node

B->>T: GET /login/challenge

T->>H: (tunnelled)

H-->>B: stateless HMAC challenge

B->>SE: credentials.get(challenge, rpId = access host)

SE->>SE: sign — private key never leaves the enclave

SE-->>B: assertion + signature

B->>T: POST /login/verify (assertion)

T->>H: (tunnelled)

H->>H: p256_verify(pubkey, msg, sig) + origin + challenge checks

H-->>B: Set-Cookie zcap — HMAC-signed by the node's own secret

Note over T: the SW jars the zcap — never enters the page or browser cookie store

B->>T: later requests carry Cookie: zcap

T->>H: _apply_cap verifies and promotes the proven principal

The passkey is bound to the access host origin (where the shell loaded), but the home node is the verifier and session issuer — oneaurica is just the venue.


5. Why it's safe & secure

graph TD

subgraph TRUST["Encrypted endpoint-to-endpoint — QUIC + TLS 1.3, raw public keys (directory-pinned)"]

B["Browser<br/>passkey private key in Secure Enclave"]

H["Home node<br/>verifies passkeys · owns sessions"]

end

R["oneaurica relay + Cloudflare"]

B <==>|ciphertext frames| R

R <==>|ciphertext frames| H

R -.->|CAN see| MD["metadata only:<br/>EndpointIds, IPs, timing, byte counts"]

R -.->|CANNOT see| PT["plaintext: URLs, bodies,<br/>cookies, passkeys, data"]

style TRUST fill:#eef7ee,stroke:#4a4

style R fill:#fff4e6,stroke:#e90

style PT fill:#fdecea,stroke:#c33

style MD fill:#fff8e1,stroke:#e90

The guarantees, and the one honest limit:

Property

How it's enforced

Encrypted endpoint-to-endpoint, to a PINNED key

QUIC + TLS 1.3 with raw public keys (RFC 7250) terminates *endpoint-to-endpoint*, inside the WebSocket. The relay forwards opaque encrypted frames it holds no key for. Read the next row before quoting this one: the tunnel terminates against whichever EndpointId the name directory returned, so "the relay cannot read it" is a statement about the relay's *keys*, not a guarantee that the far end is who you meant.

Blind relay

oneaurica + Cloudflare see only metadata (which EndpointIds talk, IPs, timing, byte counts) — never URLs, bodies, cookies, or data, under the key the browser was handed.

Mutual IP privacy

The browser path is relay-only (clear_ip_transports), so no direct address is exchanged. The home node never learns the browser's IP; the browser never learns the home node's IP.

Keys never leave the device

The passkey private key stays in the Secure Enclave; only a signature crosses the wire. oneaurica holds no private key or passkey.

Node-authoritative identity

The home node verifies the WebAuthn assertion (p256_verify) and issues its own session. oneaurica is a relay + a name directory, never the identity authority.

Sessions are proven, not asserted

The zcap is principal:expiry.HMAC(node_secret, …). Only the node can mint a valid one, so presenting it *proves* the node issued it — there is no trust-the-caller step.

Identity is the door's to assign

The bridge strips any caller-supplied ?as= and stamps only the door's authoritative principal (anon on the public door), so a stranger cannot self-promote.

Session secret never touches the page

The zcap lives in the Service Worker's jar, not the browser cookie store or any page script — tighter than a normal HttpOnly cookie.

The honest floor, part 1 (no onion routing): your own relay sees *who talks to whom* (EndpointIds, IPs, timing). Content is never exposed and IPs never cross the tunnel, but this who-talks-to-whom metadata is visible to the relay operator — that's you. Private-information-retrieval / onion routing are explicitly deferred.

The honest floor, part 2 (key discovery is PINNED, not AUTHENTICATED) — the limit that decides what the row above is worth. The browser dials whatever EndpointId registry.resolve hands back over HTTPS, and an EndpointId *is* the raw public key the tunnel terminates against. Nothing in the browser pins that key out of band, so a compromised or malicious name directory can return an attacker's key and the tunnel will faithfully encrypt everything to the attacker. The same limit holds for the messenger's store-and-forward queue, which seals each deposit to a seal_pub= learned from a registry line (network/_remote_route_line.action, network/_q_epk.action). So the property this build has is confidentiality against the RELAY — which really does hold, and is really worth having — and not authenticated key discovery. The frozen answers are ZMP/1 §12.7's device-signed zmp1.reach bundle and §10.8.6's seal-key attestation, and neither is built (roadmap/messenger-final-form/STATE.md §3). Until one of them is, do not write "end-to-end encrypted", "only the recipient can read it" or "no trust in the relay" about any of these paths: each is a claim about a key nobody has verified. Pinned by roadmap/comms-forever/verify-p2p-purity.sh rows 24 and 25.


*Deploy steps: [REMOTE-ACCESS.md](../mesh-deploy/REMOTE-ACCESS.md). Components: [mesh/](../mesh) (organ), [mesh-deploy/](../mesh-deploy) (relay).*