Security
Local application
The engine serves its Web UI and API on a loopback address. UI URLs are issued on demand rather than logged at startup for long-term reuse. If the saved port is unavailable and the engine moves to another port, sshc revokes previous browser registrations and requires enrolment against the current engine.
On Linux, other OS users on the same machine can read the one-time registration URL that sshc passes to the browser, so the engine accepts a registration only from a connection owned by the same OS user or root. It checks the same way when a registered browser signs in again after the engine restarts.
On Windows, sshc uses the OpenSSH agent pipe (\\.\pipe\openssh-ssh-agent) only when it was created by LocalSystem, Administrators or the current user, so another user who creates a pipe with that name while the ssh-agent service is stopped receives neither keys added from the SSH Keys screen nor authentication requests.
A process running as the same OS user is assumed to have access to that user's SSH files already.
Remote sshc engines
The relay of sshc open <alias> and remote sessions reach an engine on an SSH host through the SSH connection.
- Each time it opens a connection to the remote engine, the local engine checks with the secret in the remote
~/.ssh/sshc/clithat the other side is that engine before it sends a browser request or credentials. If the remote engine restarts on another port and another OS user on the remote host listens on the old port, neither the cookie nor the vault password reaches that program. - If the check fails, nothing but the check is sent and an error is shown. A remote session reads the remote
~/.ssh/sshc/cliagain and reconnects once. For the relay in another tab, runsshc open <alias>again. - With a remote session, headers such as cookies returned by the remote engine are not passed to this machine's UI, so the remote engine cannot overwrite or clear this machine's sign-in.
- Terminal input and output of a remote session is forwarded only while you are signed in to this machine's UI and this machine's vault is unlocked.
Vault
The vault key encrypts:
- account passwords, key passphrases and TOTP setup keys used for SSH connections
- VPN profile secrets (WireGuard and OpenVPN configuration files, which contain keys, VPN passwords, IPsec pre-shared keys, and the TOTP secret for OpenConnect's second factor)
- snippets, sync settings and backups
With password protection, that key is derived from the master password.
- At least four characters are required, with no character-class restrictions. Protection against copied local files depends on password strength.
- Passwords are not accepted through command-line arguments or environment variables.
- Automatic locking defaults to 12 hours and can be configured in Settings.
The sync record (sshc/sync-state.json) stores the last synchronized vault and snippet contents only as hashes keyed with the vault key, so it cannot be used to check guesses about those contents. After an update from an earlier release, sshc rewrites the record into this form the first time the vault is unlocked (at engine start for a vault without a password).
Without a password, a random unlock secret is stored on the device and the vault opens automatically at startup. Idle locking does not apply. This does not protect against someone who can read both the device secret and ciphertext. The device secret is excluded from sync; remote data keeps using an independent encryption key.
VPN
Per-connection VPN starts one Docker container per VPN profile and connects to the VPN inside it. Check the following before using it.
- Anyone who can drive Docker generally holds strong privileges on this machine. Using VPN requires the OS user that runs sshc to be able to drive Docker.
- The container receives only the VPN settings and secrets and the target's address and port. Secrets never appear in command arguments or environment variables; they are written through standard input to an in-memory file (tmpfs) inside the container and disappear when the container stops. For OpenConnect's second factor, only the generated code is passed, never the TOTP secret.
- Settings that are not secrets, such as the type, VPN server and DNS inside the VPN, are stored outside the vault without encryption so that the list can show them while the vault is locked.
- No SSH private key, account password or vault key reaches the container. The SSH handshake, authentication and host-key checks run in the engine on this machine.
- Neither
--privilegednor--network hostis used. The devices and capabilities given to the container depend on the type; see Per-connection VPN. - Traffic to a target is fail closed. The container rejects packets to the target that would leave through anything but the tunnel, so while the VPN is disconnected nothing reaches the target over Docker's ordinary network.
- The VPN screen's logs and
sshc vpn logsreplace stored secrets with[REDACTED].
Change history and backups
When sshc saves a change, it keeps the previous files as backups in ~/.ssh/sshc/backups/ and a record of the change in ~/.ssh/sshc/history/. History can restore the earlier contents. Passwords removed from the vault and private keys from before a change also remain in these backups, encrypted.
sshc keeps the backups and records of changes that are within all of these limits:
- the newest 200 changes
- no older than 90 days
- 512 backup files in total, counted from the newest change. This limit is reached when changes that rewrite many files at once, such as receiving a sync or deleting a folder, follow one another. The newest change is kept even if its backups alone exceed 512 files.
The backups and records of changes outside these limits are deleted automatically the next time a change is saved or the engine starts. The backups and record of an interrupted change are kept until it is completed or rolled back. If a change interrupted more than 90 days ago is completed, its backups and record are deleted the next time a change is saved or the engine starts.
SSH host keys
Host keys are checked against every file in UserKnownHostsFile and GlobalKnownHostsFile, as OpenSSH does. A host with HostKeyAlias is looked up under that name. When the configuration names no file, these are used:
UserKnownHostsFile:~/.ssh/known_hostsand~/.ssh/known_hosts2GlobalKnownHostsFile:/etc/ssh/ssh_known_hostsand/etc/ssh/ssh_known_hosts2on macOS and Linux,%ProgramData%\ssh\ssh_known_hostsand%ProgramData%\ssh\ssh_known_hosts2on Windows
In an interactive terminal an unknown host key is saved after you confirm it; hosts configured with StrictHostKeyChecking no or accept-new save it without confirmation, as OpenSSH does, and hosts configured with yes (or true) do not save it and do not connect. The key is saved to the first UserKnownHostsFile. sshc saves only to files inside ~/.ssh; when that file is elsewhere, such as /dev/null, the key is not saved. sshc does not run KnownHostsCommand, so hosts that set it are asked about even with StrictHostKeyChecking no or accept-new.
When the file to save to, such as ~/.ssh/known_hosts, is a symbolic link, sshc does not write through the link, so a new host does not connect. Hosts already saved still connect. To add a new host's key, connect to the host once with ssh. When ~/.ssh itself is a symbolic link, keys are saved in the folder it points to.
A changed saved key is rejected regardless of that setting. Non-interactive SSH, SFTP and public-key installation require known keys for the final host and every ProxyJump hop.
The Known Hosts screen lists and deletes only the entries in ~/.ssh/known_hosts. Keys in ~/.ssh/known_hosts2, in other files named by UserKnownHostsFile and in GlobalKnownHostsFile are not shown there. If a changed key in one of those files blocks a connection, edit that file directly. When ~/.ssh/known_hosts is a symbolic link, the Known Hosts screen does not list it.
ProxyCommand
When SSH configuration contains ProxyCommand, sshc runs that command locally while connecting. Treat the SSH configuration and any included files as executable configuration. Use only files that you have inspected and trust.
Sync
Sync is end-to-end encrypted (E2EE): only devices configured with the same encryption key can decrypt it. Encryption uses AES-256-GCM, with the cipher key derived from the sync encryption key by Argon2id. Connection settings, Vault credentials, Snippets, and the SSH keys in scope are encrypted on this device with a dedicated sync encryption key before upload; the storage provider and anyone holding the S3 credentials cannot read them in plaintext. The bucket name, S3 object names, object sizes, and modification times are not encrypted.
Anyone with the ciphertext can try guessing the encryption key offline. Use the random key sshc generates rather than a short typed string.
Exported sync settings
The file and QR code made by Export settings hold the endpoint, bucket, path in the bucket, region, and the S3 access key ID and secret access key. They are encrypted with the encryption key in the same way as a snapshot (AES-256-GCM and Argon2id). The encryption key is not in them. This encrypted form is the only way the S3 credentials leave the sshc engine.
Anyone who obtains the file or the QR code can try guessing the encryption key offline, as with a snapshot's ciphertext, and a guessed key also reveals the S3 credentials. When you chose the encryption key yourself, the export screen shows a warning. Delete the exported file once the new device has it.
The Android app uses the camera only while it is scanning a QR code. The video is neither stored nor sent anywhere.
What remains after replacing the sync key
Replacing the sync key re-encrypts only the latest snapshot with the new key. The history already in the bucket stays encrypted with the previous key: sshc can no longer read it, but anyone who knows the previous key still can. Snapshots pushed after the replacement are encrypted with the new key.
If the sync key or the S3 credentials may have leaked
Replacing the sync key alone does not protect the history. Take these steps in order:
- Replace the sync key under Manage sync settings.
- Issue a new S3 access key, disable the old one, and save the new one in the sync settings.
- Change the SSH keys and passwords that the snapshots contained on the servers that use them.
- If needed, delete the older history under
snapshots/in the bucket path. If the bucket keeps object versions, delete the older versions too.
sshc has no command to delete older history. Delete it with the S3 console or tools. History that someone has already downloaded stays exposed even after it is deleted from the bucket.
Terminal data
Scrollback stays in memory and is not persisted or synced. OSC 52 clipboard access is configurable. An OSC 8 link whose text is a different address from where it goes shows the real destination before opening, even on Ctrl-click (⌘-click on a Mac). Images are checked for their pixel count and inflated size before they are decoded, and an image past the limits is not shown, so output cannot exhaust the browser's memory. A secret sent to a remote shell can still reach remote history, TTY echo or terminal output.
Telnet
Telnet neither encrypts traffic nor authenticates the server. sshc warns before connecting but cannot secure the protocol. Use it only behind another trusted network boundary when credentials are involved.
SFTP
sshc does not trust the file names and attributes that an SFTP server returns. In case you connect to a malicious or compromised server, it handles them as follows.
- When saving to this machine, sshc checks every name the server returned. If a name would point outside the destination folder, such as one containing
\or.., the transfer fails before it starts. - When downloading a folder as a ZIP file, sshc writes only the read, write and execute permissions of each file. It does not write the setuid, setgid or sticky bits.
- sshc reads at most 100,000 entries from a single folder listing. If the server never ends the listing, sshc stops reading at that limit and reports an error.
Updates
sshc update and the web UI update an installation made by install.sh or install.ps1 with the same installer from the latest stable release tag. The installer runs only when its SHA-256 matches the entry in checksums.txt of the same GitHub Release, so a script placed on the tag without going through the release workflow is never run.
When the update is started from the web UI, the installer's standard output and standard error, followed by the steps of restarting the engine, are written to ~/.ssh/sshc/web-update.log (%USERPROFILE%\.ssh\sshc\web-update.log on Windows) and shown on About sshc.
- At most 256 KiB is recorded for one update; anything after that is not recorded.
- Starting the next update replaces the previous record.
- The file is readable only by you and is excluded from encrypted sync.
- In the web UI, only a signed-in browser can read it, including while the vault is locked after the restart.
- sshc passes no tokens or passwords to the installer. The output is recorded as written, without masking.