Skip to content

Instantly share code, notes, and snippets.

@tobert
Last active February 28, 2026 16:17
Show Gist options
  • Select an option

  • Save tobert/277940e9af2d1d16991815314b430fa9 to your computer and use it in GitHub Desktop.

Select an option

Save tobert/277940e9af2d1d16991815314b430fa9 to your computer and use it in GitHub Desktop.
Quick SEP for MCP over SSH transport

SEP-0000: SSH Transport

Abstract

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.

Motivation

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:

  1. Generate a key (or use your existing one)
  2. Add the public key to the server's authorized keys (ssh-copy-id)
  3. Connect

No certificates to manage. No OAuth dance. No browser redirects. Just keys.

Specification

Architecture

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
Loading

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

Connection Establishment

  1. The server listens for SSH connections on a configured port.
  2. The client opens a TCP connection and performs the SSH handshake (RFC 4253).
  3. The client authenticates using its SSH key (RFC 4252, see Authentication).
  4. The client opens an SSH channel and requests the mcp subsystem (RFC 4254 §6.5).
  5. The MCP lifecycle begins: the client sends InitializeRequest, the server responds with InitializeResult.
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
Loading

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 access
  • exec — arbitrary command execution
  • pty-req — pseudo-terminal allocation
  • x11-req — X11 forwarding
  • direct-tcpip and tcpip-forward — port forwarding
  • auth-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.

Message Framing

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\n on 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.

Authentication

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.

Authorization

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.

Authorized Keys

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).

Authorization Enforcement

The server MUST enforce authorization restrictions at both the listing and invocation layers:

  • List filtering: tools/list, resources/list, and prompts/list responses 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, and prompts/get requests 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 tool name field
  • restrict-resources: the resource URI
  • restrict-prompts: the prompt name field

SSH Certificates

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.pub

This 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.

Server Identity

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.

Session Lifecycle

  • The SSH connection defines the session boundary.
  • A session begins when the InitializeRequest / InitializeResult exchange 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.

Shutdown

  1. The client sends EOF on the channel (closes its write end).
  2. The server SHOULD finish processing any in-flight request, then close the channel.
  3. 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.

Error Handling

  • 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.

Client Configuration

{
  "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.

Server Configuration

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)

Capability Advertisement

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.

Rationale

Embedded SSH Server, Not System sshd

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.

Subsystem, Not Exec

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.

Authorized Keys + Certificates

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.

Why Not mTLS?

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.

Transport Maintenance Burden

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.

No Resumability

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.

Prior Art

  • 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.

Backward Compatibility

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.

Security Implications

Strengths

  • 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.

Risks and Mitigations

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

Embedded SSH Server Hardening

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:
    • Key exchange: curve25519-sha256 (RFC 8731)
    • Host key / public key: ssh-ed25519 (RFC 8709)
    • Cipher: chacha20-poly1305@openssh.com or aes256-gcm@openssh.com
    • MAC (if not using AEAD cipher): hmac-sha2-256-etm@openssh.com
  • Note: the required cipher and MAC algorithms use the @openssh.com namespace. 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

Open Questions

  1. 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?

  2. 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?

  3. Server discovery: How does a client discover that an MCP server supports the SSH transport? Should the MCP server registry include SSH connection details?

  4. 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?

  5. 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 readonly restriction. Deferred until the MCP spec supports it.

Reference Implementation

Required before Final status. Planned approach:

  • Server library (Go or Rust): Embed an SSH server using x/crypto/ssh or russh, 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment