checker-ssh/README.md

177 lines
8.1 KiB
Markdown

# checker-ssh
Deep SSH security checker for [happyDomain](https://www.happydomain.org/).
Given an `abstract.Server` service (A / AAAA / SSHFP records), the checker
connects to the advertised SSH port(s) and produces a comprehensive
audit: reachability, banner-to-CVE matches, full algorithm posture
(KEX / host-key / cipher / MAC / compression), observed host keys,
SSHFP fingerprint validation, and authentication method exposure.
## What it checks
### Reachability
- TCP connect to port 22 (and optionally extra ports) on every A/AAAA
address of the service.
- SSH-2.0 protocol banner read.
### Banner / CVE
The banner is parsed into an `OpenSSH_X.Ypz` tuple and matched against
a bundled subset of the [ssh-audit](https://github.com/jtesta/ssh-audit)
vulnerability database, including:
| CVE | Issue |
| --- | --- |
| CVE-2024-6387 | regreSSHion - unauth RCE as root (8.5p1 <= v < 9.8p1) |
| CVE-2023-38408 | ssh-agent PKCS#11 RCE (5.5 ≤ v < 9.3p2) |
| CVE-2023-48795 | Terrapin prefix-truncation (v < 9.6p1) |
| CVE-2021-41617 | AuthorizedKeysCommand privdrop (6.2 ≤ v < 8.8p1) |
| CVE-2020-15778 | scp command injection (v < 8.4p1) |
| CVE-2018-15473 | username enumeration (v < 7.8p1) |
### Algorithm posture
A raw `SSH_MSG_KEXINIT` is exchanged with the server to enumerate every
algorithm it advertises. Each entry is graded against a curated table
inspired by ssh-audit:
- **crit**: `diffie-hellman-group1-sha1` (Logjam), `3des-cbc` (Sweet32),
`arcfour*`, `hmac-md5*`, `hmac-sha1-96`, `ssh-dss`, `none`.
- **warn**: `ssh-rsa` (RFC 8332 deprecated), `diffie-hellman-group14-sha1`,
AES-CBC, non-ETM MACs, `hmac-sha1`, missing strict-KEX marker
(CVE-2023-48795 mitigation).
- **ok**: `curve25519-sha256`, `sntrup761x25519-sha512@openssh.com`,
`mlkem768x25519-sha256`, `ssh-ed25519`, AES-GCM/ChaCha20-Poly1305,
SHA-2 ETM MACs.
### SSHFP validation
For each observed host key, the checker:
- computes SHA-1 and SHA-256 fingerprints,
- matches them against the `abstract.Server.SSHFP` records declared in
the zone,
- flags `sshfp_missing`, `sshfp_not_covered`, `sshfp_only_sha1` or
`sshfp_mismatch` as appropriate, with copy-pasteable fix snippets.
### Authentication methods
A second connection is opened with a dummy user and no credentials. The
server replies with the auth-method list, which is surfaced as
`password`, `publickey`, `keyboard-interactive` chips. Password
authentication triggers a `password_auth_enabled` warning.
## HTML report
The iframe report is structured for "fix me fast":
1. **Overall status** banner + SSHFP verdict chips.
2. **What to fix**: top issues (crit -> warn), each with a
copy-pasteable remediation snippet (sshd_config lines, SSHFP DNS
records, ssh-keygen invocations).
3. **SSHFP table** with per-record match status.
4. **Per-endpoint details**: expandable sections with host-key
fingerprints, algorithm tables (broken entries highlighted), and
advertised auth methods.
## Usage
### Standalone HTTP server
```bash
make
./checker-ssh -listen :8080
```
Endpoints:
- `GET /health`
- `GET /definition`
- `POST /collect`
- `POST /evaluate`
- `POST /report`
### Docker
```bash
make docker
docker run -p 8080:8080 happydomain/checker-ssh
```
### happyDomain plugin
```bash
make plugin
# produces checker-ssh.so, loadable as a Go plugin
```
## Options
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `ports` | string | `""` | Comma-separated extra ports (port 22 is always probed). |
| `probeTimeoutMs` | number | `10000` | Per-endpoint dial + handshake timeout. |
| `includeAuthProbe` | bool | `true` | Open a second connection to enumerate auth methods. |
## Rules
| Code | Description | Severity |
|-------------------------------|---------------------------------------------------------------------------------------------------|---------------------|
| `ssh.tcp_reachable` | Verifies that every probed (address, port) pair accepts a TCP connection. | CRITICAL |
| `ssh.handshake` | Verifies that the SSH banner exchange and KEXINIT parse succeed on every reachable endpoint. | CRITICAL |
| `ssh.protocol_version` | Verifies every endpoint advertises SSH-2 and rejects the legacy SSH-1 protocol. | CRITICAL |
| `ssh.banner_software` | Flags servers whose banner is not a recognised OpenSSH build. | INFO |
| `ssh.known_vulnerabilities` | Matches the advertised OpenSSH version against a curated catalog of remotely-observable CVEs. | CRITICAL |
| `ssh.host_key_strength` | Flags SSH host keys whose size is below the currently accepted minimum (e.g. RSA < 2048 bits). | CRITICAL |
| `ssh.kex_algorithms` | Flags key-exchange algorithms advertised by the server that are weak or broken. | CRITICAL |
| `ssh.host_key_algorithms` | Flags server host-key algorithms that are weak or deprecated (ssh-rsa/SHA-1, ssh-dss, ...). | CRITICAL |
| `ssh.cipher_algorithms` | Flags symmetric ciphers advertised by the server that are weak or broken (CBC, 3DES, RC4, ...). | CRITICAL |
| `ssh.mac_algorithms` | Flags MAC algorithms advertised by the server that are weak (SHA-1, non-ETM, ...). | CRITICAL |
| `ssh.strict_kex` | Verifies the server advertises the strict-KEX marker (CVE-2023-48795 Terrapin mitigation). | WARNING |
| `ssh.preauth_compression` | Flags servers that offer pre-authentication zlib compression (prefer zlib@openssh.com). | INFO |
| `ssh.auth_methods` | Reviews the advertised authentication methods (password exposure, public-key availability). | WARNING |
| `ssh.sshfp_alignment` | Compares published SSHFP records against the observed host keys (match, missing, mismatch). | CRITICAL |
| `ssh.sshfp_hash` | Flags SSHFP record sets that only publish SHA-1 (type 1) fingerprints instead of SHA-256. | WARNING |
## Observation key
Writes a single observation under `ssh`:
```json
{
"domain": "...",
"endpoints": [
{
"host": "...", "port": 22, "address": "...",
"banner": "SSH-2.0-OpenSSH_9.3p1",
"kex_algorithms": ["curve25519-sha256", "..."],
"host_keys": [{"type": "ssh-ed25519", "sha256": "abc..."}],
"auth_methods": ["publickey"],
"issues": [ { "code": "...", "severity": "warn", ... } ]
}
],
"sshfp": { "present": true, "records": [...] },
"collected_at": "..."
}
```
## License & licensing roadmap
This project is currently licensed under the **GNU Affero General Public
License v3.0** (see `LICENSE`), because it still imports
`happydns.ServiceMessage` and `abstract.Server` from the happyDomain
server module (`git.happydns.org/happyDomain/model` and
`git.happydns.org/happyDomain/services/abstract`), which are themselves
distributed under AGPL-3.0 and a commercial license.
The core checker types (`CheckerOptions`, `CheckerDefinition`,
`ObservationProvider`, `CheckRule`, …) have already been migrated to
[`checker-sdk-go`](https://git.happydns.org/checker-sdk-go); only the
service-message types remain on the AGPL side.
**Planned relicensing:** as soon as the remaining `ServiceMessage` /
`abstract.Server` dependency has been removed (moved into a dedicated
permissively licensed module), this project will be relicensed under the
**MIT License**, in line with the rest of the happyDomain checker
ecosystem (see `checker-dummy` for the target shape).
**Contributors notice:** by submitting a contribution to this repository,
you accept that your contribution will be relicensed from AGPL-3.0 to MIT
at the time of the relicensing described above. If you do not agree with
this, please do not submit contributions until the relicensing has taken
place.
The third-party Apache-2.0 attributions for `checker-sdk-go` are recorded
in `NOTICE` and must accompany any binary or source redistribution of this
project.