
Parallel SSH service discovery and security auditor that scans any port, validates SSH banners, and audits authentication methods, weak cryptography, Terrapin vulnerability, and reused host keys across hosts and CIDR ranges.
Find every SSH service on your network, judge whether it meets your standard, and get told when that changes.
sshfinder is a single Python file with no required dependencies. Point it at
a CIDR range and it discovers SSH wherever it is actually listening — not just
port 22 — confirms each one really speaks SSH, assesses its cryptographic
posture, and returns a non-zero exit code when something fails your policy.
Most teams cannot answer three questions about their own SSH estate:
The existing tools each answer part of this and stop:
| Tool | Discovers SSH | Assesses it | Across a fleet |
|---|---|---|---|
nmap | yes | shallow, via NSE scripts | yes |
ssh-audit | no — you give it one host | deeply | no |
masscan / zmap | at internet scale | no | yes |
sshfinder | yes | yes | yes |
That gap — discovery and assessment and a verdict, in one artifact — is
what this tool exists to fill. If you only need to audit one host you already
know about, use ssh-audit; it goes
deeper on a single service than this does.
Penetration testers will find the audit and the SOCKS pivot useful, but the tool is shaped around running the same scan repeatedly against an estate you own, not around a one-off engagement.
git clone https://github.com/kabiri-labs/sshfinder.git
cd sshfinder
python sshfinder.py 10.0.0.0/24 -p 22,2222
No installation, no dependencies. Requires Python 3.9+.
The three things it does, in three commands:
# 1. INVENTORY — what SSH is out there?
python sshfinder.py 10.0.0.0/24 --audit --format csv -o ssh-inventory.csv
# 2. VERDICT — does it meet our standard? (exits 3 if not)
python sshfinder.py 10.0.0.0/24 -p 22,2222 --policy baseline
# 3. DRIFT — what changed since last night?
python sshfinder.py 10.0.0.0/24 -p 22,2222 --baseline yesterday.json \
--fail-on-drift
Scanning all 65535 ports is the default, because an SSH service on a non-standard port is precisely the one nobody has written down. Every open port is labelled, so an open port is never silently counted as an SSH one:
=== 10.0.0.5 ===
open: 10.0.0.5:22 [SSH], 10.0.0.5:8080 [not ssh]
SSH 10.0.0.5:22 (SSH-2.0-OpenSSH_7.4)
Confirmation is a real RFC 4253 identification exchange, not a glance at the first bytes on the wire. Servers that print a legal banner first, that wait for the client to identify itself, or whose banner arrives split across TCP segments are all recognised correctly — each of those is a false negative in a naive implementation.
Add --audit for the full picture of each service:
SSH 10.0.0.5:22 (SSH-2.0-OpenSSH_7.4)
host key: ssh-ed25519 SHA256:T/ZM4jOL4amTsO5K3AaCdg2...
auth: publickey, password [!] password auth enabled
[!] Terrapin (CVE-2023-48795): VULNERABLE
[!] weak ciphers: aes128-cbc
aes128-cbc [weak]: CBC mode is vulnerable to the SSH plaintext-recovery attack (CVE-2008-5161) and, …
Shared SSH host keys (possible shared/cloned hosts):
SHA256:T/ZM4jOL4amTsO5K3AaCdg2...
-> 10.0.0.5:22, 10.0.0.9:22
That last block is worth knowing about: a host key reused across machines usually means cloned VMs or a shared image, and it means compromising one host compromises the identity of all of them.
OpenSSH 10.0 made mlkem768x25519-sha256 the default key exchange, and 10.1
warns that classical sessions are open to store now, decrypt later capture.
--pq-report answers the fleet-level question directly, using only the
KEXINIT — so it needs no third-party library:
python sshfinder.py 10.0.0.0/24 -p 22,2222 --pq-report
Post-quantum readiness:
1/3 service(s) negotiate post-quantum key exchange with a current client
[!] no PQ key exchange offered (1):
10.0.0.2:22
[!] pre-standard PQ only (1) - looks post-quantum but is not:
10.0.0.3:22
2 service(s) exposed to store-now-decrypt-later capture; upgrade to OpenSSH 9.0+
The pre-standard category is the one that catches people out. A server
advertising [email protected] or a Kyber draft looks
post-quantum in an algorithm dump, but OpenSSH dropped that withdrawn
parameter set in 2020 — so a current client finds no common method and falls
back to classical crypto. Counted as ready, it would be worse than not
looking at all.
A report describes a problem. A policy asserts one, and can fail a build:
python sshfinder.py 10.0.0.0/24 -p 22,2222 --policy baseline; echo "exit $?"
Policy 'baseline':
No password login, no Terrapin exposure, no weak algorithms.
1/3 service(s) pass
[FAIL] 1 service(s):
10.0.0.3:22
- password_auth: password login accepted: publickey, password
- terrapin: vulnerable to Terrapin (CVE-2023-48795)
- post_quantum (warn): post-quantum readiness is absent, ready required
[warn] 1 service(s):
10.0.0.2:22
- post_quantum (warn): post-quantum readiness is absent, ready required
exit 3
Three policies ship built in — baseline, strict and pq — named for the
outcome they enforce rather than for a distribution. Rules carry a fail or
warn severity and --fail-on decides which gates, so a team can adopt a
stricter bar as a warning first and promote it later without editing anything.
Write your own as JSON:
{
"name": "house-rules",
"description": "What we expect of every SSH service.",
"rules": [
{"check": "password_auth", "severity": "fail"},
{"check": "terrapin", "severity": "fail"},
{"check": "post_quantum", "require": "ready", "severity": "warn"},
{"check": "forbid", "field": "ciphers",
"algorithms": ["3des-cbc", "arcfour"], "severity": "fail"},
{"check": "require", "field": "kex_algorithms",
"algorithms": ["curve25519-sha256"], "severity": "fail"}
]
}
Checks: password_auth, terrapin, weak_algorithms, post_quantum (with
require: ready, legacy or absent), and forbid / require over a
field of kex_algorithms, host_key_algorithms, ciphers or macs.
Anything else is a hard error when the policy loads, before the scan starts. A gate that silently skips a rule it does not understand is worse than no gate: the run goes green and nobody learns the check never executed.
$ sshfinder 10.0.0.0/24 --policy house.json
sshfinder: error: rule 1: unknown check 'pasword_auth'
(known: forbid, password_auth, post_quantum, require, terrapin, weak_algorithms)
Run it nightly against yesterday's report and see only what moved: