Crussh
A low-level SSH server library for Ruby.
- Ciphers:
chacha20-poly1305@openssh.com
- Key exchanges:
curve25519-sha256curve25519-sha256@libssh.org
- Host keys:
ssh-ed25519rsa-sha2-256rsa-sha2-512ecdsa-sha2-nistp256ecdsa-sha2-nistp384ecdsa-sha2-nistp521
- Authentication:
nonepasswordpublickey
- Compression:
nonezlib@openssh.com
- Channels:
sessiondirect-tcpipforwarded-tcpipx11
- Other: - Strict key exchange (KEX) -
server-sig-algsextension -ping@openssh.comextension - OpenSSH keepalive handling.
Why SSH?
When we think about SSH, we almost exclusively think of it as a tool for remote shell access — ssh user@server and you're accessing a remote machine. But SSH is a protocol, not just a tool. Like HTTP, and it comes with some really nice benefits out of the box:
- Encrypted by default — No certificates to manage, no HTTPS setup
- Built-in authentication — Literally everyone and their mother has an SSH key
- Universal client — Everyone has a beautiful SSH client already
- Terminal-native — We've all got a terminal
You can build all kinds of things over SSH: git servers, file browsers, and even coffee shops.
Crussh is a library for building these kinds of things in Ruby.
Installation
Add to your Gemfile:
gem "crussh"Quick Start
require "crussh"
class HelloHandler < Crussh::Handler
before :log_connect
after :log_disconnect
def handle
puts "Hello, #{user}!"
puts "Your terminal is #{pty&.term || "unknown"}"
exit_status(0)
close
end
private
def log_connect
logger.info("Client connected", user:)
end
def log_disconnect
logger.info("Client disconnected", user:)
end
end
class HelloServer < Crussh::Server
configure do |c|
c.port = 2222
# Automatically generate host keys
c.generate_host_keys!
# OR load from a file
# c.host_key_files << "/path/to/host_key"
end
authenticate(:none) { true }
handle :shell, HelloHandler
end
Sync { HelloServer.run }Connect with any SSH client:
ssh localhost -p 2222
# => Hello, yourname!
# => Your terminal is xterm-256colorFeatures
- No OpenSSH — No OpenSSH dependency. Runs anywhere Ruby runs.
- Modern cryptography — ChaCha20-Poly1305, Curve25519, Ed25519 by default
- Async-native — Built on Async for concurrent connections and channels
- Clean DSL — Rails-inspired configuration and authentication
- Handler-based — Separate classes for shell, exec, and subsystem requests
- Low-level access — Drop down to raw channel I/O when you need control
Authentication
Crussh currently supports none, password and publickey auth. keyboard-interactive is planned — PRs welcome!
class MyServer < Crussh::Server
authenticate(:none) { |username| username == "guest" }
authenticate(:password) do |username, password|
Users.authenticate(username, password)
end
authenticate(:publickey) do |username, key|
AuthorizedKeys.include?(username, key.fingerprint)
end
endHandlers
Handlers are plain Ruby classes that process SSH requests. They inherit from Crussh::Handler and give you a clean, testable way to organize your logic:
class ShellHandler < Crussh::Handler
def handle
puts "Welcome, #{user}!"
puts "Type 'exit' to quit."
each_line(prompt: "$ ") do |line|
break if line == "exit"
puts "You typed: #{line}"
end
exit_status(0)
close
end
end
class ExecHandler < Crussh::Handler
def setup(command)
@command = command
end
def handle
IO.popen(@command, err: [:child, :out]) do |io|
IO.copy_stream(io, channel)
end
exit_status($CHILD_STATUS.exitstatus)
close
end
end
class MyServer < Crussh::Server
configure do |c|
c.port = 2222
c.generate_host_keys!
end
authenticate(:publickey) { |user, key| authorized?(user, key) }
handle :shell, ShellHandler
handle :exec, ExecHandler
endHandlers have access to:
-
user— the authenticated username -
pty— PTY info (term, width, height) if requested -
env— environment variables from the client -
channel— the underlying channel for advanced use - I/O methods:
puts,print,gets,read,write - Lifecycle:
close,send_eof,exit_status,exit_signal
Callbacks
Handlers support Rails-style lifecycle callbacks:
class MyHandler < Crussh::Handler
before :setup_environment
after :cleanup
around :with_timing
rescue_from IOError, with: :handle_disconnect
def handle
# ...
end
private
def with_timing
start = Time.now
yield
ensure
logger.debug("Duration", seconds: Time.now - start)
end
endInput Handling
Crussh provides three levels of abstraction for reading events on the channel:
class MyHandler < Crussh::Handler
def handle
# Low-level: raw SSH events
each_event do |event|
case event
in Channel::Data(data:)
# raw bytes from client
in Channel::WindowChange(width:, height:)
# terminal resized
in Channel::EOF
# client sent EOF
end
end
# Mid-level: parsed keystrokes
each_key do |key|
case key
when :arrow_up then move_up
when :enter then submit
when String then insert(key)
end
end
# High-level: line editing with prompt
each_line(prompt: "> ") do |line|
process(line)
end
end
# Called automatically by each_key/each_line on window resize
def resize(width, height)
@width = width
@height = height
redraw
end
endConfiguration
class MyServer < Crussh::Server
configure do |c|
# Network
c.host = "0.0.0.0"
c.port = 2222
# Keys (generate or load from files)
c.generate_host_keys!
# c.host_key_files << "/path/to/ssh_host_ed25519_key"
c.max_connections = 100
c.max_auth_attempts = 6
c.connection_timeout = 10
c.auth_timeout = 30
c.inactivity_timeout = 600
c.keepalive_interval = 30
c.keepalive_max = 3
end
endPro Tips
Development SSH Config
When developing locally, add this to ~/.ssh/config to avoid known_hosts conflicts:
Host localhost
UserKnownHostsFile /dev/null
StrictHostKeyChecking no
How It Works
Crussh implements the SSH protocol from scratch using Ruby and a small Rust extension for Poly1305. OpenSSH is never involved — you can uninstall it entirely if you want.
Because there's no default shell behavior, there's no risk of accidentally exposing system access. Your server only does what you explicitly implement.
Running with systemd
For production deployments, create a systemd unit file:
# /etc/systemd/system/myapp.service
[Unit]
Description=My SSH App
After=network.target
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/home/myapp
ExecStart=/usr/bin/ruby /home/myapp/server.rb
Restart=on-failure
[Install]
WantedBy=multi-user.targetThen:
# Create a dedicated user
sudo useradd --system --user-group --create-home myapp
# Enable and start
sudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myappDocumentation
Coming soon
- Getting Started
- Configuration Reference
- Authentication Guide
- Writing Handlers
- API Reference
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/MSILycanthropy/crussh.
License
Crussh is inspired by russh (Rust) and Wish (Go). Built on Async for Ruby's concurrent future.