remote.DocumentationHome

End-to-end sealed sessions (sealed sessions)

Android Console opens end-to-end encryption from Settings, with device selection inside the page. Android and Web Console account settings support setting or changing a password and binding or replacing an email; the existing email remains active until the new address is verified. Console backs up the root private key to Exchange with an empty passphrase by default, preserving existing passphrase backups. Deleting a backup disables automatic saving on that client. An empty passphrase allows Exchange or a database holder to decrypt the root. Manual recovery verifies the workspace root before persisting it and synchronizing grants.

Goal: Even if the Exchange host is compromised or its operator is curious, it cannot read commands, output, PTY bytes, file contents, screenshots, or ubus data, and cannot decide or forge who may view or manage a device. Exchange serves only as a relay for ciphertext and routing metadata.

Definition: crates/proto/src/seal.rs (wire types, local authorization rules), crates/proto/src/seal/crypto.rs (session cryptography, ring only), crates/proto/src/seal/backup.rs (password backup: Argon2id + ChaCha20-Poly1305, not linked into the agent), crates/agent/src/seal.rs (device side), crates/cli/src/seal.rs (CLI side), shared/seal-session.js (session implementation shared by Web Console and Android Console, @noble/curves · @noble/hashes · @noble/ciphers), shared/seal-backup.js (on the same set of libraries Argon2id password backup). Cross-language test vectors: shared/seal-vectors.json (JS generated, Rust javascript_vectors_interoperate verify signature/HKDF/ciphertext consistent byte by byte), shared/seal-rust-vectors.json (Rust device response, JS shared/seal-session.test.js completes handshake and decrypts).

seal approve still publishes grants automatically. A subsequent seal grant --publish (or default publication while logged in) skips an unexpired published grant with the same issuer key, recipient key, device and level, reports “already published” and emits no new token. Use --renew-before DAYS to renew grants nearing expiry; --no-publish still signs offline tokens. This check prevents duplicates from sequential retries; it does not provide a transaction across concurrent clients or remove historical duplicates. seal show creates a local identity and registers its key when logged in; use a separate profile for experimental identities.

Threat Model

Item Sealed-session protection and boundaries
Command line, stdout/stderr, exit code Not visible; auditing records only sealed:true, the operator public key, grant level and expiry, and final outcome
PTY input and screen output Invisible, ciphertext frames are not audited
File content, screenshots, interface tree, ubus parameters and results Invisible
Who can do what (Super Administrator/Administrator/Operation and Maintenance/Read-only, View-only/Manageable) Device Judgment: Only authorization issued by the local fixed workspace root public key is recognized; Exchange cannot issue, expand or forge
Impersonating device Impossible: native device identity key signature + CLI pinned device public key; browser extension verification workspace root chain proof and response signature
Inject keystrokes/commands into session Not possible: AEAD authentication + strictly incrementing counter; agent discards any clear text sent to sealed session number
Replay, reordering, packet loss, truncation Detected and closed session (fail closed)
Metadata that remains visible Device ID, operator account, session number, timestamps, packet sizes and counts. Whether data is stdout or stderr is not visible: the type is encrypted
Actions still possible Denial of service: withholding forwarding, disconnecting, or closing a session (sealed_close). This affects availability, not authorization

Outside the protection scope: a compromised device or operator computer, or Exchange already impersonating a device on the first connection (TOFU). Compare fingerprints out of band to avoid the last case. All device operations require sealed sessions. The preset-command directory and browser root-chain authorization boundaries are described below.

Path coverage and design

Path Sealed-session behavior Device-side authorization and boundaries
HTTP POST /api/devices/{id}/exec Explicit Reject, HTTP 409, prompting to use open_sealed WebSocket session Device still rejects clear text exec_req even though Exchange ignores policy
HTTP preset command /commands/{cmd}/run Explicitly deny, also prompt to seal WS The command in the server database is not device-side authorization
Web Console/CLI preset commands Get template from directory, send exec_req by operator within sealed WS, output is also sealed Device verifies hello, root signature grant, revocation, validity period and manage level; view cannot execute template
LAN /ws/operator Sealed sessions only; hello includes the grant and certificate chain, without a Bearer token The same root-chain verification and view/manage rules as Exchange. The outer layer checks only private network addresses and Origin. /api/devices returns only the device ID and seal public key/mode
Browser Agent / native sealed routes Use open_sealed / sealed_input / sealed_event, reject old browser channel Extended validation operator root signature grant / certificate chain; CLI / Console validate extension SealAccept.peer root chain and requires its public key to be equal to the response signature public key
CLI exec-attach / exec-hold-tail / exec-hold-cat Native sealed requests, output frames, and completion events Requires manage; sealed jobs cannot be read or attached in plaintext
CLI sensors --live Use an independent sealed session (single operation/session) for each sampling, Ctrl-C to end the refresh view; do not stuff multiple samples into a single operation tunnel
CLI net net_req and net_resp seals, including URL, headers, body, results Requires manage
CLI device update update_req, all progress and final state sealed Requires manage; CLI top-level update updates local client, not remote session
exec / PTY / file / ubus / Android / sensors single / hold list, kill There is a native seal implementation and remains unchanged Use the same required_level and device-side dispatcher
Device list, metrics, presence, Exchange sessions list Routing/monitoring metadata, not sealed device operations Exchange visible; not authorized as a device operation

HTTP and preset commands

Native sealed operations now support HTTP-only callers. Use remote-cli --transport http or REMOTE_TRANSPORT=http. POST /api/sealed/connections creates a mailbox; POST /input submits signed handshakes or ciphertext, GET /events polls results, and DELETE closes the connection. Clients retain the existing end-to-end cryptography and device authorization. The original device-action endpoints still return 409. The device connection remains unchanged.

https://remote.ivjn.us/sealed-http.md

