- Status: Draft (reviewed by Claude, Gemini Pro, no human review yet, but will before submitting anything)
- Type: Standards Track
- Created: 2026-02-28
- Author(s): Amy Tobey tobert@gmail.com (@tobert)
- Sponsor: None
- PR: TBD
- SDK Prototype: https://github.com/tobert/go-sdk/tree/ssh-transport
- App Prototype: https://github.com/tobert/otlp-mcp/tree/ssh-transport
This SEP defines SSH as a transport and authorization mechanism for MCP, as an alternative to Streamable HTTP with OAuth. The MCP server embeds its own SSH server and manages key-based authentication and authorization directly — the same model that Git hosting services use for SSH access. Clients connect with their SSH key, the server maps the key to identity and permissions, and JSON-RPC messages flow over the SSH channel. No TLS certificates, no OAuth infrastructure, no dependency on system sshd.
Streamable HTTP with OAuth 2.1 is the right choice for multi-tenant web services, browser-based clients, and environments with existing identity providers. But it's the only option for remote MCP, and it carries significant infrastructure requirements:
- TLS certificates (issuance, renewal, trust management)
- An OAuth authorization server (or integration with one)
- Protected Resource Metadata and Authorization Server Metadata endpoints
- Dynamic Client Registration or Client ID Metadata Documents
- PKCE flows with browser redirects
For many remote MCP use cases — developer tools, internal services, homelab automation, CI/CD pipelines, server administration — this is a disproportionate amount of machinery. These environments typically already have SSH keys configured and don't need multi-tenant authorization.
Git solved this problem by offering SSH. You can git clone https://... with tokens,
or git clone git@...:... with keys. Both work; different trade-offs. MCP could offer
the same choice.
The SSH transport makes remote MCP as simple as:
- Generate a key (or use your existing one)
- Add the public key to the server's authorized keys (ssh-copy-id)
- Connect
No certificates to manage. No OAuth dance. No browser redirects. Just keys.
The SSH transport defines an MCP server that embeds an SSH server and an MCP client that embeds an SSH client. The SSH protocol handles encryption, authentication, and channel management. JSON-RPC messages flow over the SSH channel using the same newline-delimited framing as the stdio transport.
flowchart LR
subgraph Client Application
MC[MCP Client]
SC[SSH Client Library]
MC --- SC
end
subgraph MCP Server Process
SS[SSH Server Library]
MS[MCP Server]
SS --- MS
end
SC <-->|SSH Protocol| SS
UK[User's SSH Key] -.->|authenticates| SC
AK[Authorized Keys] -.->|authorizes| SS
Unlike the stdio transport (where the client spawns the server) or system-sshd approaches (where the server depends on external SSH infrastructure), the SSH transport server is a single process that handles both SSH and MCP. This is the same model used by Git hosting services, Gitea, and other applications that embed SSH servers.
This transport builds on the SSH protocol as defined in:
- RFC 4251 — SSH Protocol Architecture
- RFC 4252 — SSH Authentication Protocol
- RFC 4253 — SSH Transport Layer Protocol
- RFC 4254 — SSH Connection Protocol
- The server listens for SSH connections on a configured port.
- The client opens a TCP connection and performs the SSH handshake (RFC 4253).
- The client authenticates using its SSH key (RFC 4252, see Authentication).
- The client opens an SSH channel and requests the
mcpsubsystem (RFC 4254 §6.5). - The MCP lifecycle begins: the client sends
InitializeRequest, the server responds withInitializeResult.
sequenceDiagram
participant Client
participant Server
Client->>Server: TCP connect
Client->>Server: SSH handshake (key exchange, host key verify)
Client->>Server: Public key authentication
Server->>Server: Look up key fingerprint → identity + permissions
Client->>Server: Open channel, request "mcp" subsystem
Server->>Client: Channel confirmed
note over Client, Server: MCP session — newline-delimited JSON-RPC
Client->>Server: InitializeRequest
Server->>Client: InitializeResult
loop Message Exchange
Client->>Server: JSON-RPC request/notification
Server->>Client: JSON-RPC response/request/notification
end
Client->>Server: Close channel
Servers MUST use the mcp subsystem name. Servers MAY additionally
support named subsystems (e.g., mcp-files, mcp-db) to expose multiple MCP
server configurations on a single host.
Servers MUST reject the following SSH channel requests:
shell— interactive shell accessexec— arbitrary command executionpty-req— pseudo-terminal allocationx11-req— X11 forwardingdirect-tcpipandtcpip-forward— port forwardingauth-agent-req@openssh.com— agent forwarding
Servers MUST accept only subsystem requests for configured MCP subsystem
names. All other channel and global requests SHOULD be rejected.
Identical to the stdio transport:
- Messages are newline-delimited JSON-RPC, UTF-8 encoded.
- Messages MUST be terminated by a single newline (
\n, 0x0A). - Implementations MUST NOT emit
\r\n(CRLF) as the line terminator. Implementations SHOULD accept\r\non input for robustness. - Messages MUST NOT contain embedded newlines.
- The server MUST NOT write non-MCP content to the channel.
- Both directions share the same channel. The server MAY send JSON-RPC
requests and notifications to the client (e.g.,
sampling/createMessage,roots/list) interleaved with responses to client requests. Both sides MUST be prepared to receive any valid JSON-RPC message at any time. - The server MAY write to the SSH extended data channel (type 1,
SSH_EXTENDED_DATA_STDERR) for logging. The client SHOULD NOT interpret extended data as an error condition. Note that SSH extended data is defined as server-to-client only; clients MUST NOT send extended data.
Note that SSH channels are packetized (SSH_MSG_CHANNEL_DATA). A single
JSON-RPC message may span multiple SSH packets, and multiple messages may
arrive in a single packet. Implementations MUST buffer incoming channel
data and split on newline boundaries to extract complete messages.
The server authenticates clients using SSH protocol version 2 public key authentication. Implementations MUST NOT support SSH protocol version 1. The MCP authorization specification (OAuth 2.1) DOES NOT apply to this transport.
Servers MUST support public key authentication and SHOULD support SSH
certificates. The client's identity is determined by the public key
fingerprint presented during authentication (or the certificate's Key ID
when using SSH certificates), not by the SSH username. Key fingerprints
MUST use SHA-256 and MUST be represented in the format
SHA256:<base64> (the same format used by OpenSSH).
Clients MUST verify the server's host key. On first connection to an unknown server, interactive clients SHOULD prompt the user to verify and accept the host key (trust-on-first-use). Clients MUST reject connections where the host key has changed from a previously accepted value.
Automated (non-interactive) clients — such as CI/CD pipelines — MUST be
pre-provisioned with the expected server host key via the hostKey
configuration parameter and MUST NOT use trust-on-first-use, as TOFU is
vulnerable to MITM attacks when there is no human to verify the fingerprint.
Clients MUST persist accepted host keys for future verification. Clients
MAY use the system SSH known hosts database (~/.ssh/known_hosts) or a
dedicated MCP-specific store. The storage mechanism is implementation-defined.
Clients SHOULD use the local SSH agent when available for key access.
Clients SHOULD respect ~/.ssh/config for connection parameters (hostname
aliases, identity files, proxy jumps) when the system SSH configuration is
accessible.
The SSH transport combines authentication and authorization: the server maps each public key to an identity and a set of permissions. Two authorization models are defined.
The simplest model. The server maintains a mapping of authorized public keys
to identity and permissions. The configuration format is inspired by OpenSSH's
~/.ssh/authorized_keys but defines MCP-specific options — it is not an
OpenSSH authorized_keys file. How the server stores this mapping (flat file,
database, API) is an implementation detail; the format below defines the
logical schema and a standard file-based representation.
The server MUST support an authorized keys configuration with the following format:
# Each line: [options] key-type base64-key comment
# Options are comma-separated key="value" pairs before the key type
ssh-ed25519 AAAA...keydata... amy@workstation
ssh-ed25519 AAAA...keydata... ci-bot@jenkins
# Restrict to specific tools
restrict-tools="query_*,list_*" ssh-ed25519 AAAA...keydata... intern@laptop
# Restrict to specific resources
restrict-resources="file:///data/public/**" ssh-ed25519 AAAA...keydata... partner@external
The server MUST support the following options:
| Option | Description |
|---|---|
identity="name" |
Assign a logical identity to this key |
restrict-tools="pattern" |
Glob pattern limiting which tools the key may call |
restrict-resources="pattern" |
Glob pattern limiting accessible resources |
restrict-prompts="pattern" |
Glob pattern limiting accessible prompts |
Patterns use glob syntax as defined by IEEE Std 1003.2 §2.13
(fnmatch),
with the addition of ** for recursive matching (globstar). Specifically:
*matches any sequence of characters within a single path segment**matches zero or more path segments?matches any single character[...]matches a character class
Multiple options of the same type MAY be specified; permissions are the union of all matching patterns.
When no restrict options are present, the key has full access to all server capabilities.
The server SHOULD reload the authorized keys file without restart when it
changes (e.g., via SIGHUP or file watching).
The server MUST enforce authorization restrictions at both the listing and invocation layers:
- List filtering:
tools/list,resources/list, andprompts/listresponses MUST be filtered to include only items the client is authorized to access. Unauthorized items MUST NOT appear in list responses. - Call rejection:
tools/call,resources/read, andprompts/getrequests for unauthorized items MUST be rejected with JSON-RPC error code-32601(Method Not Found). Servers MAY use a more specific error message to aid debugging, since the client is already authenticated.
Restriction patterns are matched against:
restrict-tools: the toolnamefieldrestrict-resources: the resource URIrestrict-prompts: the promptnamefield
For organizational deployments, the server SHOULD support SSH certificates (PROTOCOL.certkeys).
SSH certificates encode identity and authorization in the certificate itself, signed by a trusted CA. This eliminates per-server key management — the server trusts the CA, and the CA issues certificates with the appropriate permissions.
The server trusts one or more CA public keys. When a client presents a certificate signed by a trusted CA, the server extracts identity and permissions from the certificate's fields:
| Certificate Field | MCP Usage |
|---|---|
| Key ID | Client identity (user name, service account, etc.) |
| Valid principals | Server checks against its required principal list |
| Critical options | Reserved for future SSH-level restrictions |
| Extensions | MCP-specific permissions (see below) |
MCP-specific permissions are encoded as certificate extensions using the
name@domain namespacing convention defined in OpenSSH's PROTOCOL.certkeys.
The namespace modelcontextprotocol.io is used (forward DNS, per SSH
convention; note that MCP _meta keys use reverse DNS per Java convention —
these are different namespacing systems):
| Extension | Value | Description |
|---|---|---|
restrict-tools@modelcontextprotocol.io |
Glob pattern | Tools the certificate may call |
restrict-resources@modelcontextprotocol.io |
Glob pattern | Resources the certificate may access |
restrict-prompts@modelcontextprotocol.io |
Glob pattern | Prompts the certificate may use |
Example certificate issuance:
ssh-keygen -s /path/to/ca_key \
-I "amy@example.com" \
-n mcp-user \
-V +8h \
-O extension:restrict-tools@modelcontextprotocol.io="query_*,list_*" \
-O extension:restrict-resources@modelcontextprotocol.io="file:///data/analytics/**" \
user_key.pubThis issues a certificate valid for 8 hours that restricts the holder to query/list tools and analytics resources.
The server MUST be configured with a list of accepted principals. When
authenticating with a certificate, the server MUST verify that at least
one of the certificate's valid principals matches its accepted principals
list. This follows standard SSH certificate verification — the principal
functions as a role or group name (e.g., mcp-user, mcp-admin,
mcp-readonly) that the CA asserts and the server checks.
When both authorized keys and certificate extensions specify restrictions, the server MUST evaluate both independently. A request is authorized only if it matches the certificate's restriction AND matches the authorized keys restriction. Both must permit the operation.
Servers MUST have a persistent host key pair used for SSH host authentication. Servers SHOULD use Ed25519 host keys.
The server's host key SHOULD be stable across restarts. Changing the host key will cause clients to reject the connection (host key mismatch), requiring manual intervention.
For environments with multiple server instances (load balancing, failover), all instances MUST share the same host key.
- The SSH connection defines the session boundary.
- A session begins when the
InitializeRequest/InitializeResultexchange completes. - A session ends when the SSH channel or connection closes.
- Sessions are not resumable. After disconnection, the client MUST establish a new connection and re-initialize.
The server MAY support multiple concurrent MCP sessions from the same client over separate SSH channels on a single connection.
Both sides SHOULD send SSH keepalive requests
(keepalive@openssh.com global requests, or TCP keepalives) to detect dead
peers. Implementations SHOULD consider a peer dead after 3 consecutive
missed keepalives.
- The client sends EOF on the channel (closes its write end).
- The server SHOULD finish processing any in-flight request, then close the channel.
- If the server does not close the channel within a reasonable timeout, the client MAY close the SSH connection.
Either side MAY close the channel at any time. The other side SHOULD treat unexpected channel closure as session termination.
- If the client requests an unknown or unauthorized subsystem, the server
MUST reject the channel request with
SSH_MSG_CHANNEL_OPEN_FAILURE. - If the MCP server process fails to initialize after the channel is opened, the server MUST close the channel immediately rather than leaving it open indefinitely.
- If SSH authentication fails, the server MUST limit retry attempts (no more than 6 per connection) and MUST rate limit authentication attempts per source IP address to mitigate key probing attacks.
{
"mcpServers": {
"devbox": {
"transport": "ssh",
"host": "devbox.example.com"
},
"analytics": {
"transport": "ssh",
"host": "analytics.internal",
"subsystem": "mcp-query",
"username": "readonly",
"identityFile": "~/.ssh/analytics_ed25519"
}
}
}| Parameter | Required | Default | Description |
|---|---|---|---|
host |
Yes | Hostname or IP address | |
port |
No | 2222 | SSH port |
subsystem |
No | mcp |
SSH subsystem name |
username |
No | mcp |
SSH username (see below) |
identityFile |
No | SSH default | Path to private key |
hostKey |
No | Known hosts | Server host key fingerprint (SHA256:<base64>) |
The username field is sent during SSH authentication but is not the
primary identity mechanism. Because the MCP server embeds its own SSH server
(not system sshd), there are no Unix user accounts to map to. The server
identifies clients by their public key fingerprint, not by username. This
is the same model GitHub uses — all users connect as git@github.com and
GitHub identifies them by which SSH key they authenticate with.
Servers SHOULD accept any username and MUST determine client identity
from the authenticated public key (or certificate Key ID). The default
username of mcp is conventional, like Git's git.
This SEP does not mandate a server configuration format, but a minimal server needs:
- A host key pair
- An authorized keys file or trusted CA public key(s)
- The MCP server capability configuration (tools, resources, prompts)
The server SHOULD include transport metadata in the InitializeResult
serverInfo to help clients understand the authorization model:
{
"serverInfo": {
"name": "my-mcp-server",
"version": "1.0.0"
},
"capabilities": { ... },
"_meta": {
"ssh": {
"authModel": "authorized_keys",
"keyFingerprint": "SHA256:abcdef123456...",
"identity": "amy@workstation"
}
}
}This is informational and non-normative. It allows clients to display who the server thinks they are, aiding debugging and transparency.
Depending on system sshd creates operational coupling — sshd configuration changes can break MCP, MCP security depends on sshd hardening, and deployment requires SSH access to configure sshd. An embedded SSH server makes the MCP server self-contained, like how HTTPS servers embed TLS rather than depending on a system TLS terminator.
This also enables single-binary distribution: download the server, generate a host key, add authorized keys, run it. No root access or system configuration needed.
That said, implementations MAY also work with system sshd by registering
as a subsystem in sshd_config. This is a valid deployment model, especially
in enterprise environments with existing SSH infrastructure and hardening
policies. The embedded model is the primary recommendation because it is
self-contained and does not require system-level configuration.
SSH subsystems avoid the entire class of problems around shell initialization output (MOTD, banners, rc files) corrupting the JSON-RPC stream. They also give the server explicit control over what runs — there's no shell interpretation, no PATH lookup, no argument injection risk.
The two-tier authorization model serves different scales:
-
Authorized keys are simple and familiar. A small team can manage them by hand, just like
~/.ssh/authorized_keys. This is the "get started in five minutes" path. -
SSH certificates scale to organizations. A CA issues short-lived certificates with embedded permissions. No per-server key distribution, no stale keys to clean up, time-bounded access by default. This is how Facebook, Netflix, and other large organizations manage SSH access.
Both use the same SSH authentication protocol — the difference is whether the server checks a local key list or validates a CA signature.
Mutual TLS (mTLS) with client certificates provides similar properties to SSH
key authentication — mutual authentication, encryption, no passwords. However,
mTLS requires the same certificate infrastructure that makes Streamable HTTP
heavy: a CA, certificate issuance and renewal, trust store configuration, and
TLS-specific tooling (openssl, certbot, etc.). SSH key management is
operationally simpler — ssh-keygen generates a key pair, you copy the public
key to the server, done. There is no CA, no expiration by default, no chain of
trust to configure. For the target use cases (developer tooling, internal
services), this simplicity is the point.
Adding a third transport increases the implementation surface for MCP SDKs. This is a real cost. The SSH transport mitigates it in two ways: (1) the message framing is identical to stdio, so the JSON-RPC handling code is fully reusable — only the connection layer is new, and (2) mature SSH libraries exist for every major language ecosystem. The incremental implementation effort is the SSH handshake, authentication, and channel management — not the MCP protocol logic.
SSH connections are persistent and reliable. SSH's own keepalive mechanism maintains connections through NAT and firewalls. When connections do fail, a clean reconnect with fresh initialization is simpler and more predictable than resumption state machines. The target use cases (dev tools, internal services, automation) tolerate brief reconnection.
- Git over SSH: GitHub, GitLab, Gitea all embed SSH servers that authenticate with keys, map keys to accounts, and authorize repository access. This is the closest prior art and the direct inspiration.
- SFTP servers: Often embedded (e.g., in Go, Rust) rather than relying on system sftp-server.
- HashiCorp Vault SSH: Issues SSH certificates with embedded permissions for time-bounded access — the same pattern proposed here for MCP authorization.
This SEP introduces a new transport. It does not modify stdio or Streamable HTTP. Clients and servers that do not implement the SSH transport are unaffected.
Servers MAY support both SSH and Streamable HTTP transports simultaneously on different ports.
- Encryption: All traffic encrypted (ChaCha20-Poly1305, AES-GCM, etc.) with no option to disable.
- No credential transmission: Public key authentication proves key possession without transmitting secrets. No passwords, no bearer tokens that can be stolen in transit or from logs.
- Mutual authentication: The client verifies the server (host key) and the server verifies the client (public key). Both directions are mandatory.
- No DNS rebinding: Direct TCP connection, no HTTP Origin header concerns.
- Audit: Every connection attempt is authenticatable to a specific public key.
| Risk | Mitigation |
|---|---|
| Host key compromise | Rotate host key, notify clients out of band |
| Client key compromise | Remove from authorized keys; use short-lived certs |
| Overly broad permissions | Default-deny with explicit restrict options |
| Resource exhaustion (DoS) | Rate limit connections, max concurrent sessions |
| Embedded SSH implementation bugs | Use well-audited libraries; fuzz test |
Servers MUST:
- Disable password authentication (public key only)
- Reject all channel requests except configured subsystems (see Connection Establishment)
- Enforce a pre-authentication timeout (analogous to OpenSSH's
LoginGraceTime). Connections that do not complete authentication within 30 seconds SHOULD be dropped to prevent resource exhaustion - Support the following algorithms at minimum:
- Note: the required cipher and MAC algorithms use the
@openssh.comnamespace. These are OpenSSH extensions, not IETF standards, but are universally supported by modern SSH implementations. No RFC-standardized AEAD ciphers are widely deployed for SSH at this time. - All required key exchange algorithms MUST provide forward secrecy
Servers SHOULD:
- Limit concurrent connections per client key
- Log authentication attempts
- Support key revocation (CRL or explicit revocation list)
- Support ECDSA (
ecdsa-sha2-nistp256,ecdsa-sha2-nistp384) keys in addition to Ed25519, for environments that require NIST curves
-
Default port: This draft uses 2222 as the default since port 22 requires root privileges, which contradicts the goal of zero-root deployment. Should we request an IANA assignment for a dedicated MCP-SSH port, or is 2222 (or operator-configured) sufficient?
-
Key discovery: Should there be a standard mechanism for clients to register their public key with a server (analogous to adding a deploy key on GitHub), or is that always out of band?
-
Server discovery: How does a client discover that an MCP server supports the SSH transport? Should the MCP server registry include SSH connection details?
-
Multiple subsystems: When a server exposes multiple subsystems (
mcp-files,mcp-db), should there be a discovery mechanism to list available subsystems, or is this purely a configuration concern? -
Read-only mode: MCP does not currently annotate tools as read-only vs state-modifying. If such annotations are added in the future, the authorized keys and certificate models should support a
readonlyrestriction. Deferred until the MCP spec supports it.
Required before Final status. Planned approach:
- Server library (Go or Rust): Embed an SSH server using
x/crypto/sshorrussh, handle key auth, authorized keys parsing, subsystem dispatch, and JSON-RPC framing. - Client library: Extend an existing MCP SDK with an SSH transport using an embedded SSH client library.
- Example server: A minimal MCP-over-SSH server demonstrating authorized keys and certificate-based auth.
- Example client config: Integration with an existing MCP client (e.g., Claude Code) demonstrating SSH transport configuration.