remote-access
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).*