Creation, editing, and listing of preset commands remain an Exchange database management surface, and template text, names, and descriptions in directories are not sealed data, nor are commands signed by the root allow-list. The console and CLI execute the fetched template as a normal sealed exec (non-hold) while holding the key; the device only trusts the native policy and operator grant, not the directory ID, server role, or "this is a preset command" argument. Members holding only view grant cannot run preset commands; root grant manage is required. Grant cannot be expanded if the directory is tampered with, but manage allows arbitrary exec, so please check the template and do not write the password/token into the server directory. The output is only decrypted and displayed locally on the operator; the run path audit only records sealed metadata and does not record commands and output.

LAN and client selection

CLI device sessions use only end-to-end sealing, independent of seal_key, capability, or policy hints reported by Exchange. Missing or removed capability information never causes a plaintext downgrade. Failure to read the local pin database is an error; corrupted trust records are not ignored.

Sealed held / non-held exec and file tasks are marked sealed before being published to the global task directory, and cannot rely on the race condition of "closing the session before the first outbound event is processed" to read the clear text preview. Device logs do not record HTTP probe targets or methods, ubus objects or methods.

LAN reuses native envelopes, signature transcripts, counters, AEAD, device fixation and revocation rules without creating new cryptographic algorithms. LAN outbound goes through Tunnels::seal_outbound and is translated to OperatorDown; the plaintext injection of the sealed session number is discarded by the device dispatcher. The connection is closed while still detaching the held exec, canceling other operations, and late output is discarded by the fenced id. There is no LAN token: the client puts the offline cached grant and certificate chain into hello, and the device determines view/manage according to the same authorize as Exchange. The outer layer only checks the private network address and Origin. Clear text sessions are always denied on the LAN, regardless of historical policy values. The hosted web console is still connected to Exchange.

Optional device information info.seal_mode (allow / require), seal_key, and the sealed capability remain compatible. They are neither authorization nor trusted device public keys; the device ultimately enforces policy. Commands, terminals, files, ubus, sensors, jobs, network probes, desktop operations, device updates, preset commands, and browser operations in CLI, Web Console, and Android Console all use native sealed sessions. No plaintext path is available. The policy value allow also rejects plaintext operations. Handshake, device-pinning, grant, or decryption failures stop the operation without a plaintext retry. Online policy changes refresh LAN information immediately; cloud information refreshes on reconnection and registration.

Browser Agent’s workspace root chain authentication

The extension advertises sealed, its Ed25519 seal_key and seal_mode: require. After pairing or device configuration it calls POST /api/seal/device with device credentials and a signed proof of possession. Exchange derives the workspace from the device and binds its public key. A client holding the trusted workspace root private key signs a view grant for that exact browser device. Registration and device bearers provide relay access and credential retrieval; browser sessions require certificate authentication.

The extension verifies the incoming signature, root chain, device scope, level, expiry and local revocations, then signs SealAccept with its own signed SealHello in peer. CLI finish_workspace_operator and shared finishBrowser verify that proof, require peer.operator_key == accept.device_key, and verify the accept signature. Missing or invalid proofs close the session. Valid sessions start automatically. Raw CDP additionally requires local per-request consent or unattended approval scoped to operator key, server and device; view grants cannot use CDP.

The extension scopes authorization records by server origin, workspace and device. Its local identity seed is accessible only to trusted extension contexts. It pins the first workspace root locally and rejects changes. CLI and Console use their normal seal identities and workspace root chain without a separate browser pin editor. Root-key signing, certificate authority and device enrollment credentials are distinct checks; native-device key TOFU remains separate.

Security boundary: First-root TOFU cannot distinguish a fake root supplied by a malicious relay; verify it out of band. PoP proves key possession, not member approval. Root-client auto-approval depends on the roster; sensitive deployments should review new keys. Protect the extension profile, seed, and web-console publishing chain. Compromised endpoints or full CDP access granted to malicious operators are outside the protection of sealing. Deleting directory records does not revoke cached signed credentials; device-local revocation and expiry rules still matter. Exchange can still observe routing metadata, withhold credentials, and deny service.

When identity is unavailable: If the device identity cannot be loaded or written, sealed sessions are unavailable and all plaintext operations remain rejected (fail closed). Missing or corrupted policy files and historical policy values never enable plaintext. Repair key storage locally on the device.

HTTP mailboxes require affinity to the creating Exchange instance and expire after 90 seconds without activity. Serialize inputs, retry only the same sequence and ciphertext, and acknowledge processed events with after. Web Console, mobile and LAN retain their existing transport. Preset execution still requires manage.

Regression verification

Rust tests cover real device-grant handshakes, sealed exec/attach/tail/cat/sensors/net/update results, plaintext rejection under every policy value, LAN WebSocket sealed handshakes/frames/policy updates, HTTP 409 guidance, browser require-mode route closure, and mandatory-sealing invariants for clients and device information. JavaScript tests cover the existing bidirectional Rust/JS vectors, operation envelopes, sealed preset-command output with no fallback after rejection, browser configuration/registration/device rejection/local policy changes, and client pinning errors. Process-wide seal singletons are initialized in isolated child processes during testing so concurrent tests cannot modify each other's device policies.

CARGO_PROFILE_DEV_DEBUG=0 CARGO_PROFILE_TEST_DEBUG=0 cargo test --workspace
cargo fmt --all --check
CARGO_PROFILE_DEV_DEBUG=0 CARGO_PROFILE_TEST_DEBUG=0 cargo clippy --workspace --all-targets -- -D warnings
npm test --prefix shared
npm test --prefix website/console
npm test --prefix browser-agent
node --test scripts/test-pages-worker.mjs

