# net-ssh/net-ssh: a pure Ruby SSH client that will not negotiate a weak algorithm unless you ask twice

> Net::SSH is an MIT licensed pure Ruby implementation of the SSH2 client protocol, with port forwarding, SSH subsystems and parallel command execution over one connection. Its algorithm tables are the interesting part, because every legacy cipher, MAC and key exchange is off by default and must be opted into with a `+` prefix, so the secure choice is the default and the insecure one requires typing the name of the weakness.

**net-ssh/net-ssh** — Pure Ruby implementation of an SSH (protocol 2) client

- Repository: https://github.com/net-ssh/net-ssh
- Website: http://net-ssh.github.io/net-ssh
- Stars: 1,024 · Forks: 482
- Language: Ruby
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/net-ssh-net-ssh

## A protocol library, not a command line tool

Net::SSH is a pure-Ruby implementation of the SSH2 client protocol. It allows you to write programs that invoke and interact with processes on remote servers, via SSH2.

That framing matters, because the obvious question about a Ruby SSH library is why not shell out to the `ssh` binary. The answer the project implies is in the feature list, which is all about programmability rather than about interactive use. You can execute processes on remote servers and capture their output, run multiple processes in parallel over a single SSH connection, support SSH subsystems, and forward local and remote ports via an SSH connection.

Parallel execution over one connection is the feature that changes how you write the code around it. A script that opens forty SSH connections is a script that hits connection limits, pays forty times for the handshake and fails in confusing ways. One connection with forty channels is how an efficient tool does it, and a library that gives you channels is a library you can build a fleet management tool on.

Port forwarding is the other one. A library that can open a local port that tunnels to a remote host, and a remote port that tunnels back, is a dependency manager's building block and a bastion client's, and doing that with `ssh -L` from Ruby means parsing a command line rather than using an API.

The project is MIT licensed, currently on the 7.x line, with the documentation at net-ssh.github.io, and since version 2.6.4 all gem releases are signed, which is a supply chain property worth having for something that authenticates to your infrastructure.

## The plus sign is the whole security policy

Net::SSH only offers secure algorithms by default, and the project strongly recommends that you install a server version that supports the latest algorithms.

Weak algorithms are still supported as opt-ins, so legacy servers such as embedded routers and old network gear stay reachable. To enable one, prefix it with `+` in the matching option. The options named are `encryption: '+aes256-cbc'`, `hmac: '+hmac-sha1-96'`, `kex: '+diffie-hellman-group1-sha1'` and `host_key: '+ssh-dss'`, and the same opt-in works in an ssh_config through the `Ciphers`, `MACs`, `KexAlgorithms`, `HostKeyAlgorithms` and `PubkeyAcceptedAlgorithms` directives.

The `+` prefix is a good interface decision, and it is worth understanding why. A boolean flag like `allow_weak: true` gives you no record of what you just enabled, and the flag is likely to be copy-pasted between environments and left on in the one that matters. A `+` prefix in a specific algorithm name means the weakening is visible in the configuration, it is scoped to the one algorithm rather than to a category, and reviewing the file shows exactly which legacy cipher you are depending on.

There is a second mechanism, `append_all_supported_algorithms: true`, which appends every supported algorithm except the `none` cipher and MAC. Those two are only used when named explicitly, for example `encryption: '+none'` or `hmac: '+none'`, so there is one algorithm that a bulk switch can never turn on for you, and turning it on requires typing it out.

The effect is that the default configuration is the safe one, and every departure from it is a visible, named, greppable decision in your own configuration file.

## Host keys: Ed25519 and RSA-SHA2, with the SHA-1 era retired

The host key table is the most detailed part of the documentation and it reads like a history of SSH's cryptographic standards.

`ssh-ed25519` and its certificate variant are supported, backed by Ruby OpenSSL raw Ed25519 APIs, which the table annotates as requiring `openssl >= 3.2.0`. The `ecdsa-sha2-nistp521`, `384` and `256` families are supported but flagged as using weak elliptic curves, with a link to the safe curves project.

