Security model#
This page describes what SSH Server Manager protects, what it deliberately does not, and the invariants the code enforces. The developer-facing rules live in references/security.md.
Trust boundary#
The tool assumes your OS user account is trusted. Anything running as
your user is inside the boundary — the same assumption OpenSSH itself makes
about ~/.ssh. The UI's re-authentication layer is an additional
disclosure gate for humans standing at your screen, not a sandbox against
malicious local code.
What it defends against:
- Secrets at rest on disk, in dotfiles, in backups, or in a synced repo
- Secrets leaking through argv, environment variables, logs, shell history, or an AI agent's context window
- Other machines on your network reaching the management UI
- Cross-origin browser attacks against the local API (CSRF, DNS rebinding)
- Shoulder-surfing and casual disclosure of stored secrets
- MITM via silently accepted host keys
What it does not defend against:
- Malware running as your user
- An attacker with your unlocked OS session
- A compromised remote server
- A malicious or compromised local skill file chosen by the same OS user
Where secrets live#
| Secret | Storage |
|---|---|
| Passwords, key passphrases | macOS Keychain / Windows Credential Locker / Linux Secret Service, service name ssh-server-manager |
| Private keys | Never stored, never read — only the absolute path |
| Master password | Argon2id hash in the local SQLite database |
| Passkey (WebAuthn) | Your platform authenticator (Touch ID, Windows Hello, security key) |
| Database, rendered config | User-only file permissions; contain no secret values |
The remote Files panel browses read-only and transfers only when you ask it to. Browsing invokes the OpenSSH SFTP subsystem through the same rendered config and AskPass boundary as other connections, rejects control characters in paths, quotes paths as literal SFTP arguments, and returns directory metadata rather than file contents.
Downloading and uploading are separate, explicit actions with their own rules:
- Both are
POSTroutes that require the browser session and a matching CSRF token. Download is deliberately not aGET: a link cannot carry the CSRF header, and a request that moves bytes off one of your hosts is exactly what a hostile page would try to forge. - Both are capped at 100 MB. Larger files belong on
serverctl get/put, which stream straight through sftp without staging. - Bytes are staged in a per-transfer temporary directory inside the private
runtime directory (
0700, owner-checked) and removed when the request ends, whether it succeeded or failed. - Nothing about credential handling changes. Transfers reuse the same rendered config, host-key policy, ProxyJump chain, and AskPass boundary as every other connection, so a transfer cannot see a secret that a listing could not.
Unsafe keyring backends (null, plaintext file, keyrings.alt) are rejected
as a hard error — the tool refuses to run rather than degrade to plaintext.
How SSH authentication works without exposing secrets#
serverctl never types your password. It sets SSH_ASKPASS to a small
helper and passes it only credential identifiers plus prompt-matching
metadata. When OpenSSH asks for a password or passphrase, the helper:
- Classifies the prompt (it refuses non-interactive host-key confirmation and OTP prompts; interactive first-use confirmation goes to the human's controlling terminal);
- Matches it to the right credential by username/hostname/key path;
- Fetches the secret from the OS vault and writes it directly to the ssh process.
The secret exists only in the memory of the helper and ssh. It never appears in command lines, environment variables, files, logs, or the calling process's output.
Host-bound skill routing#
Host-skill bindings are local routing metadata, not a capability or policy grant. The user or agent still selects the target hosts and operations through the normal workflow; binding a skill cannot authorize a command, install software, execute a hook, or weaken credential and host-key rules.
skill discover scans only local agent skill roots and SSM_SKILLS_DIRS; it
makes no network request and changes no state. Registration stores a normalized
absolute SKILL.md path plus its name and description. The database and
skill resolve do not copy or return the instruction body. Removing a
registration is blocked until every host has been detached and never deletes
the source files.
Names are case-insensitively unique. Same-name candidates at different paths
are reported as a conflict rather than selected by search order. At resolution
time, missing, invalid, or renamed files make the operation fail; agents must
not substitute another file or reuse context from the previously selected
host. Multi-host results include applies_to so one host's procedures are not
implicitly applied to another.
This mechanism is not code signing, content pinning, or a sandbox. A local skill file is trusted to the same extent as other code and configuration owned by the OS user, and it may change after registration. Review skills before registering them and protect their directories with normal filesystem permissions.
The web UI's layered defenses#
- Loopback only — binds
127.0.0.1; Host and Origin headers are validated to stop DNS-rebinding. - One-time launch token — the URL
serverctl uiopens carries a single-use token; it is exchanged for anHttpOnly,SameSite=Strictsession cookie and cannot be replayed. - CSRF token on every mutation, checked with constant-time comparison.
- Strict CSP (
default-src 'self', no frames, no external origins) andno-storeon all API responses. - Reveal gating — displaying a stored secret requires WebAuthn user verification (or the enrolled Argon2id master password, rate-limited to 5 attempts/minute). Grants are single-use, expire in 30 seconds, and the rendered secret is cleared from the DOM after 15 seconds.
Connecting or testing a server uses the vault secret without ever returning it to the caller — reveal authentication protects display, not normal operation.
Host keys#
StrictHostKeyChecking is never weakened. Automated checks and transfers
reject unknown or changed keys with host-key-untrusted. Under OpenSSH's
normal ask policy, interactive serverctl connect ALIAS shows a first-use
fingerprint in the human's controlling terminal even when AskPass supplies
a saved password or passphrase. Verify it out-of-band before explicitly
confirming; missing terminal input fails closed. Stricter configured policies
still apply. Investigate changed or revoked keys with the server administrator
before modifying trust records; the manager never automatically removes or
replaces them. There is no flag to bypass verification.
Reporting#
If you find a vulnerability, open a private security advisory or contact the maintainers directly rather than filing a public issue.