Skip to main content
Glama
README.md
# Linux SSH MCP

Independent Docker deployment of [tufantunc/ssh-mcp](https://github.com/tufantunc/ssh-mcp), pinned to npm version **2.16.0**. This project wraps the unchanged upstream MCP/SSH engine; it is not a new SSH implementation.

## Quick Start (New Operator)

Clone the repo, then create your **own** private config — you never copy anyone's keys.
Full detail and the VM-side steps are in [ONBOARDING.md](ONBOARDING.md).

```powershell
git clone <repo-url>
cd Linux-SSH-MCP
.\start.ps1 -Action Initialize     # creates your private folder %LOCALAPPDATA%\LinuxSSHMCP (empty profiles + token)
.\start.ps1 -Action Build          # builds and pins the secret-free image
# Prepare the VM and add a profile (see ONBOARDING.md sections 4-5):
#   - create a dedicated non-root SSH user, authorize a dedicated key, pin the SHA256 host key
#   - fill %LOCALAPPDATA%\LinuxSSHMCP\config\stdio.toml from profiles.example.toml
.\start.ps1 -Action RegisterClaude # register with Claude Desktop, then fully quit/reopen it
```

Code is shared and safe to commit; your profiles, keys and token live outside the repo
and must never be committed. See [ONBOARDING.md](ONBOARDING.md) for the step-by-step.

## Current State

- Two transports: per-client stdio for Claude/VS Code, and authenticated HTTP at `http://127.0.0.1:8814/`.
- Both configurations start with **zero VM profiles**. Tool discovery works; VM operations are refused.
- Up to 32 explicitly named profiles. Multiple VMs can use one deployment; separate inspection and administrative accounts/profiles are recommended.
- No Ubuntu VM has been contacted. No account, SSH key, authorized_keys entry, sudoers rule or firewall rule has been changed remotely.
- AD, WinRM, Microsoft 365, n8n and the Addis-Nearby workspace are independent and were not modified by this deployment.

## Local Commands

Run in this project using Windows PowerShell 5.1 with Docker Desktop running Linux containers:

```powershell
.\start.ps1 -Action Initialize
.\start.ps1 -Action Test
.\start.ps1 -Action Build
.\start.ps1 -Action Http
.\start.ps1 -Action Status
node .\verify.mjs --http http://127.0.0.1:8814/
```

`Initialize` creates private configuration without overwriting existing profiles or tokens. `Build` saves the built image's immutable SHA256 ID; launchers refuse an absent/invalid pin and never pull a substitute. `Test` runs 25 checks in a non-root, read-only, network-isolated Linux container. The SSH fixture generates temporary keys, only returns fixed text and never executes a shell command.

`verify.mjs` tests the actual Windows launcher, two concurrent stdio containers, 14-tool discovery, absent-profile refusal, persistent audit writes, resource/mount/port restrictions, EOF cleanup and optionally deployed HTTP authentication/token confidentiality. It only runs when both profile lists are empty, so it cannot accidentally contact a real VM. Its generated evidence is in `verification/report.json`.

`npm test` runs the 16 platform-independent configuration tests. Actual Ubuntu/OpenSSH interoperability and each client's approval dialog remain separate onboarding checks.

```powershell
.\start.ps1 -Action StopHttp
.\start.ps1 -Action Http -HttpPort 8815
```

Changing the port recreates only this project's HTTP service. Match the new URL in clients. Localhost remains the default; no automatic Docker restart policy is configured.

## Optional WSL HTTP Test

For a client on a separate Docker engine inside WSL, explicitly bind to the Windows WSL-facing IPv4 address. Find that address from `ip -4 route show default` in WSL and confirm it belongs to the Windows WSL interface. For example, if it is `172.25.160.1`:

```powershell
.\start.ps1 -Action Http -HttpBindAddress 172.25.160.1
```

The launcher rejects wildcard/public addresses and addresses not assigned to Windows. The same exact IP is added to the MCP Host allowlist; other private IPs are not automatically trusted. This is a bind restriction, not per-client authorization. Keep Windows Firewall enabled and restrict any required inbound rule to the intended WSL source address or subnet. Never disable the firewall to make the test pass. WSL addresses may change after a restart.

Plain HTTP does not encrypt bearer tokens or tool output. Use this opt-in mode only for a trusted, restricted local test; it is not a public/LAN production deployment. Add only explicitly approved read-only profiles to the private `config\http.toml`. Stdio profiles are not copied automatically. An empty HTTP configuration supports discovery but refuses VM tools.

Open WebUI 0.6.31+ supports native MCP Streamable HTTP. In Admin > Integrations > External Tool Servers, add a connection with Type **MCP (Streamable HTTP)**, URL `http://172.25.160.1:8814/` (using your actual address), and Auth **Bearer**. Enter the existing token privately in the Key field, not in a URL, chat, command argument or source file. Restrict Access Control to the intended test user; the token grants access to every profile in this HTTP instance. Do not use OpenAPI, Session or OAuth authentication for this deployment.

Check `/health` from inside the Open WebUI container first, then verify authenticated tool discovery. A health response is not an MCP handshake or a live SSH test. No MCPO bridge is needed for native Streamable HTTP. See the [Open WebUI MCP documentation](https://docs.openwebui.com/features/extensibility/mcp/).

To stop the test, use `.\start.ps1 -Action StopHttp`. To return to localhost-only access, use `.\start.ps1 -Action Http` without the address option and remove any test-only firewall rule you created. Existing Claude stdio connections are unaffected; changes to HTTP profiles require restarting only the HTTP service.

## Client Setup

For packaged Claude Desktop, `RegisterClaude` adds only `mcpServers.linux-ssh-mcp`, preserves other settings after a structured round-trip check, and creates a timestamped backup alongside the existing configuration:

```powershell
.\start.ps1 -Action RegisterClaude
```

The entry uses Windows PowerShell with this project's `start.ps1 -Action Stdio`. It contains no credentials. Registration does **not** restart Claude. Fully quit/reopen Claude when ready, then check Settings > Developer for `linux-ssh-mcp`. A launcher handshake is not proof that Claude has reloaded the entry.

This project's `.vscode/mcp.json` is ready when this folder is opened in VS Code. Start/trust the server explicitly there. It does not modify any other workspace's MCP configuration.

The optional HTTP endpoint uses MCP Streamable HTTP at `/`, not an SSE-only endpoint. Use a client that supports bearer authentication, entering the token privately in the client's protected secret setting. Do not place it in a URL, command-line argument, source-controlled JSON or chat. Claude's configured connection uses stdio and needs no HTTP token.

## Private Files

Default directory: `%LOCALAPPDATA%\LinuxSSHMCP`.

| File or directory | Purpose |
| --- | --- |
| `config\stdio.toml` | Inspection and explicitly approved administrative profiles |
| `config\http.toml` | Read-only profiles only |
| `keys\` | Dedicated SSH private keys, read-only mounted at `/run/keys` |
| `http-token` | Random 32-byte bearer secret, stored as 64 hexadecimal characters |
| `image-id` | Immutable local runtime image pin |

Initialization restricts directory ACLs to the current Windows user, SYSTEM and Administrators. Keep inherited ACLs on added key files and review them after importing a key. Do not mount your entire `.ssh` directory. Profiles/keys are excluded from the image build context and are not stored in this repository.

This initial bootstrap supports dedicated **unencrypted key files** protected by Windows ACLs and read-only mounts. Encrypted-key passphrases, SSH-agent forwarding, passwords and sudo-password delivery are not wired up. Do not remove encryption from an existing personal key to make it fit; agree on a dedicated deployment key or add a reviewed secret-delivery mechanism first.

## First Ubuntu VM

Supply the hostname/IP, SSH port, dedicated non-root username, local key-file path and independently verified host-key fingerprint. A key-file path and public host fingerprint are safe to share; private keys, passwords and passphrases are not.

From the VM console or another already trusted administrative channel, obtain the server's public fingerprint:

```sh
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
```

Use the complete `SHA256:...` fingerprint. Do not trust an unverified `ssh-keyscan` result as identity proof. The verified pin must match the host-key algorithm actually presented. A rebuilt VM or rotated host key requires fresh out-of-band verification, never disabling strict checking.

`profiles.example.toml` is an intentionally non-runnable inspection template. Replace its documentation-only address, username, key filename and invalid fingerprint placeholder using approved VM details. Place the completed profile in the private stdio configuration; add it separately to HTTP only when desired. Repeat `[[profiles]]` with unique names for more VMs. Restart only the affected MCP connection after edits.

Start with a dedicated inspection user without sudo, access only to the files/logs needed, and a narrowly authorized SSH key. VM-side account/key provisioning is an administrator action and has not been performed here. SSH access also needs reachability from Docker Desktop, not just from the Windows host.

First live verification should use an explicitly selected profile and benign commands such as `id` and `uname -a`. Review returned output before enabling further access. Logs, services, package tools and file visibility depend on Ubuntu permissions and the upstream command grammar.

## Administration

Use a separate stdio-only profile with `role = "admin"`, `readOnly = false`, an honest environment group and `approvalPolicy = "ask-all"`. Reusable approval grants remain disabled. A client without MCP elicitation support fails closed; never change to automatic approvals to bypass this.

Upstream `admin` in `prod` allows read-only/safe/destructive classes but **not privileged commands by default**. Granting privileged access requires an explicitly reviewed policy and VM-side sudo permissions. Do not mislabel production as `dev` to gain privileges. This deployment has enabled no such grant.

The upstream `privileged-command` tool wraps a command in `sudo ... sh -c`. That is a shell grant and is incompatible with pretending a generic sudo shell is a narrow service-only permission. For least privilege, review exact sudo commands/wrappers and the upstream policy together before enabling them. No blanket `NOPASSWD: ALL` setup is supplied.

The service exposes all 14 upstream tools, but their presence is not permission to use them. Read-only policy denies writes, shell escapes, sudo and process signals. The local streaming file tools are disabled because `transferRoot` is unset. Remaining SFTP operations still obey profile policy and remote filesystem permissions; they have not been live-tested against Ubuntu here.

## Security And Limits

- Non-root UID/GID 1000, read-only container root, no added capabilities, no-new-privileges, 512 MiB RAM, 96 PIDs, 1.5 CPUs and bounded temporary storage. No Docker socket mount.
- SSH host pins are mandatory in strict mode. Startup rejects missing/invalid configuration, root users, unbounded local transfer roots and unsafe administrative approval modes.
- HTTP is localhost-only by default; the explicit private-address option above is for a restricted local test. Bearer holders share access to the same configured read-only profiles, connections and sessions: this is **not multi-user isolation or per-user RBAC**. Do not expose it to a LAN or the Internet without a separately reviewed TLS/authentication design.
- The bearer token enters upstream only in in-process memory, not the OS command line or environment. Tests verify Docker metadata, logs and `/proc` arguments/environment. The server process, Windows administrator and Docker administrator can still access mounted secrets; Docker is not protection from its own administrator.
- The wrapper removes inherited `SSH_MCP_*` overrides and `SSH_AUTH_SOCK`. No credentials are stored in Claude JSON.
- Dedicated audit volumes: `linux-ssh-mcp_stdio-audit` and `linux-ssh-mcp_http-audit`. Per-container files live beneath `/home/node/.local/share/ssh-mcp/instances/<container-hostname>/ssh-mcp/`. Hashes are tamper-evident, not signatures or a guarantee against privileged tampering. Rotation is per instance; old instance directories require an approved retention/cleanup process. Logs can contain sensitive commands despite redaction. Never put secrets into commands.
- Read-only command classification is defense in depth, not an OS security boundary. Remote Unix permissions remain essential, and readable confidential data can still leave the VM through tool output. Treat remote output as untrusted data, not instructions.
- Stdio containers end when the client closes stdin. Multiple client-owned instances are normal. HTTP remains running until `StopHttp` or Docker shutdown. Stop/remove only this project's services, never use global Docker prune commands.

## Sources And Rollback

- [Pinned upstream source](https://github.com/tufantunc/ssh-mcp/tree/v2.16.0), inspected revision `b5fc51542a2b0f58410595a14b2a290ccbf42f54`.
- [Official MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk), pinned package 1.30.0.
- `package-lock.json` pins transitive packages; Dockerfile pins the Node base digest. Installed dependency licenses remain in their packages. Review upstream security advisories before updating pins; do not auto-upgrade production.

To disable: stop this project's HTTP service, remove only the `linux-ssh-mcp` client entry and restart that client when approved. Keep private configuration, audit volumes and timestamped Claude backups until a deliberate retention decision. No other MCP project's files or containers need to be removed.