For RSA there is the modern split. `rsa-sha2-512` and `rsa-sha2-256` are supported, and `ssh-rsa` is not offered by default because it uses SHA-1 signatures, matching the behaviour of OpenSSH 8.8, with an opt-in for legacy servers through `HostKeyAlgorithms +ssh-rsa` or `host_key: '+ssh-rsa'`. The same applies to RSA public key authentication, which uses `rsa-sha2-512` or `rsa-sha2-256` by default, with `ssh-rsa` and `ssh-dss` keys only offered when enabled, for example with `PubkeyAcceptedAlgorithms +ssh-rsa` or `pubkey_algorithms: %w[+ssh-rsa]`.

The `ssh-dss` entry is marked not offered by default, described as unsecure and off by default as in OpenSSH 7.0.

One entry is a corner case worth knowing about, because it will bite you with an old server and the error message will not be obvious. `ssh-rsa-cert-v00@openssh.com` is deprecated, not offered by default, warns when used and will be removed, and net-ssh cannot parse v00 certificates at all, so you have to use `ssh-rsa-cert-v01@openssh.com` or a modern key type.

The pattern across the whole table is that the defaults match what a current OpenSSH would negotiate, and the exceptions are all named in the option that would enable them.

## Key exchange and ciphers follow the same rule

The key exchange table follows the same structure. `curve25519-sha256` and the `@libssh.org` variant are supported, again backed by the raw X25519 APIs in Ruby OpenSSL and marked as requiring `openssl >= 3.2.0`. The `ecdh-sha2-nistp521`, `384` and `256` entries are supported with the weak curves caveat.

The Diffie-Hellman group entries are where the retirement shows. `diffie-hellman-group-exchange-sha256` and `diffie-hellman-group14-sha256` are supported. `diffie-hellman-group14-sha1` is not offered by default, because of SHA-1, matching OpenSSH 8.2, and `diffie-hellman-group-exchange-sha1` and `diffie-hellman-group1-sha1` are described as unsecure and opt-in for legacy servers.

The cipher table is the same story. `chacha20-poly1305@openssh.com` is supported through the Ruby OpenSSL ChaCha20 and Poly1305 APIs with the same 3.2.0 requirement, and the AES counter and GCM modes are supported. The CBC modes, `rijndael-cbc@lysator.liu.se`, Blowfish, Cast128, 3DES and IDEA are all not offered by default and marked unsecure, and `none` is not offered by default either, with no encryption and usable only when named explicitly and never added by the bulk append option.

So the whole algorithm surface follows one rule, and the documentation does the tedious work of writing down which name corresponds to which weakness. The MAC table starts the same way, with `hmac-sha2-512-etm@openssh.com` supported and the older MACs handled the same way further down.

For an engineer reading a `net-ssh` configuration in a diff, the practical value is that the algorithm lists look like an OpenSSH configuration, so the knowledge transfers directly.

## The test environment is a real SSH server in a container

The `Dockerfile` in the repository shows how the project tests itself, and the detail is that it does not mock the server. It installs `openssh-server` along with `sudo` and netcat, creates two users, `net_ssh_1` and `net_ssh_2`, each with a home directory and a bash shell, sets their passwords, prepares the `.ssh` directories, and adds passwordless sudo for both in the sudoers file. It then generates a user CA key with `ssh-keygen`.

The compose file builds the same image for three Ruby versions, 3.1, 3.0 and 2.7.8, with the 2.7 build pinned to bundler 2.2.28 because that is what the older Ruby needs.

There is a small piece of archaeology in the Dockerfile that says the project has been paying attention to its own build. The Ruby 2.7 and 3.0 images are built on Debian bullseye, which only `archive.debian.org` still serves, so the build rewrites the apt sources when it detects bullseye in the release file.

The entrypoint starts the SSH service and runs `rake test`, which means the test suite runs against an actual `sshd` with actual users and actual keys, exercising authentication and the protocol rather than an in-process fake. For a library whose entire job is to talk to a server correctly, that is the right way to test it. There is also a `Dockerfile.openssl3` and a `Dockerfile.zlib_ng`, so the matrix covers the OpenSSL and zlib variants that affect the crypto and compression paths, plus a `docker-compose.yml` for the three Ruby versions.