JavaScript uses Node 24+. Added Sealed LAN regression for real HTTP Upgrade / WebSocket via in-memory duplex streaming, with the ability to verify handshakes, frames and mandatory sealing under every policy value in a sandbox that disables network sockets. The existing loopback WS/TLS/HTTP test still needs to be in an environment that allows port binding, otherwise it will fail with Operation not permitted and cannot be used as evidence that the full set of tests passed. Network probe and update path test verify seal success/error results, updates are not downloaded or installed.

Special risks of Web Console: The web page code itself is delivered by the Pages site. The compromised site/CDN can issue malicious scripts to read the plain text of this page or sign it with the browser's key (but it cannot take away the non-exportable private key). The prerequisite for confidentiality from the Exchange operator is that you trust the publishing chain of the static site; if you are unsure about this, please use the CLI or Android Console.

Identity and Key

Key Type Storage Who can see the public key
Device identity Ed25519 agent: agent.seal.key (0600). When the state file is on tmpfs (/var/run, /run, /tmp), storage moves to /etc/remote-agent/; override with REMOTE_SEAL_DIR. OpenWrt includes it in conffiles Reported as info.seal_key during registration (display only; the CLI does not trust this copy)
Operator identity Ed25519 CLI: seal-identity.pk8 (0600) beside config.toml; Web Console: a non-exportable WebCrypto Ed25519 CryptoKeyPair in IndexedDB remote-seal (falls back to a random seed in IndexedDB if the browser lacks WebCrypto Ed25519 support); Android Console: a seed in expo-secure-store (encrypted by Android Keystore) Sent during the handshake
Workspace root Ed25519 is the operator key on the super administrator client. The seed must be exportable on the Web Console (the super administrator will replace the non-exportable WebCrypto key with a local seed when generating the root); it can be exported as rsk1. text, backed up to Exchange by Console with an empty passphrase by default rsr1. root record (Exchange published, device fixed)
Session key Ephemeral X25519 keys → HKDF-SHA256 → one ChaCha20-Poly1305 key per direction Memory only; discarded when the session ends (forward secrecy) Nobody

Operator private keys are stored locally. Console uploads a root backup with an empty passphrase by default; Exchange or database holders can decrypt it. Set a backup passphrase manually, or delete the backup to disable automatic saving on this client.

Authorization (Exchange is not authoritative)

The device only trusts one workspace root public key. Local policy agent.seal.json:

{"mode":"require","root_key":"<base64 Ed25519>","revoked_keys":[]}

The agent determines the level for each sealed_open by itself:

  1. No workspace root is pinned on the device → reject (no workspace root pinned on this device).
  2. Operator public key at revoked_keys → Rejected.
  3. The operator’s public key is root_key → owner (manageable + modify local policy).
  4. A grant is supplied: its signature is valid, operator_key matches the handshake, device_id identifies this device or *, and the grant is within its validity period (±5 minutes of clock skew). Its signer must be the pinned root, or hello must carry a rsc1. certificate chain (hello.chain, not covered by the handshake signature), ordered from the root-issued certificate to the grant signer. Each certificate must permit further issuance, and path length, level, device scope, and expiry must stay within the preceding certificate. The grant level is view or manage, never owner.
  5. Others → Reject.

Certificate chain constraints

Devices accept a rsc1. chain that verifies back to the pinned workspace root, not an intermediate certificate in isolation (hello.chain, ordered from the root-issued certificate to the grant signer). There is no organizational depth limit: organizations may have more than 10 levels. A generous DoS guardrail limits the chain to MAX_CERT_CHAIN = 64 certificates and its encoded size (the sum of the canonical rsc1. token lengths) to MAX_CERT_CHAIN_BYTES ≤ 64 KiB (check_chain_limits / checkChainLimits in crates/proto/src/seal.rs / shared/seal-cert.js; identical rules on devices, Exchange, CLI, Web, and Android). Exceeding either limit causes immediate rejection.

Each certificate also has a max_path_len (0–255), an optional issuer constraint on the number of certificates that may follow it. Without --max-path, seal cert issue defaults to all remaining issuer depth (255 is effectively unlimited). Verification requires the number of subsequent certificates to be no greater than max_path_len. Intermediate certificates issued by the root to administrators have max_path_len 0: administrators may sign grants but cannot issue another certificate. A false can_issue also prevents further certificate issuance.

The child certificate must fall inside the parent certificate (cert_within_parent / verify_chain on the device):

  • level does not exceed parent level. The root is owner; the level granted can only be view or manage, and cannot be higher than the parent certificate. owner cannot appear in the certificate or grant.
  • Device scope is a subset of the parent. A child can cover all devices only if the parent contains *; otherwise each device of the child must be in the parent's list. The child is written * only if the parent is also *.
  • Expiration time cannot be later than the parent certificate. Roots have no expiration cap. Grant also cannot be later than the certificate that issued it, nor can the device exceed the scope of that certificate.

Revocation of an intermediate certificate cascades to everything below it. The device checks every subject_key and serial on the chain against revoked_keys in the policy. After the middle one is revoked, any longer chain still carrying it fails, so the subordinate certificates it issued, as well as the authority issued by those certificates, become invalid. Deleting the certificate from Exchange only stops further distribution, and the copy already held on the device is still valid before expiration; to make the entire chain below invalid immediately, write the subject public key (or serial number) of the certificate into the device policy revoked_keys.

Mapping of levels to actions (proto::seal::required_level): view (view only) allows sensors, hold job lists, Android read-only actions (status/tree/screenshot/find_text); everything else (exec, PTY, files, ubus, network detection, self-update, Android input) requires manage.

Authorization is a root-signed JSON. Any field tampering by Exchange (such as view→manage, changing devices, extending the validity period, changing authorized persons) will invalidate the signature; the handshake signature also binds the authorization to this session. The account role on the Exchange side only determines "whether messages can be routed to this device" (tenant visibility), and does not expand or reduce the permissions of sealed sessions. There are no device-level claims, owners, or co-owners: the only owner is the root.

