Skip to content

Security

carryOn is designed so that no server operator, network observer, or compromised infrastructure component can read your terminal data. The security model splits into two layers: local access relies on filesystem permissions and localhost trust, while remote access uses end-to-end encryption with forward secrecy. This page is a comprehensive reference for security-conscious users and auditors.

All local communication uses Unix domain sockets and localhost networking. No authentication is required for local connections - this follows the standard localhost trust model used by tools like Docker, SSH agents, and development servers.

The daemon listens on ~/.carryon/daemon.sock with 0600 permissions (owner read/write only). Any process running as the same user can connect. This is intentional - if an attacker has local access as your user, they already have access to your terminal sessions, shell history, and everything else in your home directory.

Each session’s holder process has its own socket at ~/.carryon/holders/*.sock, also with 0600 permissions. The daemon connects to these sockets to multiplex I/O between clients and session backends.

All carryOn configuration and state files live under ~/.carryon/. Non-sensitive files like config.json use standard 0644 permissions. Sensitive files like private keys and auth tokens use 0600.

The built-in web UI binds to 127.0.0.1:8384 by default. Because it uses a localhost trust model with no authentication, binding to 0.0.0.0 would expose your terminal sessions to anyone on the network. If you need to access the web UI from another machine, use SSH port forwarding or a VPN rather than binding to a public interface. You are responsible for tunneling and firewall configuration if exposing carryOn on a network.

carryOn uses OAuth 2.0 social login through Cloudflare Workers. Supported identity providers are Google, GitHub, and Microsoft. There are no carryOn-specific passwords to manage or leak.

Devices authenticate using the RFC 8628 Device Authorization Grant flow. When you run carryon remote login, the CLI displays a URL and code. You visit the URL in a browser, log in with your identity provider, and enter the code. The device receives a long-lived device token upon successful authorization.

The web app and API use session JWTs with a 1-hour lifetime. These tokens are automatically refreshed as needed. The daemon uses a separate long-lived device token for persistent authentication to the signaling service. Device tokens are revocable from the web dashboard - revoking a token immediately prevents that device from connecting to the signaling service.

When you run carryon remote login, an X25519 keypair is generated locally on the device. The public key is registered in the D1 database so other devices can encrypt data for this device. The private key never leaves the device.

  • Private key: ~/.carryon/remote/device.key with 0600 permissions
  • Public key: ~/.carryon/remote/device.pub with 0644 permissions (public by design)

Persistent devices (CLI daemon, desktop apps) keep their keys across restarts. The keypair is generated once during carryon remote login and reused until the device is deregistered.

Ephemeral devices (web browser sessions) store keys in memory or sessionStorage. Keys are cleaned up when the browser tab is closed or the session ends. Each new browser session generates a fresh keypair.

carryOn uses two distinct encryption layers for different purposes. Both ensure that infrastructure components never see plaintext terminal data.

When a remote client connects to a session through the relay, both sides perform an ephemeral key exchange:

  1. Both the daemon and remote client generate a fresh X25519 ephemeral keypair
  2. They exchange public keys through the relay during the pairing phase
  3. A shared session key is derived from the key exchange using HKDF
  4. All terminal I/O frames are encrypted with ChaCha20-Poly1305 using the derived key
  5. When the connection ends, the ephemeral keys are discarded

This provides forward secrecy - a new connection always gets new keys, even to the same session. If an attacker somehow obtains a session key, they cannot decrypt past or future connections. Compromise of the long-lived device identity key does not allow retroactive decryption of past relay connections.

Cached Data (Session Lists, Notifications)

Section titled “Cached Data (Session Lists, Notifications)”

Session metadata cached in the signaling service uses a sender-key model, the same approach used by Signal, WhatsApp, and Matrix:

  1. A random AES-256-GCM data key is generated and used to encrypt the payload (the session list, notification, etc.)
  2. The data key is individually wrapped (encrypted) for each recipient device’s X25519 public key
  3. The signaling server stores only the encrypted bundle - a single encrypted payload plus one wrapped key per device
  4. Any authorized device decrypts by unwrapping the data key with their private key, then decrypting the payload

This means the signaling server stores session lists but cannot read session names, metadata, or any other content. Adding or removing a device updates the set of wrapped keys without re-encrypting the payload.

ComponentSees plaintext?What it can access
Local clients (CLI, VS Code)YesFull terminal I/O via IPC (localhost trust)
DaemonYesAll session data, manages encryption for relay
Local Web UIYesTerminal I/O via WebSocket (localhost trust)
Signaling serviceNoDevice presence, encrypted blobs, connection metadata
D1 databaseNoAccount info, device public keys, encrypted session caches
Relay nodeNoOpaque encrypted bytes only, connection metadata
Remote clientYesDecrypted terminal I/O (has session key)

The daemon is the trust boundary. It holds plaintext session data locally and performs encryption/decryption for relay connections. Everything beyond the daemon - the signaling service, relay nodes, and the network between them - sees only encrypted data.

Relay nodes are stateless Go binaries that act as WebSocket proxies. They have no persistence, no database, and do not log terminal data.

When the signaling service orchestrates a connection between two devices, it issues a signed JWT pairing token with a 30-second expiry. Both the daemon and the remote client present this token to the relay. The relay validates the JWT signature without making any callback to the signaling service - verification is fully stateless.

  • The relay cannot pair arbitrary connections. It requires a valid signed token from the signaling service.
  • The relay cannot decrypt traffic. It has no access to the ephemeral session keys negotiated between the two endpoints.
  • Relay nodes are horizontally scalable. Any relay node can handle any connection - there are no sticky sessions, no shared state, and no affinity requirements.
  • If a relay connection drops, both sides fall back to the signaling service for re-pairing, which generates a new token and potentially selects a different relay node.

Impact: Minimal. A compromised relay node cannot read terminal data because all traffic is end-to-end encrypted with ChaCha20-Poly1305. It cannot forge new connections because pairing requires a valid JWT signed by the signaling service. The attacker can only observe encrypted traffic volume and timing metadata.

Impact: Limited. A compromised signaling service cannot read session lists or terminal data because cached data uses sender-key encryption. The attacker could potentially disrupt connection pairing or deny service by refusing to issue pairing tokens. They cannot forge device identity because that would require the device’s X25519 private key, which never leaves the device.

Impact: None on data confidentiality. TLS protects the transport layer. End-to-end encryption protects the payload independently of TLS. Even if TLS is compromised (for example, through a rogue CA or corporate proxy), the E2E encryption layer remains intact. An observer can see connection metadata (which relay a device connects to, connection timing and duration) but not terminal content.

Impact: Contained to that device. If a device’s X25519 private key is compromised, an attacker could decrypt data sent to that device - specifically, sender-key encrypted session lists and any active relay connections where the attacker can also intercept traffic. To mitigate, revoke the device via the web dashboard to remove it from recipient lists immediately. Other devices are not affected. Forward secrecy means past relay connections cannot be retroactively decrypted even with the identity key, because relay connections use ephemeral keys that are discarded after each connection.

Impact: Same as network observer. The service operator (the team running carryon.dev) cannot read terminal data at any layer. The system is designed for zero-trust of infrastructure. The operator can see account metadata, device presence, and encrypted blobs, but never plaintext session data. This is by design - even a fully compromised server-side infrastructure cannot expose terminal content.

All sensitive files are stored under ~/.carryon/ with restricted permissions.

FilePermissionsContentSensitivity
~/.carryon/daemon.sock0600IPC endpointAccess grants full session control
~/.carryon/remote/device.key0600X25519 private keyDevice identity - do not share
~/.carryon/remote/device.pub0644X25519 public keyPublic - registered in cloud
~/.carryon/remote/token.json0600Device auth tokenRevocable via web dashboard
~/.carryon/config.json0644User configurationNon-sensitive

If you suspect a device key has been compromised, revoke the device from the web dashboard and run carryon remote login again to generate a fresh keypair.

All cryptographic operations use Go standard library packages and golang.org/x/crypto. No custom cryptographic primitives are used.

OperationAlgorithmLibrary
Key exchangeX25519golang.org/x/crypto/curve25519
Key derivationHKDFgolang.org/x/crypto/hkdf
Relay frame encryptionChaCha20-Poly1305golang.org/x/crypto/chacha20poly1305
Sender-key data encryptionAES-256-GCMcrypto/aes + crypto/cipher
Pairing token verificationEd25519 / HMACcrypto/ed25519 or crypto/hmac

The choice of ChaCha20-Poly1305 for relay frames provides high performance on devices without hardware AES acceleration (mobile, older hardware). AES-256-GCM is used for sender-key encryption where the data is encrypted once and stored, rather than streaming.