## Where Net::SSH is the wrong tool

The first wrong case is a team that just needs to run a command. If the requirement is a script that shells out to a host and captures the output, the `ssh` binary is present on every machine and adds no dependency. Net::SSH earns its place when you need channels, port forwarding or a program that speaks the protocol as part of an application, and if none of those apply you are adding a gem to do something a subprocess call does.

The second is server side work. This is a client protocol implementation, and the repository carries a `SECURITY.md` for reporting vulnerabilities in it, which is a reminder that the project's scope is the client.

The third is a host without a modern OpenSSL. The Ed25519, X25519 and ChaCha20 paths are documented as requiring Ruby OpenSSL 3.2.0 or newer, and on an older Ruby distribution you lose the best host key and cipher options, which means the opt-in path becomes the normal path, which is exactly the configuration you did not want to end up with.

The fourth is a deployment that cannot install a gem, or an application that is not Ruby at all. The protocol implementation is a Ruby library, and while the language it is written in is not the strongest argument either way, the pure Ruby nature does mean the crypto comes from the host's OpenSSL bindings rather than from a bundled library, so the host's Ruby build is part of your security posture.

On the state of the project, the repository is not archived and the last push was on 2026-10-18, with the gem published rather than tagged on GitHub, and there is no GitHub release list to consult. The tree is that of a mature library, with a `Rakefile`, a `Gemfile`, `.rubocop.yml` with a todo file, `CHANGES.txt`, `DEVELOPMENT.md`, `THANKS.txt` and a `Manifest` for the gem packaging, and the gem has been signed since version 2.6.4.

## Conclusion

Adopt Net::SSH when you need to drive remote machines from Ruby, whether for deployment tooling, a monitoring agent or a device management script, and you want a library that speaks the protocol directly rather than a wrapper around the ssh binary. Do not adopt it expecting a general purpose remote administration toolkit, since it is the protocol layer and the higher level tools are separate gems. Verify first which algorithms your target server offers, since anything older than Ed25519 or AES-GCM needs an explicit opt-in, and that the Ruby OpenSSL version on the host is new enough, because the Ed25519, X25519 and ChaCha20 paths are documented as requiring openssl 3.2.0 or newer.

## FAQ

### How do I allow a weak algorithm in Net::SSH?

Prefix the algorithm name with `+` in the matching option, for example `encryption: '+aes256-cbc'`, `hmac: '+hmac-sha1-96'`, `kex: '+diffie-hellman-group1-sha1'` or `host_key: '+ssh-dss'`, or use the equivalent ssh_config directives such as Ciphers, MACs, KexAlgorithms, HostKeyAlgorithms and PubkeyAcceptedAlgorithms. The `none` cipher and MAC are only ever used when named explicitly.

### Which host key algorithms does Net::SSH support?

ssh-ed25519 and its certificate variant, backed by Ruby OpenSSL raw Ed25519 APIs requiring openssl 3.2.0 or newer, the ecdsa-sha2-nistp521, 384 and 256 families, and rsa-sha2-512 and rsa-sha2-256. The SHA-1 era options, ssh-rsa and ssh-dss, are not offered by default and must be opted in for legacy servers.

### What can Net::SSH do besides run a command?

It runs multiple processes in parallel over a single SSH connection, supports SSH subsystems, and forwards local and remote ports through the connection. The library description is a pure-Ruby implementation of the SSH2 client protocol for writing programs that invoke and interact with processes on remote servers.

### How does Net::SSH test itself?

With a real SSH server rather than a mock. The Dockerfile installs openssh-server, creates two test users with passwordless sudo, generates a user CA key, and the container entrypoint starts the SSH service and runs the test suite with rake, with compose services for Ruby 3.1, 3.0 and 2.7.8.

## Sources

- [Issues](https://github.com/net-ssh/net-ssh/issues)
- [License: MIT](https://github.com/net-ssh/net-ssh/blob/master/LICENSE)
- [net-ssh/net-ssh on GitHub](https://github.com/net-ssh/net-ssh)
- [Project website](http://net-ssh.github.io/net-ssh)
- [README](https://github.com/net-ssh/net-ssh/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/net-ssh-net-ssh