How devices pin the root:

  • TOFU: When a sealing-capable device connects to Exchange, Exchange sends seal_root {token, rotation?} (a self-signed rsr1. root record). The device verifies the self-signature; if no root is pinned locally, it pins this root and writes it to agent.seal.json.
  • Rotation: After the root is fixed, the root announced by Exchange changes. The device only changes to the new root when it simultaneously receives a rsx1. rotation record signed by the fixed root (old_root = fixed root, new_root = new root, same workspace); otherwise, it is ignored and logged. The root can also be changed in a sealed session with seal_policy_set {root_key} (only the current root has the owner level).
  • Local settings: remote-agent seal root <key> is written directly to the root; remote-agent seal forget-root is cleared and TOFU is repeated the next time the Exchange is connected (restarting the agent will take effect).

Risk: If Exchange is already compromised on the device's first connection, TOFU lets it supply a fake root. If this is a concern, pin the root public key with remote-agent seal root <key> before bringing the device online (see remote-cli seal root for its fingerprint). Once pinned, Exchange cannot replace that root.

Revocation: Root remote-cli seal policy <设备> --revoke <key>, or on device remote-agent seal revoke <key>. The root itself cannot be revoked. The authorization comes with a validity period (90 days by default). Deleting a published grant from Exchange simply no longer distributes it; the copy that has been imported locally is still valid until it expires and must be revoked by device policy.

Exchange as an untrusted grant relay

Colleagues no longer have to copy the rsg1. token or public key into the chat window: Exchange provides a workspace-level Operator Public Key Directory and grant relay. This does not change the threat model - Exchange still cannot see the clear text and cannot issue, extend or forge authorizations. It only saves:

  1. The operator uses the local private key to prove the Ed25519 public key (POST /api/seal/keys) he holds
  2. Original text of the signed rsg1. token (POST /api/seal/grants)
  3. The original text of the signed rsc1. intermediate certificate (POST /api/seal/certs). Exchange verifies the issuer's Ed25519 signature, but cannot forge certificates. GET /api/seal/certs/chain?subject_key= Retrieve tokens along the issuing relationship (limited by the same 64 tokens / 64 KiB guardrails), root-issued first

Device authorization still only recognizes the fixed root, or a rsc1. certificate chain that can verify the root. Exchange parses tokens only to build indexes according to owner_key / operator_key / expiration time, and never regards uploading as authorization. The worst case is that the operator withholds the token (the other party cannot get the grant, which appears as no grant), and cannot change view to manage, nor can it pretend to be the root.

Any workspace member can upload a grant: the token comes with its own signature, and uploading is not an authorization act; only root-signed grants are valid for the device. The CLI defaults to --publish when logged in; the Web/Android console uses the same interface when "signing and publishing".

Possession proof binding for key registration:

remote-seal-key-register:v1:<workspace_id>:<operator_id>:<public_key>:<issued_at_ms>

The proof is an Ed25519 signature made with the private key corresponding to this public key; issued_at_ms must be within ±10 minutes of server time. The CLI attempts registration on login and seal show; consoles register after login.

If there is no matching grant locally before the sealing operation, the operator will try its best to seal sync; when the handshake is rejected due to no grant, it will synchronize again and try again.

When the issuer's client goes online, it will automatically renew the issued authorization that it issued and is about to expire. The signature is still completed locally; Exchange only saves the new rsg1. token and deletes the old record, and cannot change the level or change the authorized person.

  • When to run: CLI after successful login, and when any logged-in command starts (seal renew takes the manual route and does not apply a layer of automatic renewal). Web Console will also enter the workspace when logging in and entering the workspace (if the session already exists after the page is loaded). Android Console after logging in, and when the app comes back to the foreground. If you are not logged in, or if this public key does not have a published grant, it will not be renewed.
  • Renewal Rules: GET /api/seal/grants?owner_key=<本机公钥>. Grants that expire within 30 days will be reissued with the original device_id, operator_key, and level for 90 days. After successful release, the old grant will be DELETE. If GET /api/seal/keys is successful, operator_key that is no longer in the public key directory will be skipped; if the directory request fails, it will not be filtered by directory.
  • Throttling: Web/Android: The same issuing public key will be tried at most once every 12 hours. The CLI is recorded in the configuration directory as seal-renew-state.json, the Web Console uses localStorage, and the Android Console uses SecureStore. Failure does not affect the current operation: the CLI only prints a warning line on stderr, and the console uses console.warn. When the renewal is successful, the CLI prints a brief explanation on stderr. The Web Console holding the root will issue one round first when entering the workspace, and Android will also issue one round first when "end-to-end encryption" is turned on; these two times are not subject to the 12-hour throttling. Only refreshes after the same stay will be throttled.
  • CLI renewal: Success is tracked per server and issuing key. Renewal network requests are skipped for 12 hours after success; failure or interruption backs off for only 5 minutes. The whole renewal has a 30-second budget. Errors include the stage and elapsed time; failure of the main command cancels renewal immediately. Pending device revocations bypass the success throttle and retain a separate 4-second budget. Run remote-cli seal renew for explicit renewal. A successful login forces one refresh.
  • Root client: The super administrator client that holds the workspace root uses the role-based issuance in the next section to replace this general renewal to avoid signing back the newly deleted authorization.
  • Manual: Still available remote-cli seal renew (default --before 30 --days 90), same rules as automatic renewal.
# 按工作区用户名授权对方每一把已登记的钥匙,并发布到 Exchange(默认 90 天)
remote-cli seal grant --device lab-router --to alice --level view
# 只打印 token,不上传
remote-cli seal grant --device lab-router --operator-key <KEY> --no-publish
remote-cli seal keys          # 工作区公钥目录
remote-cli seal sync          # 拉取签给自己钥匙的 grant 并导入本地
remote-cli seal renew         # 手动续签,默认 --before 30 --days 90

The workspace super administrator is the root of trust

The workspace root is the Ed25519 key on the superadmin client. The device only trusts this root public key and can verify the root's rsg1. authorization along the rsc1. certificate chain; the root client automatically issues and renews member authorizations based on account roles. Exchange is still not trustworthy: it only saves self-signed rsr1. root records, rsx1. rotation records, and rsi1. invitations, and checks the format against the Ed25519 signature, without deciding who is approved. A malicious Exchange can withhold records or fill the roster with fake members; it cannot forge root signatures. Automatic approval is enabled by default in CLI, Web, and Android, and new public keys in the roster will be signed directly by these root clients. Only when automatic approval is explicitly turned off, new public keys that have never been approved and have not been published by this root will remain in the pending list.

Trust model and trade-offs

  • The root public key is fixed on first use (TOFU). When the client sees the verified root record for the first time, it fixes the public key locally. The CLI writes seal-root.json (0600) next to config.toml, and the key is 服务器|工作区 id. Web Console uses localStorage (remote.seal.root.<工作区 id>). Android Console uses SecureStore (remote.seal.rootstate, placed in the same JSON by server and workspace). Then check the self-signature and fingerprint every time. If the fixed root public key changes, a loud warning will be issued.The client will not automatically change the password (the device will automatically change the password according to the rsx1. rotation record, see above). After checking the fingerprint, execute remote-cli seal root --trust <key> explicitly, or click "Trust New Root Public Key" in the console.
  • Invitation only opens for registration. Root public key issued rsi1. (workspace id, root public key, 16-byte nonce, expiration time). Administrators, operators, and clients with read-only accounts pull invitations when logging in, following commands, loading pages, or when the application returns to the foreground, verify the signature with the fixed root public key, and then use the original POST /api/seal/keys to register their operator public key. An invitation is not an authorization.
  • Issue by role, approve new keys locally. The super administrator client where the root public key is located reads GET /api/seal/roster (super administrator only; including disabled members, roles, operator_device_grants, registered public keys) and its own published grant, and calculates the expected authorization. The same registered public key shares this set of ranges:
    • Super administrator, and unrestricted administrator: manage *
    • Restricted Administrator: Authorized by device, can_manage is manage, otherwise view
    • Unrestricted operators: view *, plus manage for each can_manage device
    • Restricted Operator: Per device authorization manage or view
    • Unrestricted read-only: view *
    • Restricted read-only: view per authorized device. Read-only accounts cannot operate the device, so even can_manage will not be sent manage
    • Disabled members: do not post
    • Default 90 days, renewal with less than 30 days left
  • Administrator intermediate certificate. Root client issues rsc1. to enabled administrators (manage, can_issue, max_path_len is 0, 90 days). The device scope of an unrestricted administrator is *; a restricted administrator is the device to which it is assigned can_manage. Renew with only 30 days left. No certificate will be issued for new public keys that have not yet been approved. When the administrator client holds a still valid intermediate certificate, it uses its own key and this chain to issue grants to the members it manages, and the scope is the intersection of the certificate and the device delegation; if the root already has a still fresh grant covering the same operator, device and level (the root's * covers the specific device, manage covers view), it will no longer sign. Revoking a certificate from Exchange only stops further distribution; copies already on the device remain valid until they expire. Hard revocation relies on revoked_keys in the device policy.
  • Auto-approval is on by default. In Web Console (localStorage) and Android Console (SecureStore), when the record does not have this field, it is considered to be on (absent = on). Only the clear false remains closed. Those that have been closed will not be reopened by the default logic. After opening, the root client signs directly according to the roster, weaker: a compromised Exchange can insert fake members and get signatures immediately. The switch copy is "Automatically approve new public keys". CLI defaults to on, just like Web/Android Console: a missing seal-approved.json file or auto_approve field means on; explicit "auto_approve": false stays off. Run remote-cli seal approve --no-auto-approve to disable it or remote-cli seal approve --auto-approve to enable it again; the choice is saved. After closing, keys that have never been approved or published by this root public key will enter the local pending list (CLI seal-approved.json, Web localStorage, Android SecureStore). remote-cli seal pending displays username and fingerprint; seal approve --all or seal approve <key>… is issued immediately upon approval. For keys that have been approved, as well as keys that have been issued by this root public key, renewal and role changes are automatically completed. Issuing the client's own public key does not require approval again.
  • Sign as soon as you open it. The root client is signed immediately when the Web Console enters the workspace and Android turns on "End-to-End Encryption". This round is not subject to 12-hour throttling. Automatic refreshes after the same stay are still throttled. Changing the switch or clicking "Approve" will force you to sign again.
  • Account dialog, badges and failure lines. The Web Console switch and pending list are in the "Account" dialog box on the device page ("Account" on the toolbar). When there are keys waiting for approval, the badge "N public keys pending approval" is displayed on the same toolbar. Click the badge to open this dialog box. When signing fails, there is a status line in the toolbar: "Workspace root signing failed:...". Sealing tokens and long key material are erased from this line. Android switches, same-sentence badges, and same-line failure instructions are on the End-to-End Encryption page.
  • Downgrade does not revoke old authorizations on the device. When a member is removed, disabled, or the public key disappears from the directory: remove the grant from Exchange, stop renewal, and try to write revoked_keys on a device that is online and supports sealed sessions. When the role is downgraded, only a new grant will be exchanged, and the key will not be put into revoked_keys. Old grants that have been imported on the device are still valid before expiration. The root public key itself is never put on the revocation list. Revocations that are not completed within the 4-second budget are recorded locally and will be continued with the next command, without being subject to the 12-hour issuance throttling limit. When the key becomes the desired authorization again, the revocation will only be revoked on the device that has been successfully revoked.
  • Root records are automatically generated. When the super administrator account is created (register/the first account in the workspace) or any subsequent super administrator login, if there is no root record on Exchange, the super administrator's client (either CLI / Web / Android) will publish its own key as the root locally and immediately send a 90-day invitation. Later, when the invitation is missing or will expire within 30 days, it will be refreshed with the issuance throttling. remote-cli seal root --publish can still be executed manually.

One-time setting

  1. The super administrator registers or logs in to any client: the root is automatically generated and announced (CLI prints a prompt on stderr; the console pops up a prompt).
  2. Keep the root private key (choose one or both):
    • Just keep it yourself and upload nothing: remote-cli seal root --export root.rsk (0600 file; - is output to stdout), or "Export root private key" in the console.
    • Manual cloud backup settings: remote-cli seal backup, or console "Root Private Key Cloud Backup". The password can be left blank, but if the password is blank or too weak, Exchange can unlock the root private key and take control of all devices.
  3. As long as the device supports sealed sessions and is connected to Exchange, it will automatically fix the root (TOFU); a more reliable approach is to set remote-agent seal root <根公钥> on the device before going online.
  4. New members will register their public keys after logging in. Automatic approval is enabled by default in CLI, Web, and Android, and the root client will sign directly. If automatic approval is turned off: remote-cli seal pending View, remote-cli seal approve --all (or console "Approve") will issue immediately after approval.
remote-cli seal root                       # 显示已固定的根公钥和指纹
remote-cli seal root --export root.rsk     # 导出根私钥(rsk1. 文本)
remote-cli seal root --import root.rsk     # 在另一台机器装上根私钥(或 seal restore)
remote-cli seal root --rotate              # 生成新根,旧根签 rsx1. 轮换,自动按新根重签成员授权
remote-cli seal invite                     # 单独刷新邀请(默认 90 天,--days N)
remote-cli seal pending
remote-cli seal approve --all              # 或 seal approve <key>…;--no-auto-approve 关闭自动批准(保持开启较弱)

Change to another machine/change root

  • Use root on other devices: After the super administrator logs in, if the machine does not have a published root and there is rsb1. cloud backup on Exchange, CLI / Web / Android will automatically try to restore (see "Automatic recovery at login"). Otherwise, the client prompts to manually import: seal root --import <文件|->, paste rsk1. in the console and then "Import the root private key", or restore the encrypted backup from Exchange (seal restore / console "Restore from Exchange"). What is imported is the current root and is only installed on the local machine without changing any records.
  • Rotation: seal root --rotate (Console "Generate new root and rotate") or import a rsk1. key that is different from the current root. When the local machine holds the current root, the client uses the old root to sign a rsx1. rotation record, together with the new rsr1. PUT /api/seal/root; Exchange pushes it to the online device, and the device verifies that the rotation is signed by the fixed root and then changes to the new root, and the offline device receives it the next time it connects. Members approved by the old root will continue to be considered approved on this machine, and then all member authorizations will be re-signed with the new root (90 days). The old local key remains seal-identity.<毫秒>.previous.pk8. Please re-export or back up the new root after rotation.
  • Old root has been lost: Unable to sign rotation record. Clients will still advertise the new root, but devices that have the old root pinned will not accept it; remote-agent seal root <新根公钥> needs to be executed on each device and the agent restarted.
  • Super Administrator Handover: See "Super Administrator Handover = Root Certificate Handover". Simply changing the account role is not enough, the device trusts the root private key.

Automatically restore the root private key when logging in

After a superadministrator logs in successfully, before creating the local sealed identity, CLI / Web Console / Android Console use the same pure decision table (Rust decide_auto_restore, JS decideAutoRestore) to attempt recovery of the published workspace root from the Exchange rsb1. backup:

1.Skip: Not a super administrator; the machine already holds the same key as the published root; cloud backup is turned off (CLI: REMOTE_SEAL_CLOUD_BACKUP=0; Web: localStorage.seal.cloudBackup=off; Android: SecureStore remote.seal.cloudBackup=off); there is no published root on Exchange. 2.Pull a backup: GET /api/seal/backup (limited by the number of times per hour). When there is no backup, it prompts that the root is elsewhere and does not block login. 3.Silent attempt with empty password: Decrypt with empty string first. When it is successful and the decrypted public key is equal to the published root and the machine does not have another different key, it is loaded into the machine and fixed as the workspace root. 4. Otherwise the password will be prompted: CLI tries again REMOTE_SEAL_BACKUP_PASSPHRASE, the input is hidden on the TTY; Web/Android only sends non-blocking prompts, please enter the password on the seal page and click "Recover from Exchange". The blocking dialog box does not pop up and the login is completed as usual. 5.Security Boundary: Never silently overwrite another sealed key that already exists on this machine (recovery requires seal restore --force or the console to confirm); the decrypted public key must be equal to the published root; only a warning will be given on failure and no login failure will occur; passwords, seeds or rsb1. ciphertext will never be printed. 6.Risk warning (non-blocking): After installing the root, it will prompt "Multiple machines holding the root will increase the risk of leakage"; if the empty password is successfully unlocked, it will prompt that Exchange can unlock the root private key.

Superadministrator handover = root-key handover

The superadministrator account role and the workspace root trusted by devices are separate. Transferring the role alone does not give the new superadministrator the root private key or device trust. Handover transfers the root private key, followed immediately by rotation to invalidate the transferred key.

  1. The current superadministrator initiates transfer while the local client holds the published root: POST /api/superadmin/transfer, with body {operator_id, root_handover?}. root_handover is a passphrase-encrypted rsb1. package using the cloud-backup algorithm (Argon2id, 64 MiB, t=3, p=1, and ChaCha20-Poly1305). The passphrase may be empty. CLI: remote-cli seal handover <账号>. When transferring the superadministrator role in Web/Android Console, a client holding the root private key prompts for an optional passphrase and includes this package.
  2. Exchange only saves, not decrypts. It checks that the prefix is ​​rsb1. or rsk1., is no longer than 16384, and contains only ASCII URL-safe characters. When there is no root_handover in the request, delete the remaining handover package of the workspace.
  3. The new super administrator GET /api/superadmin/handover retrieves {blob, from_operator, created_at} (if not, 404), unlocks it with the password, installs it to the local machine, and then performs the same rotation as "generate new root and rotate": the old root issues rsx1., and publishes the new root. Finally DELETE /api/superadmin/handover. CLI: remote-cli seal accept-handover. The seal page of the console displays "Receive root private key handover" when there is a handover package.
  4. Rotation makes old copies invalid. What is handed over is the root before rotation. Online devices only switch to the new root if the rotation record is signed by the old root they already trust. The old private key in the hands of the predecessor cannot sign the authorization of the new root, nor can it change the root of the device that has been converted. Before the rotation is completed, the empty password or weak password rsb1. can still be unlocked; please pass the password separately.

Out-of-band: When you don't want to put the ciphertext into Exchange, use remote-cli seal handover <账号> --out-of-band for the CLI. The standard output has only one line rsb1., and the transfer request does not have root_handover. Give this line and password to the other party through other channels, and the other party will execute remote-cli seal accept-handover <那一行>. Click Cancel on the password prompt in the console (Android only transfers the account). It also only transfers the role and does not upload the root private key; the root private key still needs to be handed over separately (export rsk1., or out-of-band rsb1.).

Handshake and line format

CLI                         Exchange                       agent
 │ open_sealed{session_id,    │                               │
 │   device_id, hello}        │──sealed_open{session_id,hello}▶│ 验证 hello 签名、授权 → 级别
 │                            │◀─sealed_accept{session_id,     │
 │◀─sealed_accept (原样转发) ──│        accept}                │
 │ 验证设备签名 + pin          │                               │
 │ sealed_input{packet} ─────▶│──sealed_input────────────────▶│ 解密、校验级别、执行
 │ 二进制帧 kind=4 (PTY 键入)─▶│── 只改写帧头编号 ─────────────▶│
 │◀── sealed_event{packet,last}│◀── sealed_event ──────────────│ 所有输出在出站队列加密
 │◀── 二进制帧 kind=4 ─────────│◀── 二进制帧 kind=4 ───────────│
  • hello = {v, operator_key, operator_ephemeral, nonce, grant?, sig}, the signature covers ["remote-seal-v1/hello", v, device_id, operator_key, operator_ephemeral, nonce, grant.sig|""] (JSON array, i.e. canonical bytes).
  • Root record rsr1. signature covers ["remote-seal-v1/workspace-root", v, workspace_id, root_key, issued_at_ms]; rotation rsx1. = {v, workspace_id, old_root, new_root, issued_at_ms, sig}, signed by old_root over ["remote-seal-v1/root-rotation", v, workspace_id, old_root, new_root, issued_at_ms]. Exchange → agent: {"type":"seal_root","token":"rsr1.…","rotation":"rsx1.…"?} (only accepts clear text control messages from Exchange, invalid in the tunnel).
  • accept = {v, device_key, device_ephemeral, nonce, level, sig}, the signature covers ["remote-seal-v1/accept", v, device_id, operator_key, operator_ephemeral, hello.nonce, hello.sig, device_key, device_ephemeral, nonce, level]. Exchange changing to level will invalidate the signature.
  • Session keys: salt = SHA256(accept 签名字节), PRK = HKDF-Extract(salt, X25519(o, b)), K_od = Expand("remote-seal-v1/operator_to_device"), K_do = Expand("remote-seal-v1/device_to_operator").
  • AEAD: ChaCha20-Poly1305, AAD = remote-seal-v1, nonce = channel bytes (0 text / 1 binary) ‖ three zero bytes ‖ u64 counter (big endian). The counter in each direction and each channel is strictly increased by one from 0; any discrepancy directly closes the session.
  • Text: {"counter":n,"ciphertext":"<base64>"}. The clear text is JSON: operator → device is SealedRequest (existing DeviceDown message, or root management message seal_policy_get / seal_policy_set {mode?, root_key?, revoke?, unrevoke?}); device → operator is SealedResponse (existing DeviceUp message, or seal_policy). The inner layer session_id is rewritten as the tunnel number by the agent.
  • Binary frame: StreamKind::Sealed = 4, payload = counter(u64 BE) ‖ AEAD(inner_kind ‖ bytes), inner_kind is 1/2/3 (stdout/stderr/PTY).
  • Each sealed session carries one operation (one exec, one PTY, one file transfer...), following the 1:1 session model. The termination message is encrypted by the agent and marked last:true in the outer layer. Exchange releases routing and backfill auditing accordingly, without the need to understand the content.
  • close_session → sealed_close{detach:false} (termination); operation and maintenance disconnect → sealed_close{detach:true} (judged by the agent: hold exec only detach, other termination).
  • Maximum size: approximately 2.8 MiB for ciphertext base64 (accommodates 2 MiB Android screenshot), outer packaging MAX_SEALED_WIRE.

Agent-side defense line: The plaintext belonging to the sealed session number in the outbound queue will only be encrypted or discarded, and the number is permanently isolated after the session ends (the late output of the hold process is discarded instead of being sent in plaintext); the hold job started/attached through the sealed session does not appear in the list of the old path, nor can it be attached/tail/cat/killed by the old path.

Root private key export and backup

The root private key can control all devices in the workspace. The storage method is determined by the super administrator, Console uploads an empty-passphrase backup by default; delete it to disable automatic saving:

  • Export (no upload): rsk1. + base64url (no padding) 32 byte seed. CLI seal root --export FILE|- (file 0600), console "Export root private key" (Web is displayed in the input box, Android is copied to the clipboard). Anyone who gets it can control the entire device.
  • Encrypted backup to Exchange (automatic in Console, manual in CLI): Only super administrators can upload if the local key is the published root. The ciphertext is a versioned rsb1. token (base64url encoded JSON, up to 4096 characters). The key is derived from the password by Argon2id, and the production parameters are fixed at 64 MiB of memory, iteration 3, and parallelism 1, written in the token. The salt is 16 bytes, the nonce of ChaCha20-Poly1305 is 12 bytes, and is randomly generated for each backup. The additional authentication data is remote-seal-v1/key-backup:{version}:{public_key}. If the version, public key or ciphertext is changed, the verification will fail; after unlocking, it must be checked that the public key derived from the seed is equal to the public key in the token. The decoder rejects parameters that are too weak or too large before running Argon2 (memory 8 KiB–256 MiB, production decryption at least 64 MiB; iterations 1–8, production at least 3; parallelism can only be 1).
  • Password is optional, strength is not mandatory. Empty passwords also use Argon2id (the password is an empty string), and the format is the same. The client only prompts: When the password is empty or weak (less than 12 characters, too few character categories, repeated, common passwords), CLI / Web / Android will clearly warn: Exchange (or someone who can read the database) can unlock the root private key and control all devices. Rust and JavaScript use the same set of prompt rules and the same set of test vectors (including empty password vectors).

PUT / GET / DELETE /api/seal/backup only serve the current login account; PUT and GET only allow super administrators. The request body is {blob}. Exchange checks the rsb1. prefix and length and never decrypts it. GET Each account, each process, up to 5 times per hour, if exceeded, returns 429; this only slows down online guessing, and weak passwords can be exhausted offline after the library is copied. Reading, writing, and deletion will be audited, and the details are only the number of bytes.

  • CLI: remote-cli seal backup (hide entering the password twice, you can directly enter and leave it blank; or --passphrase-stdin); remote-cli seal restore (add --force when there is a different key, and keep the old key as seal-identity.<毫秒>.previous.pk8). After recovery, fix it as the workspace root of the machine. Recovery is also automatically attempted when logging in (see previous section). Do not print private keys, passwords, or ciphertext.
  • Web Console: The root private key must be exportable. When a super administrator generates root, a browser that holds a non-exportable WebCrypto key will instead use the local seed in IndexedDB as the root. Login bootstrap automatically resumes before creating identity; localStorage.seal.cloudBackup=off can be turned off.
  • Android Console: Seeds are in expo-secure-store. bootstrapSeal also automatically restores first; remote.seal.cloudBackup=off can be turned off.

Use

remote-cli seal show                       # 生成/显示本机操作员公钥与指纹
remote-cli exec lab-router -- uname -a
remote-cli shell lab-router
remote-cli seal policy lab-router          # 根:查看设备授权与吊销策略

# 根给同事手动授权(通常不需要:根客户端按角色自动签发)(默认 90 天;对方已登录的客户端会自动登记公钥)
remote-cli seal grant --device lab-router --to alice --level view
# 或仍用公钥;--no-publish 只打印 token
remote-cli seal grant --device lab-router --operator-key <KEY> --level view --no-publish
# 手动续签即将到期的已发布授权(默认 --before 30 --days 90;上线时也会自动续签)
remote-cli seal renew
# 同事:拉取 Exchange 上签给自己的授权(登录后也会自动 sync)
remote-cli seal sync
# 仍可手动导入
remote-cli seal import-grant rsg1.xxxxx

# 根私钥:导出(不上传)或可选的加密云端备份
remote-cli seal root --export root.rsk
remote-cli seal backup
remote-cli seal restore                    # 已有不同钥匙时加 --force

# 带外固定设备指纹(在设备上 `remote-agent seal show`)
remote-cli seal trust-device lab-router <DEVICE_KEY>

Web Console / Android Console: All device operations use native sealed sessions: the command, hold List/kill/tail/cat, terminal, files, screenshots and input (Android/desktop), interface tree, sensors, ubus, system logs, and self-update all go through sealed tunnels; "🔒 End-to-end encryption" is displayed on the page. Web device details or Android Settings → "End-to-end encryption" panel is equivalent to CLI: display local public key/fingerprint, workspace root (super administrator: export/import/rotate root private key, receive root private key handover, approve new public key), view/modify device keys and revocation policy (root required), issue authorization (output rsg1. token, CLI can directly seal import-grant), import authorization, trusted device public key, and automatic cloud backup of the root private key (the password can be empty, and Exchange is explicitly warned if the password is weak and can be unlocked). When connecting to a fixed device fingerprint (TOFU) for the first time, subsequent fingerprint changes will be directly rejected and prompted for verification. Ordinary members have three different keys on the CLI, browser, and mobile phone. After each registration, the root client will issue them according to their roles. If a super administrator wants to be root on multiple devices, he or she can upload a password backup and automatically restore it when logging in to a new device, or import the root private key into each device.

On the device: remote-agent seal show | root <key> | forget-root | revoke <key> (restart the agent after local changes; changes through sealed seal policy sessions apply immediately).

Default behavior

Scope Current behavior
CLI, Web Console, Android Console All device operations use native sealed sessions, without a disable switch or plaintext fallback
Device-local policy Every policy value, including allow, rejects plaintext operations
  • A new device pins the workspace root on connecting to Exchange (TOFU); the root and its signed grants authorize operations. If Exchange may be untrusted on first connection, run remote-agent seal root <key> locally before bringing the device online.
  • Unavailable identity keys or corrupted policy fail closed. Repair keys and policy locally before reconnecting.