29 Commits
Author SHA1 Message Date
Joe Testa dbf8b69633 Bump version numbers for 3.9.0 release. 2026-07-04 14:49:40 -04:00
Joe Testa 13fd7102d9 Added tabulation for "Not allowed at this time" banners (when the PerSourcePenalties directive triggers in OpenSSH 9.8 and later), along with tabulation for invalid SSH banners. 2026-07-04 14:40:38 -04:00
Joe Testa 419df1b788 Added hardening policy for OpenSSH 10.4. 2026-07-03 11:14:28 -04:00
Joe Testa 859cb7012e Added ssh-mldsa44-ed25519@openssh.com host key type. 2026-07-03 10:59:16 -04:00
Joe Testa 0f8bbdbfd9 Fixed bug where algorithm added in OpenSSH 10.x was being recommended for OpenSSH 9.x targets. 2026-07-03 10:58:48 -04:00
Joe Testa 1eb11050b3 Added 9 new host key types: 'mldsa-44', 'mldsa-65', 'mldsa-87', 'ssh-mldsa-44', 'ssh-mldsa-65', 'ssh-mldsa-87', 'ssh-mldsa44', 'ssh-mldsa65', 'ssh-mldsa87'. 2026-06-20 19:02:08 -04:00
Joe Testa 240840f580 Added note in README that support was added for Python 3.14. 2026-06-20 10:32:43 -04:00
Joe Testa 36def4b5ac Expanded SOCKS5 protocol support, refactored exception handling, updated documentation, added more tests. (#347) 2026-06-20 10:07:14 -04:00
f369689cd1 Add --socks argument for scanning via SOCKS5 proxy (#347)
* Add --socks argument for scanning via SOCKS5 proxy

Implement SOCKS5 proxy support without external dependencies:
- Add socks_proxy field to AuditConf
- Add --socks host:port CLI argument with input validation
- Implement SOCKS5 handshake (no-auth, domain-name addressing) in
  SSH_Socket._connect_via_socks5()

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Michał Majchrowicz <sectroyer@gmail.com>

* Add SOCKS proxy regression tests

---------

Signed-off-by: Michał Majchrowicz <sectroyer@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-20 09:57:26 -04:00
Joe Testa 3bd2dd95a9 Updated built-in policy for Debian 12 to match latest hardening guide. (#341) 2026-06-19 21:33:53 -04:00
Joe Testa b7749a7e17 Added built-in policies for OpenSSH 10.1, 10.2, and 10.3. 2026-06-19 20:50:58 -04:00
Joe Testa 974dd324a6 Added Rocky Linux 10 hardening guides and policies. (#354) 2026-06-17 20:20:19 -04:00
Joe Testa e42d993b07 Added hardening guides and policies for Ubuntu 26.04. 2026-06-14 19:55:06 -04:00
Joe Testa 0ccb915f37 Added support for scanning servers over UNIX sockets. (#351) 2026-06-14 17:21:13 -04:00
Joe Testa abf5b8326a Fixed image pull command when using podman. 2026-06-14 17:05:43 -04:00
Joe Testa e01bd61df0 Added usedforsecurity=False to hashlib.md5() call to suppress security scanner warning. 2026-06-13 10:54:54 -04:00
Joe Testa f2ed8c01ae Added new host key: webauthn-sk-ecdsa-sha2-nistp256-cert-v01@openssh.com (#348) 2026-06-13 10:27:07 -04:00
Joe Testa bbec6dbf23 Bumped copyright year. 2026-06-13 10:23:36 -04:00
Joe Testa 2573235f6a Added support for Python 3.14. Dropped support for Python 3.9. 2026-06-13 09:08:15 -04:00
Joe Testa 4f9a630de4 Added Debian 13 policies and hardening guides. 2025-09-01 18:22:46 -04:00
Joe Testa f821565ff9 Renamed hardeningguides.py. 2025-09-01 17:39:07 -04:00
Joe Testa 062a1f3cb4 Updated changelog message for version 2 of Ubuntu Server 24 policy. 2025-09-01 16:43:59 -04:00
Joe Testa c900874406 Added policy option to allow host key subsets and/or reorderings. 2025-09-01 16:22:40 -04:00
Joe Testa 0382cf9b2d Aside from linking to online hardening guides, mention that built-in guides are also available. 2025-08-30 16:26:57 -04:00
Joe Testa d8d90a3a89 Dropped support for Python 3.8, as it reached its end-of-life in October 2024. 2025-08-24 15:50:58 -04:00
Joe Testa aaa7d24565 Updated GEX fallback detection for OpenSSH 10.0 and later, as version 9.9 was the last to include it. (#310) 2025-08-24 15:41:14 -04:00
Joe Testa d3b1551520 Added OpenSSH 10.0 policy. 2025-08-24 12:52:22 -04:00
Joe Testa 970d747dcb Smoothed out some rough edges from PR #307. 2025-08-17 16:34:32 -04:00
oam7575andGitHub 1c0d3d5df1 print config v2 Issue #191 (#307)
* print config v2

 - printconfig script
 - test_printconfig for tox testing
 - update globals for GUIDES_UPDATED date value
 - update ssh_audit for print_config argument and checks

* pr307 update 1

* pr307 update 2

* pr307 - attempt 2

* Update ssh_audit.py

Missed a TAB
2025-08-17 16:05:14 -04:00
37 changed files with 1416 additions and 153 deletions
+3 -3
View File
@@ -7,12 +7,12 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
+1 -1
View File
@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (C) 2017-2025 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2017-2026 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
+88 -36
View File
@@ -35,18 +35,24 @@
- historical information from OpenSSH, Dropbear SSH and libssh;
- policy scans to ensure adherence to a hardened/standard configuration;
- runs on Linux and Windows;
- supports Python 3.8 - 3.13;
- supports Python 3.10 - 3.14;
- no dependencies
## Usage
```
usage: ssh-audit.py [-h] [-4] [-6] [-b] [-c] [-d]
[-g <min1:pref1:max1[,min2:pref2:max2,...]> / <x-y[:step]>] [-j] [-l {info,warn,fail}] [-L]
[-M custom_policy.txt] [-m] [-n] [-P "Built-In Policy Name" / custom_policy.txt] [-p N]
[-T targets.txt] [-t N] [-v] [--conn-rate-test N[:max_rate]] [--dheat N[:kex[:e_len]]]
[--lookup alg1[,alg2,...]] [--skip-rate-test] [--threads N]
[-g <min1:pref1:max1[,min2:pref2:max2,...]> / <x-y[:step]>]
[-j] [-l {info,warn,fail}] [-L] [-M custom_policy.txt]
[-m] [-n] [-P "Built-In Policy Name" / custom_policy.txt]
[-p N] [-T targets.txt] [-t N] [-v]
[--conn-rate-test N[:max_rate]] [--dheat N[:kex[:e_len]]]
[--get-hardening-guide platform] [--list-hardening-guides]
[--lookup alg1[,alg2,...]] [--skip-rate-test]
[--socks5 host:port] [--threads N]
[host]
# ssh-audit.py v3.4.0-dev, https://github.com/jtesta/ssh-audit
positional arguments:
host target hostname or IPv4/IPv6 address
@@ -55,44 +61,76 @@ optional arguments:
-4, --ipv4 enable IPv4 (order of precedence)
-6, --ipv6 enable IPv6 (order of precedence)
-b, --batch batch output
-c, --client-audit starts a server on port 2222 to audit client software config (use -p to change port; use -t
to change timeout)
-c, --client-audit starts a server on port 2222 to audit client software
config (use -p to change port; use -t to change
timeout)
-d, --debug enable debugging output
-g <min1:pref1:max1[,min2:pref2:max2,...]> / <x-y[:step]>, --gex-test <min1:pref1:max1[,min2:pref2:max2,...]> / <x-y[:step]>
conducts a very customized Diffie-Hellman GEX modulus size test. Tests an array of minimum,
preferred, and maximum values, or a range of values with an optional incremental step amount
-j, --json enable JSON output (use -jj to enable indentation for better readability)
conducts a very customized Diffie-Hellman GEX modulus
size test. Tests an array of minimum, preferred, and
maximum values, or a range of values with an optional
incremental step amount
-j, --json enable JSON output (use -jj to enable indentation for
better readability)
-l {info,warn,fail}, --level {info,warn,fail}
minimum output level (default: info)
-L, --list-policies list all the official, built-in policies. Combine with -v to view policy change logs
-L, --list-policies list all the official, built-in policies. Combine with
-v to view policy change logs
-M custom_policy.txt, --make-policy custom_policy.txt
creates a policy based on the target server (i.e.: the target server has the ideal
configuration that other servers should adhere to), and stores it in the file path specified
-m, --manual print the man page (Docker, PyPI, Snap, and Windows builds only)
-n, --no-colors disable colors (automatic when the NO_COLOR environment variable is set)
creates a policy based on the target server (i.e.: the
target server has the ideal configuration that other
servers should adhere to), and stores it in the file
path specified
-m, --manual print the man page (Docker, PyPI, Snap, and Windows
builds only)
-n, --no-colors disable colors (automatic when the NO_COLOR
environment variable is set)
-P "Built-In Policy Name" / custom_policy.txt, --policy "Built-In Policy Name" / custom_policy.txt
run a policy test using the specified policy (use -L to see built-in policies, or specify
filesystem path to custom policy created by -M)
-p N, --port N the TCP port to connect to (or to listen on when -c is used)
run a policy test using the specified policy (use -L
to see built-in policies, or specify filesystem path
to custom policy created by -M)
-p N, --port N the TCP port to connect to (or to listen on when -c is
used)
-T targets.txt, --targets targets.txt
a file containing a list of target hosts (one per line, format HOST[:PORT]). Use -p/--port
to set the default port for all hosts. Use --threads to control concurrent scans
-t N, --timeout N timeout (in seconds) for connection and reading (default: 5)
a file containing a list of target hosts (one per
line, format 'HOST[:PORT]'; for UNIX socket servers,
use 'unix:///path/socket'). Use -p/--port to set the
default port for all hosts. Use --threads to control
concurrent scans
-t N, --timeout N timeout (in seconds) for connection and reading
(default: 5)
-v, --verbose enable verbose output
--conn-rate-test N[:max_rate]
perform a connection rate test (useful for collecting metrics related to susceptibility of
the DHEat vuln). Testing is conducted with N concurrent sockets with an optional maximum
rate of connections per second
perform a connection rate test (useful for collecting
metrics related to susceptibility of the DHEat vuln).
Testing is conducted with N concurrent sockets with an
optional maximum rate of connections per second
--dheat N[:kex[:e_len]]
continuously perform the DHEat DoS attack (CVE-2002-20001) against the target using N
concurrent sockets. Optionally, a specific key exchange algorithm can be specified instead
of allowing it to be automatically chosen. Additionally, a small length of the fake e value
sent to the server can be chosen for a more efficient attack (such as 4).
continuously perform the DHEat DoS attack
(CVE-2002-20001) against the target using N concurrent
sockets. Optionally, a specific key exchange algorithm
can be specified instead of allowing it to be
automatically chosen. Additionally, a small length of
the fake e value sent to the server can be chosen for
a more efficient attack (such as 4).
--get-hardening-guide platform
retrieves the hardening guide for the specified
platform name (use --list-hardening-guides to see list
of available guides).
--list-hardening-guides
list all official, built-in hardening guides for
common systems. Their full names can then be passed to
--get-hardening-guide. Add -v to this option to view
hardening guide change logs and prior versions.
--lookup alg1[,alg2,...]
looks up an algorithm(s) without connecting to a server.
--skip-rate-test skip the connection rate test during standard audits (used to safely infer whether the DHEat
attack is viable)
--threads N number of threads to use when scanning multiple targets (-T/--targets) (default: 32)
looks up an algorithm(s) without connecting to a
server.
--skip-rate-test skip the connection rate test during standard audits
(used to safely infer whether the DHEat attack is
viable)
--socks5 host:port connect via a SOCKS5 proxy (implies --skip-rate-test)
--threads N number of threads to use when scanning multiple
targets (-T/--targets) (default: 32)
```
* if both IPv4 and IPv6 are used, order of precedence can be set by using either `-46` or `-64`.
* batch flag `-b` will output sections without header and without empty lines (implies verbose flag).
@@ -106,6 +144,7 @@ ssh-audit 127.0.0.1
ssh-audit 127.0.0.1:222
ssh-audit ::1
ssh-audit [::1]:222
ssh-audit unix:///run/ssh-unix-local/socket
```
To run a standard audit against many servers (place targets into servers.txt, one on each line in the format of `HOST[:PORT]`):
@@ -114,13 +153,13 @@ To run a standard audit against many servers (place targets into servers.txt, on
ssh-audit -T servers.txt
```
To audit a client configuration (listens on port 2222 by default; connect using `ssh -p 2222 anything@localhost`):
To audit a client configuration (listens on port 2222/tcp by default; connect using `ssh -p 2222 anything@localhost`):
```
ssh-audit -c
```
To audit a client configuration, with a listener on port 4567:
To audit a client configuration, with a listener on port 4567/tcp:
```
ssh-audit -c -p 4567
```
@@ -183,7 +222,7 @@ Below is a screen shot of the client-auditing output when an unhardened OpenSSH
![client_screenshot](https://user-images.githubusercontent.com/2982011/68867998-b946c100-06c4-11ea-975f-1f47e4178a74.png)
## Hardening Guides
Guides to harden server & client configuration can be found here: [https://www.ssh-audit.com/hardening_guides.html](https://www.ssh-audit.com/hardening_guides.html)
Guides to harden server & client configuration are built into the tool (see `--list-hardening-guides` and `--get-hardening-guide` options). Additionally, they are also available online at: [https://www.ssh-audit.com/hardening_guides.html](https://www.ssh-audit.com/hardening_guides.html)
## Pre-Built Packages
Pre-built packages are available for Windows (see the [Releases](https://github.com/jtesta/ssh-audit/releases) page), PyPI, Snap, and Docker:
@@ -213,14 +252,27 @@ For convenience, a web front-end on top of the command-line tool is available at
## ChangeLog
### v3.4.0-dev
### v3.9.0 (2026-07-04)
- BIG THANKS to [realmiwi](https://github.com/realmiwi) for being the project's *very first sponsor!!*
- Added support for Python 3.14.
- Dropped support for Python 3.8, as it reached end-of-life in October 2024.
- Dropped support for Python 3.9, as it reached end-of-life in October 2025.
- Added warning to all key exchanges that do not include protections against quantum attacks due to the Harvest Now, Decrypt Later strategy (see https://en.wikipedia.org/wiki/Harvest_now,_decrypt_later).
- Removed SSHv1 support (rationale is documented in: https://github.com/jtesta/ssh-audit/issues/298).
- Added hardening guides (see `--list-hardening-guides` and `--get-hardening-guide`). Previously, they were only available at <https://ssh-audit.com/hardening_guides.html>, but now they are built-in for convenience; partial credit [oam7575](https://github.com/oam7575).
- Added `allow_hostkey_subset_and_reordering` policy option to allow targets to have a more stringent list of host keys and/or a different ordering of them.
- Migrated from deprecated `getopt` module to `argparse`; partial credit [oam7575](https://github.com/oam7575).
- When running against multiple hosts, now prints each target host regardless of output level.
- Batch mode (`-b`) no longer automatically enables verbose mode, due to sometimes confusing results; users can still explicitly enable verbose mode using the `-v` flag.
- Added UNIX server socket scanning (specify the target with `unix:///path/to/socket`).
- Added SOCKS5 proxy support (specify the proxy with `--socks5 host:port`); partial credit [Michał Majchrowicz](https://github.com/sectroyer).
- Updated built-in policy for Debian 12.
- Added built-in policies for OpenSSH 10.0, 10.1, 10.2, 10.3, and 10.4.
- Added hardening guides and policies for Debian 13.
- Added hardening guides and policies for Rocky Linux 10.
- Added hardening guides and policies for Ubuntu 26.04.
- Added 2 new key exchanges: `mlkem768nistp256-sha256`, `mlkem1024nistp384-sha384`.
- Added 11 new host keys: `webauthn-sk-ecdsa-sha2-nistp256-cert-v01@openssh.com`, `mldsa-44`, `ssh-mldsa44-ed25519@openssh.com`, `mldsa-65`, `mldsa-87`, `ssh-mldsa-44`, `ssh-mldsa-65`, `ssh-mldsa-87`, `ssh-mldsa44`, `ssh-mldsa65`, `ssh-mldsa87`.
- Added 2 new ciphers: `AEAD_CAMELLIA_128_GCM`, `AEAD_CAMELLIA_256_GCM`.
### v3.3.0 (2024-10-15)
+4 -1
View File
@@ -23,7 +23,7 @@
IMAGE_VERSION=3
# This is the name of our docker image.
IMAGE_NAME=positronsecurity/ssh-audit-test-framework
IMAGE_NAME=docker.io/positronsecurity/ssh-audit-test-framework
# Terminal colors.
@@ -793,6 +793,9 @@ run_custom_policy_test "config2" "test16" "${PROGRAM_RETVAL_FAILURE}"
# Passing test with larger key matching.
run_custom_policy_test "config2" "test17" "${PROGRAM_RETVAL_GOOD}"
# Passing test with host key subset matching.
run_custom_policy_test "config2" "test18" "${PROGRAM_RETVAL_GOOD}"
# Failing test for built-in OpenSSH 8.0p1 server policy (RSA host key size is 3072 instead of 4096).
run_builtin_policy_test "Hardened OpenSSH Server v8.0 (version 4)" "8.0p1" "test1" "-o HostKeyAlgorithms=rsa-sha2-512,rsa-sha2-256,ssh-ed25519 -o KexAlgorithms=curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256 -o Ciphers=chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr -o MACs=hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com" "${PROGRAM_RETVAL_FAILURE}"
+2 -5
View File
@@ -19,14 +19,11 @@ classifiers =
License :: OSI Approved :: MIT License
Operating System :: OS Independent
Programming Language :: Python :: 3
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Programming Language :: Python :: 3.12
Programming Language :: Python :: 3.13
Programming Language :: Python :: Implementation :: CPython
Programming Language :: Python :: Implementation :: PyPy
Programming Language :: Python :: 3.14
Topic :: Security
Topic :: Security :: Cryptography
@@ -34,7 +31,7 @@ classifiers =
packages = find:
package_dir =
= src
python_requires = >=3.8,<4
python_requires = >=3.10,<4
[options.packages.find]
where = src
-2
View File
@@ -117,8 +117,6 @@ class Algorithms:
sshv, alg_db = alg_pair.sshv, alg_pair.db
rec[sshv] = {}
for alg_type, alg_list in alg_pair.items():
if alg_type == 'aut':
continue
rec[sshv][alg_type] = {'add': {}, 'del': {}, 'chg': {}}
for n, alg_desc in alg_db[alg_type].items():
versions = alg_desc[0]
+8 -1
View File
@@ -1,7 +1,7 @@
"""
The MIT License (MIT)
Copyright (C) 2017-2025 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2017-2026 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -67,6 +67,7 @@ class AuditConf:
self.conn_rate_test_enabled = False
self.conn_rate_test_threads = 0
self.conn_rate_test_target_rate = 0
self.socks5_proxy: Optional[str] = None # SOCKS5 proxy in "host:port" format
def __setattr__(self, name: str, value: Union[str, int, float, bool, Sequence[int]]) -> None:
@@ -185,6 +186,12 @@ class AuditConf:
if not isinstance(value, int):
valid = False
elif name == "socks5_proxy":
valid = True
if isinstance(value, str):
host, port = Utils.parse_host_and_port(value, 0)
if len(host) == 0 or port <= 0 or port >= 65535:
raise ValueError("SOCKS5 host must be in host:port format.")
if valid:
object.__setattr__(self, name, value)
+30 -2
View File
@@ -1,7 +1,7 @@
"""
The MIT License (MIT)
Copyright (C) 2020-2024 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2020-2026 Joe Testa (jtesta@positronsecurity.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -38,12 +38,18 @@ BUILTIN_POLICIES: Dict[str, Dict[str, Union[Optional[str], Optional[List[str]],
'Hardened Debian 12 (version 2)': {'version': '2', 'changelog': 'Re-ordered host keys to prioritize ED25519 due to efficiency. Re-ordered cipher list to prioritize larger key sizes as a countermeasure to quantum attacks.', 'banner': None, 'compressions': None, 'host_keys': ['ssh-ed25519', 'rsa-sha2-512', 'rsa-sha2-256'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'kex-strict-s-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-gcm@openssh.com', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "sk-ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}, "sk-ssh-ed25519@openssh.com": {"hostkey_size": 256}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 3072}, 'server_policy': True},
'Hardened Debian 12 (version 3)': {'version': '3', 'changelog': 'Added sntrup761x25519-sha512 key exchange.', 'banner': None, 'compressions': None, 'host_keys': ['ssh-ed25519', 'rsa-sha2-512', 'rsa-sha2-256'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['sntrup761x25519-sha512', 'sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'kex-strict-s-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-gcm@openssh.com', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "sk-ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}, "sk-ssh-ed25519@openssh.com": {"hostkey_size": 256}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 3072}, 'server_policy': True},
# Rocky Linux 9
'Hardened Debian 13 (version 1)': {'version': '1', 'changelog': 'Initial revision. As compared to the Debian 12 guide, the following changes were made: 1.) all non-post-quantum key exchanges were removed, and mlkem768x25519-sha256 and sntrup761x25519-sha512 were added, 2.) editing the /etc/ssh/moduli file is no longer done, as we no longer use any group-exchange algorithms (none of them currently protect against quantum attacks), 3.) editing the sshd_config file is no longer done, in order to make system upgrades easier for users, 4.) network-level connection throttling is no longer done, as OpenSSH prevents the DHEat attack by default now.', 'banner': None, 'compressions': None, 'host_keys': ['ssh-ed25519', 'rsa-sha2-512', 'rsa-sha2-256'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['mlkem768x25519-sha256', 'sntrup761x25519-sha512', 'sntrup761x25519-sha512@openssh.com', 'ext-info-s', 'kex-strict-s-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-gcm@openssh.com', 'aes128-ctr'], 'macs': ['hmac-sha2-512-etm@openssh.com', 'hmac-sha2-256-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "sk-ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}, "sk-ssh-ed25519@openssh.com": {"hostkey_size": 256}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 3072}, "allow_hostkey_subset_and_reordering": True, 'server_policy': True},
# Rocky Linux
'Hardened Rocky Linux 9 (version 1)': {'version': '1', 'changelog': 'Initial version', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'kex-strict-s-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "sk-ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}, "sk-ssh-ed25519@openssh.com": {"hostkey_size": 256}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 3072}, 'server_policy': True},
'Hardened Rocky Linux 9 (version 2)': {'version': '2', 'changelog': 'Re-ordered host keys to prioritize ED25519 due to efficiency. Re-ordered cipher list to prioritize larger key sizes as a countermeasure to quantum attacks.', 'banner': None, 'compressions': None, 'host_keys': ['ssh-ed25519', 'rsa-sha2-512', 'rsa-sha2-256'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'kex-strict-s-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-gcm@openssh.com', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "sk-ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}, "sk-ssh-ed25519@openssh.com": {"hostkey_size": 256}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 3072}, 'server_policy': True},
'Hardened Rocky Linux 10 (version 1)': {'version': '1', 'changelog': 'Initial revision. In comparison to the Rocky Linux 9 hardening guide, the following changes were made: the key exchange list was set to include post-quantum algorithms only.', 'banner': None, 'compressions': None, 'host_keys': ['ssh-ed25519', 'rsa-sha2-512', 'rsa-sha2-256'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['mlkem768x25519-sha256', 'sntrup761x25519-sha512', 'sntrup761x25519-sha512@openssh.com', 'ext-info-s', 'kex-strict-s-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-gcm@openssh.com', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "sk-ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}, "sk-ssh-ed25519@openssh.com": {"hostkey_size": 256}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 3072}, 'server_policy': True},
# Ubuntu Server policies
@@ -57,8 +63,12 @@ BUILTIN_POLICIES: Dict[str, Dict[str, Union[Optional[str], Optional[List[str]],
'Hardened Ubuntu Server 22.04 LTS (version 6)': {'version': '6', 'changelog': 'Re-ordered host keys to prioritize ED25519 due to efficiency. Re-ordered cipher list to prioritize larger key sizes as a countermeasure to quantum attacks.', 'banner': None, 'compressions': None, 'host_keys': ['ssh-ed25519', 'rsa-sha2-512', 'rsa-sha2-256'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'kex-strict-s-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-gcm@openssh.com', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "sk-ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}, "sk-ssh-ed25519@openssh.com": {"hostkey_size": 256}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 3072}, 'server_policy': True},
'Hardened Ubuntu Server 24.04 LTS (version 2)': {'version': '2', 'changelog': 'Removed commands directly editing sshd_config to make system updates easier for users.', 'banner': None, 'compressions': None, 'host_keys': ['ssh-ed25519', 'rsa-sha2-512', 'rsa-sha2-256'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'diffie-hellman-group16-sha512', 'ext-info-s', 'kex-strict-s-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-gcm@openssh.com', 'aes128-ctr'], 'macs': ['hmac-sha2-512-etm@openssh.com', 'hmac-sha2-256-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "sk-ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}, "sk-ssh-ed25519@openssh.com": {"hostkey_size": 256}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 3072}, "allow_hostkey_subset_and_reordering": True, "server_policy": True},
'Hardened Ubuntu Server 24.04 LTS (version 1)': {'version': '1', 'changelog': 'Initial version.', 'banner': None, 'compressions': None, 'host_keys': ['ssh-ed25519', 'rsa-sha2-512', 'rsa-sha2-256'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'diffie-hellman-group16-sha512', 'ext-info-s', 'kex-strict-s-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-gcm@openssh.com', 'aes128-ctr'], 'macs': ['hmac-sha2-512-etm@openssh.com', 'hmac-sha2-256-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "sk-ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}, "sk-ssh-ed25519@openssh.com": {"hostkey_size": 256}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 3072}, 'server_policy': True},
'Hardened Ubuntu Server 26.04 LTS (version 1)': {'version': '1', 'changelog': 'Initial revision. As compared to the Ubuntu Server 24.04 guide, the following changes were made: 1.) all non-post-quantum key exchanges were removed, and mlkem768x25519-sha256 and sntrup761x25519-sha512 were added, 2.) editing the /etc/ssh/moduli file is no longer done, as we no longer use any group-exchange algorithms (none of them currently protect against quantum attacks), 3.) editing the sshd_config file is no longer done, in order to make system upgrades easier for users, 4.) network-level connection throttling is no longer done, as OpenSSH prevents the DHEat attack by default now.', 'banner': None, 'compressions': None, 'host_keys': ['ssh-ed25519', 'rsa-sha2-512', 'rsa-sha2-256'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['mlkem768x25519-sha256', 'sntrup761x25519-sha512', 'sntrup761x25519-sha512@openssh.com', 'ext-info-s', 'kex-strict-s-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-gcm@openssh.com', 'aes128-ctr'], 'macs': ['hmac-sha2-512-etm@openssh.com', 'hmac-sha2-256-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "sk-ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}, "sk-ssh-ed25519@openssh.com": {"hostkey_size": 256}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 3072}, "allow_hostkey_subset_and_reordering": True, 'server_policy': True},
# Generic OpenSSH Server policies
'Hardened OpenSSH Server v7.7 (version 4)': {'version': '4', 'changelog': 'No change log available.', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 3072}, 'server_policy': True},
@@ -107,6 +117,18 @@ BUILTIN_POLICIES: Dict[str, Dict[str, Union[Optional[str], Optional[List[str]],
'Hardened OpenSSH Server v9.9 (version 1)': {'version': '1', 'changelog': 'Initial version.', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['sntrup761x25519-sha512', 'sntrup761x25519-sha512@openssh.com', 'mlkem768x25519-sha256', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'ext-info-s', 'kex-strict-s-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "sk-ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}, "sk-ssh-ed25519@openssh.com": {"hostkey_size": 256}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {'diffie-hellman-group-exchange-sha256': 3072}, 'server_policy': True},
'Hardened OpenSSH Server v10.0 (version 1)': {'version': '1', 'changelog': 'Initial version.', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['mlkem768x25519-sha256', 'sntrup761x25519-sha512', 'sntrup761x25519-sha512@openssh.com', 'ext-info-s', 'kex-strict-s-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-gcm@openssh.com', 'aes128-ctr'], 'macs': ['hmac-sha2-512-etm@openssh.com', 'hmac-sha2-256-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "sk-ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}, "sk-ssh-ed25519@openssh.com": {"hostkey_size": 256}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {}, 'server_policy': True},
'Hardened OpenSSH Server v10.1 (version 1)': {'version': '1', 'changelog': 'Initial version.', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['mlkem768x25519-sha256', 'sntrup761x25519-sha512', 'sntrup761x25519-sha512@openssh.com', 'ext-info-s', 'kex-strict-s-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-gcm@openssh.com', 'aes128-ctr'], 'macs': ['hmac-sha2-512-etm@openssh.com', 'hmac-sha2-256-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "sk-ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}, "sk-ssh-ed25519@openssh.com": {"hostkey_size": 256}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {}, 'server_policy': True},
'Hardened OpenSSH Server v10.2 (version 1)': {'version': '1', 'changelog': 'Initial version.', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['mlkem768x25519-sha256', 'sntrup761x25519-sha512', 'sntrup761x25519-sha512@openssh.com', 'ext-info-s', 'kex-strict-s-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-gcm@openssh.com', 'aes128-ctr'], 'macs': ['hmac-sha2-512-etm@openssh.com', 'hmac-sha2-256-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "sk-ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}, "sk-ssh-ed25519@openssh.com": {"hostkey_size": 256}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {}, 'server_policy': True},
'Hardened OpenSSH Server v10.3 (version 1)': {'version': '1', 'changelog': 'Initial version.', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['mlkem768x25519-sha256', 'sntrup761x25519-sha512', 'sntrup761x25519-sha512@openssh.com', 'ext-info-s', 'kex-strict-s-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-gcm@openssh.com', 'aes128-ctr'], 'macs': ['hmac-sha2-512-etm@openssh.com', 'hmac-sha2-256-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "sk-ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}, "sk-ssh-ed25519@openssh.com": {"hostkey_size": 256}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {}, 'server_policy': True},
# Note that the "ssh-mldsa44-ed25519@openssh.com" host key was added in this release as an experimental non-default option. Once it is no longer considered experimental by the OpenSSH developers, it will become a requirement.
'Hardened OpenSSH Server v10.4 (version 1)': {'version': '1', 'changelog': 'Initial version.', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['mlkem768x25519-sha256', 'sntrup761x25519-sha512', 'sntrup761x25519-sha512@openssh.com', 'ext-info-s', 'kex-strict-s-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-gcm@openssh.com', 'aes128-ctr'], 'macs': ['hmac-sha2-512-etm@openssh.com', 'hmac-sha2-256-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "sk-ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}, "sk-ssh-ed25519@openssh.com": {"hostkey_size": 256}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {}, 'server_policy': True},
# Amazon Linux Policies
'Hardened Amazon Linux Client 2023 (version 1)': {'version': '1', 'changelog': 'Initial version.', 'banner': None, 'compressions': None, 'host_keys': ['sk-ssh-ed25519-cert-v01@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'sk-ssh-ed25519@openssh.com', 'ssh-ed25519', 'rsa-sha2-512', 'rsa-sha2-256'], 'optional_host_keys': None, 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'ext-info-c', 'kex-strict-c-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'dh_modulus_sizes': None, 'server_policy': False},
@@ -120,6 +142,8 @@ BUILTIN_POLICIES: Dict[str, Dict[str, Union[Optional[str], Optional[List[str]],
'Hardened Debian Client 12 (version 2)': {'version': '2', 'changelog': 'Re-ordered cipher list to prioritize larger key sizes as a countermeasure to quantum attacks.', 'banner': None, 'compressions': None, 'host_keys': ['sk-ssh-ed25519-cert-v01@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'sk-ssh-ed25519@openssh.com', 'ssh-ed25519', 'rsa-sha2-512', 'rsa-sha2-256'], 'optional_host_keys': None, 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'ext-info-c', 'kex-strict-c-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-gcm@openssh.com', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'dh_modulus_sizes': None, 'server_policy': False},
'Hardened Debian Client 13 (version 1)': {'version': '1', 'changelog': 'Initial revision. As compared to the Debian 12 guide, the following changes were made: all non-post-quantum key exchanges were removed, and mlkem768x25519-sha256 and sntrup761x25519-sha512 were added.', 'banner': None, 'compressions': None, 'host_keys': ['sk-ssh-ed25519-cert-v01@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'sk-ssh-ed25519@openssh.com', 'ssh-ed25519', 'rsa-sha2-512', 'rsa-sha2-256'], 'optional_host_keys': None, 'kex': ['mlkem768x25519-sha256', 'sntrup761x25519-sha512', 'sntrup761x25519-sha512@openssh.com', 'ext-info-c', 'kex-strict-c-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-gcm@openssh.com', 'aes128-ctr'], 'macs': ['hmac-sha2-512-etm@openssh.com', 'hmac-sha2-256-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'dh_modulus_sizes': None, 'server_policy': False},
# Rocky Linux Policies
@@ -127,6 +151,8 @@ BUILTIN_POLICIES: Dict[str, Dict[str, Union[Optional[str], Optional[List[str]],
'Hardened Rocky Linux Client 9 (version 2)': {'version': '2', 'changelog': 'Re-ordered cipher list to prioritize larger key sizes as a countermeasure to quantum attacks.', 'banner': None, 'compressions': None, 'host_keys': ['sk-ssh-ed25519-cert-v01@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'sk-ssh-ed25519@openssh.com', 'ssh-ed25519', 'rsa-sha2-512', 'rsa-sha2-256'], 'optional_host_keys': None, 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'ext-info-c', 'kex-strict-c-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-gcm@openssh.com', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'dh_modulus_sizes': None, 'server_policy': False},
'Hardened Rocky Linux Client 10 (version 1)': {'version': '1', 'changelog': 'Initial revision. In comparison to the Rocky Linux 9 hardening guide, the following changes were made: the key exchange list was set to include post-quantum algorithms only.', 'banner': None, 'compressions': None, 'host_keys': ['sk-ssh-ed25519-cert-v01@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'sk-ssh-ed25519@openssh.com', 'ssh-ed25519', 'rsa-sha2-512', 'rsa-sha2-256'], 'optional_host_keys': None, 'kex': ['mlkem768x25519-sha256', 'sntrup761x25519-sha512', 'sntrup761x25519-sha512@openssh.com', 'ext-info-c', 'kex-strict-c-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-gcm@openssh.com', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'dh_modulus_sizes': None, 'server_policy': False},
# Ubuntu Client policies
'Hardened Ubuntu Client 16.04 LTS (version 2)': {'version': '2', 'changelog': 'No change log available.', 'banner': None, 'compressions': None, 'host_keys': ['ssh-ed25519', 'ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256', 'rsa-sha2-512'], 'optional_host_keys': None, 'kex': ['curve25519-sha256@libssh.org', 'diffie-hellman-group-exchange-sha256', 'ext-info-c'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'dh_modulus_sizes': None, 'server_policy': False},
@@ -141,4 +167,6 @@ BUILTIN_POLICIES: Dict[str, Dict[str, Union[Optional[str], Optional[List[str]],
'Hardened Ubuntu Client 24.04 LTS (version 1)': {'version': '1', 'changelog': 'Initial version.', 'banner': None, 'compressions': None, 'host_keys': ['sk-ssh-ed25519-cert-v01@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'sk-ssh-ed25519@openssh.com', 'ssh-ed25519', 'rsa-sha2-512', 'rsa-sha2-256'], 'optional_host_keys': None, 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'diffie-hellman-group16-sha512', 'ext-info-c', 'kex-strict-c-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-gcm@openssh.com', 'aes128-ctr'], 'macs': ['hmac-sha2-512-etm@openssh.com', 'hmac-sha2-256-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'dh_modulus_sizes': None, 'server_policy': False},
'Hardened Ubuntu Client 26.04 LTS (version 1)': {'version': '1', 'changelog': 'Initial revision. As compared to the Ubuntu Client 24.04 guide, the following changes were made: all non-post-quantum key exchanges were removed, and mlkem768x25519-sha256 and sntrup761x25519-sha512 were added.', 'banner': None, 'compressions': None, 'host_keys': ['sk-ssh-ed25519-cert-v01@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'sk-ssh-ed25519@openssh.com', 'ssh-ed25519', 'rsa-sha2-512', 'rsa-sha2-256'], 'optional_host_keys': None, 'kex': ['mlkem768x25519-sha256', 'sntrup761x25519-sha512', 'sntrup761x25519-sha512@openssh.com', 'ext-info-c', 'kex-strict-c-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-gcm@openssh.com', 'aes128-ctr'], 'macs': ['hmac-sha2-512-etm@openssh.com', 'hmac-sha2-256-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'dh_modulus_sizes': None, 'server_policy': False},
}
+77 -39
View File
@@ -1,7 +1,7 @@
"""
The MIT License (MIT)
Copyright (C) 2023-2024 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2023-2026 Joe Testa (jtesta@positronsecurity.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -194,7 +194,9 @@ class DHEat:
self.num_connect_timeouts = 0
self.num_read_timeouts = 0
self.num_socket_exceptions = 0
self.num_openssh_throttled_connections = 0
self.num_em = 0 # Number of "Exceeded MaxStartups" messages.
self.num_naatt = 0 # Number of "No allowed at this time" messages.
self.num_invalid_banners = 0
# The time we started the attack.
self.start_timer = 0.0
@@ -351,10 +353,10 @@ class DHEat:
rate_str = ""
if aconf.conn_rate_test_target_rate > 0:
rate_str = " at a max rate of %s%u%s connections per second" % (DHEat.WHITEB, aconf.conn_rate_test_target_rate, DHEat.CLEAR)
rate_str = f" at a max rate of {DHEat.WHITEB}{aconf.conn_rate_test_target_rate:d}{DHEat.CLEAR} connections per second"
print()
print("Performing non-disruptive rate test against %s[%s]:%u%s with %s%u%s concurrent sockets%s. No Diffie-Hellman requests will be sent." % (DHEat.WHITEB, target_ip_address, aconf.port, DHEat.CLEAR, DHEat.WHITEB, concurrent_sockets, DHEat.CLEAR, rate_str))
print(f"Performing non-disruptive rate test against {DHEat.WHITEB}[{target_ip_address}]:{aconf.port:d}{DHEat.CLEAR} with {DHEat.WHITEB}{concurrent_sockets:d}{DHEat.CLEAR} concurrent sockets{rate_str}. No Diffie-Hellman requests will be sent.\n")
print()
# Make room for the multi-line output.
@@ -369,14 +371,17 @@ class DHEat:
server_dh_kex.append(server_kex)
if len(server_dh_kex) == 0:
out.d("Skipping DHEat.dh_rate_test() since server does not support any DH algorithms: [%s]" % ", ".join(kex.kex_algorithms))
kex_list = ", ".join(kex.kex_algorithms)
out.d(f"Skipping DHEat.dh_rate_test() since server does not support any DH algorithms: [{kex_list}]")
return ""
else:
out.d("DHEat.dh_rate_test(): starting test; parameters: %f seconds, %u max connections, %u concurrent sockets." % (max_time, max_connections, concurrent_sockets), write_now=True)
out.d(f"DHEat.dh_rate_test(): starting test; parameters: {max_time:,.1f} seconds, {max_connections:,d} max connections, {concurrent_sockets:,d} concurrent sockets.", write_now=True)
num_attempted_connections = 0
num_opened_connections = 0
num_exceeded_maxstartups = 0
num_em = 0 # Number of "Exceeded MaxStartups" messages.
num_naatt = 0 # Number of "Not available at this time" messages.
num_invalid_banners = 0
socket_dict: Dict[socket.socket, float] = {}
start_timer = time.time()
now = start_timer
@@ -395,13 +400,15 @@ class DHEat:
if (now - last_update) >= 1.0:
seconds_running = now - start_timer
if multiline_output:
print("\033[5ARun time: %s%.1f%s seconds" % (DHEat.WHITEB, seconds_running, DHEat.CLEAR))
print("TCP SYNs: %s%u%s (total); %s%.1f%s (per second)" % (DHEat.WHITEB, num_attempted_connections, DHEat.CLEAR, DHEat.BLUEB, num_attempted_connections / seconds_running, DHEat.CLEAR))
print("Completed connections: %s%u%s (total); %s%.1f%s (per second)" % (DHEat.WHITEB, num_opened_connections, DHEat.CLEAR, DHEat.BLUEB, num_opened_connections / seconds_running, DHEat.CLEAR))
print("\"Exceeded MaxStartups\" responses: %s%u%s (total); %s%.1f%s (per second)" % (DHEat.WHITEB, num_exceeded_maxstartups, DHEat.CLEAR, DHEat.BLUEB, num_exceeded_maxstartups / seconds_running, DHEat.CLEAR))
print("%s%s%s" % (DHEat.WHITEB, spinner[spinner_index], DHEat.CLEAR))
print(f"\033[7A Run time: {DHEat.WHITEB}{seconds_running:,.1f}{DHEat.CLEAR} seconds")
print(f" TCP SYNs: {DHEat.WHITEB}{num_attempted_connections / seconds_running:,.1f}/sec{DHEat.CLEAR}; {DHEat.WHITEB}{num_attempted_connections:,d}{DHEat.CLEAR} total ")
print(f" Completed connections: {DHEat.WHITEB}{num_opened_connections / seconds_running:,.1f}/sec{DHEat.CLEAR}; {DHEat.WHITEB}{num_opened_connections:,d}{DHEat.CLEAR} total ")
print(f" \"Exceeded MaxStartups\" responses: {DHEat.WHITEB}{num_em / seconds_running:,.1f}/sec{DHEat.CLEAR}; {DHEat.WHITEB}{num_em:,d}{DHEat.CLEAR} total ")
print(f"\"Not allowed at this time\" responses: {DHEat.WHITEB}{num_naatt / seconds_running:,.1f}/sec{DHEat.CLEAR}; {DHEat.WHITEB}{num_naatt:,d}{DHEat.CLEAR} total ")
print(f" Invalid banners: {DHEat.WHITEB}{num_invalid_banners / seconds_running:,.1f}/sec{DHEat.CLEAR}; {DHEat.WHITEB}{num_invalid_banners:,d}{DHEat.CLEAR} total ")
print(DHEat.WHITEB + spinner[spinner_index] + DHEat.CLEAR)
else:
print("%s%s%s Run time: %s%.1f%s; TCP SYNs: %s%u%s; Compl. conns: %s%u%s; TCP SYNs/sec: %s%.1f%s; Compl. conns/sec: %s%.1f%s \r" % (DHEat.WHITEB, spinner[spinner_index], DHEat.CLEAR, DHEat.WHITEB, seconds_running, DHEat.CLEAR, DHEat.WHITEB, num_attempted_connections, DHEat.CLEAR, DHEat.WHITEB, num_opened_connections, DHEat.CLEAR, DHEat.BLUEB, num_attempted_connections / seconds_running, DHEat.CLEAR, DHEat.BLUEB, num_opened_connections / seconds_running, DHEat.CLEAR), end="")
print(f"{DHEat.WHITEB}{spinner[spinner_index]}{DHEat.CLEAR} Run time: {DHEat.WHITEB}{seconds_running:,.1f}{DHEat.CLEAR}; TCP SYNs: {DHEat.WHITEB}{num_attempted_connections:,d}{DHEat.CLEAR}; Compl. conns: {DHEat.WHITEB}{num_opened_connections:,d}{DHEat.CLEAR}; TCP SYNs/sec: {DHEat.BLUEB}{num_attempted_connections / seconds_running:,.1f}{DHEat.CLEAR}; Compl. conns/sec: {DHEat.BLUEB}{num_opened_connections / seconds_running:,.1f}{DHEat.CLEAR} \r", end="")
last_update = now
spinner_index = (spinner_index + 1) % 4
@@ -435,7 +442,11 @@ class DHEat:
s.setblocking(False)
# out.d("Creating socket (%u of %u already exist)..." % (len(socket_dict), concurrent_sockets), write_now=True)
ret = s.connect_ex((target_ip_address, aconf.port))
if target_address_family == socket.AF_UNIX:
ret = s.connect_ex(target_ip_address)
else:
ret = s.connect_ex((target_ip_address, aconf.port))
num_attempted_connections += 1
if ret in [0, errno.EINPROGRESS, errno.EWOULDBLOCK]:
socket_dict[s] = now
@@ -463,8 +474,14 @@ class DHEat:
num_opened_connections += 1
# out.d("Number of opened connections: %u (max: %u)." % (num_opened_connections, max_connections))
elif buf == b"Exceeded":
num_exceeded_maxstartups += 1
num_em += 1
# out.d("Number of \"Exceeded MaxStartups\": %u" % num_exceeded_maxstartups)
elif buf == b"Not allo":
num_naatt += 1
# out.d("Number of \"Not allowed at this time\": {num_naatt:,d}")
else:
num_invalid_banners += 1
# out.d("Number of invalid banners: {num_invalid_banners:,d}")
_close_socket(socket_dict, s)
@@ -716,35 +733,39 @@ class DHEat:
# Print extensive statistics on what just happened.
seconds_running = time.time() - self.start_timer
print("\n\n")
print(" %s %sSTATISTICS%s %s" % (self.BAR_CHART, self.WHITEB, self.CLEAR, self.CHART_UPWARDS))
print(" %s----------%s" % (self.WHITEB, self.CLEAR))
print(f" {self.BAR_CHART} {self.WHITEB}STATISTICS{self.CLEAR} {self.CHART_UPWARDS}")
print(f" {self.WHITEB}----------{self.CLEAR}")
print()
print(" Run time: %s%.1f seconds%s" % (self.WHITEB, seconds_running, self.CLEAR))
print(f" Run time: {self.WHITEB}{seconds_running:,.1f} seconds{self.CLEAR}")
print()
print(" Attempted TCP connections: %s%.1f/sec, %u total%s" % (self.WHITEB, self.num_attempted_tcp_connections / seconds_running, self.num_attempted_tcp_connections, self.CLEAR))
print(" Successful TCP connections: %s%.1f/sec, %u total%s" % (self.WHITEB, self.num_successful_tcp_connections / seconds_running, self.num_successful_tcp_connections, self.CLEAR))
print(f" Attempted TCP connections: {self.WHITEB}{self.num_attempted_tcp_connections / seconds_running:,.1f}/sec, {self.num_attempted_tcp_connections:,d} total{self.CLEAR}")
print(f" Successful TCP connections: {self.WHITEB}{self.num_successful_tcp_connections / seconds_running:,.1f}/sec, {self.num_successful_tcp_connections:,d} total{self.CLEAR}")
print()
print(" Bytes written: %s%s/sec, %s total%s" % (self.WHITEB, DHEat.add_byte_units(self.num_bytes_written / seconds_running), DHEat.add_byte_units(self.num_bytes_written), self.CLEAR))
print(f" Bytes written: {self.WHITEB}{DHEat.add_byte_units(self.num_bytes_written / seconds_running)}/sec, {DHEat.add_byte_units(self.num_bytes_written)} total{self.CLEAR}")
print()
print(" Successful DH KEX replies: %s%.1f/sec, %u total%s" % (self.WHITEB, self.num_successful_dh_kex / seconds_running, self.num_successful_dh_kex, self.CLEAR))
print(" Unexpected DH KEX replies: %s%.1f/sec, %u total%s" % (self.WHITEB, self.num_failed_dh_kex / seconds_running, self.num_failed_dh_kex, self.CLEAR))
print("\"Exceeded MaxStartups\" replies*: %s%.1f/sec, %u total%s" % (self.WHITEB, self.num_openssh_throttled_connections / seconds_running, self.num_openssh_throttled_connections, self.CLEAR))
print(f" Successful DH KEX replies: {self.WHITEB}{self.num_successful_dh_kex / seconds_running:,.1f}/sec, {self.num_successful_dh_kex:,d} total{self.CLEAR}")
print(f" Unexpected DH KEX replies: {self.WHITEB}{self.num_failed_dh_kex / seconds_running:,.1f}/sec, {self.num_failed_dh_kex:,d} total{self.CLEAR}")
print(f" \"Exceeded MaxStartups\" replies*: {self.WHITEB}{self.num_em / seconds_running:,.1f}/sec, {self.num_em:,d} total{self.CLEAR}")
print(f"\"Not allowed at this time\" replies+: {self.WHITEB}{self.num_naatt / seconds_running:,.1f}/sec, {self.num_naatt:,d} total{self.CLEAR}")
print(f" Invalid banners: {self.WHITEB}{self.num_invalid_banners / seconds_running:,.1f}/sec, {self.num_invalid_banners:,.1f} total{self.CLEAR}")
print()
print(" Connection timeouts: %s%.1f/sec, %u total%s (timeout setting: %.1f sec)" % (self.WHITEB, self.num_connect_timeouts / seconds_running, self.num_connect_timeouts, self.CLEAR, self.connect_timeout))
print(" Read timeouts: %s%.1f/sec, %u total%s (timeout setting: %.1f sec)" % (self.WHITEB, self.num_read_timeouts / seconds_running, self.num_read_timeouts, self.CLEAR, self.read_timeout))
print(" Socket exceptions: %s%.1f/sec, %u total%s" % (self.WHITEB, self.num_socket_exceptions / seconds_running, self.num_socket_exceptions, self.CLEAR))
print(f" Connection timeouts: {self.WHITEB}{self.num_connect_timeouts / seconds_running:,.1f}/sec, {self.num_connect_timeouts:,d} total{self.CLEAR} (timeout setting: {self.connect_timeout:,.1f} sec)")
print(f" Read timeouts: {self.WHITEB}{self.num_read_timeouts / seconds_running:,.1f}/sec, {self.num_read_timeouts:,d} total{self.CLEAR} (timeout setting: {self.read_timeout:,.1f} sec)")
print(f" Socket exceptions: {self.WHITEB}{self.num_socket_exceptions / seconds_running:,.1f}/sec, {self.num_socket_exceptions:,d} total{self.CLEAR}")
print()
if seconds_running < 5.0:
print("%sTotal run time was under 5 seconds; try running it for longer to get more accurate analysis.%s" % (DHEat.YELLOWB, DHEat.CLEAR))
print(f"{DHEat.YELLOWB}Total run time was under 5 seconds; try running it for longer to get more accurate analysis.{DHEat.CLEAR}")
elif self.num_successful_tcp_connections / seconds_running < DHEat.MAX_SAFE_RATE:
print("Because the number of successful TCP connections per second (%.1f) is less than %.1f, it appears that the target %sis using rate limiting%s to prevent CPU exaustion." % (self.num_successful_tcp_connections / seconds_running, DHEat.MAX_SAFE_RATE, DHEat.GREENB, DHEat.CLEAR))
print(f"Because the number of successful TCP connections per second ({self.num_successful_tcp_connections / seconds_running:,.1f}) is less than {DHEat.MAX_SAFE_RATE:,.1f}, it appears that the target {DHEat.GREENB}is using rate limiting{DHEat.CLEAR} to prevent CPU exaustion.")
else:
print("Because the number of successful TCP connections per second (%.1f) is greater than %.1f, it appears that the target %sis NOT using rate limiting%s to prevent CPU exaustion." % (self.num_successful_tcp_connections / seconds_running, DHEat.MAX_SAFE_RATE, DHEat.REDB, DHEat.CLEAR))
print(f"Because the number of successful TCP connections per second ({self.num_successful_tcp_connections / seconds_running:,.1f}) is greater than {DHEat.MAX_SAFE_RATE:,.1f}, it appears that the target {DHEat.REDB}is NOT using rate limiting{DHEat.CLEAR} to prevent CPU exaustion.")
print()
print()
print(" * OpenSSH has a throttling mechanism (controlled by the MaxStartups directive) to prevent too many pre-authentication connections from overwhelming the server. When triggered, the server will probabilistically return \"Exceeded MaxStartups\" instead of the usual SSH banner, then terminate the connection. In order to maximize the DoS effectiveness, this metric should be greater than zero, though the ideal rate of rejections depends on the target server's CPU resources.")
print(" [*] Prior to OpenSSH 9.8, the sole connection throttling mechanism is controlled by the MaxStartups directive. This prevents too many pre-authentication connections from overwhelming the server. When triggered, the server will probabilistically return \"Exceeded MaxStartups\" instead of the usual SSH banner, then terminate the connection. In order to maximize the DoS effectiveness, this metric should be greater than zero, though the ideal rate of rejections depends on the target server's CPU resources.")
print()
print(" [+] Starting in OpenSSH 9.8, a more fine-gained connection throttling mechanism was added via the PerSourcePenalties directive. When triggered, the server returns \"Not allowed at this time\" instead of the usual SSH banner. This countermeasure is more effective than simply relying on the MaxStartups directive, and seems to block this attack under its default settings.")
print()
@@ -752,6 +773,10 @@ class DHEat:
def _resolve_hostname(host: str, ip_version_preference: List[int]) -> Tuple[int, str]:
'''Resolves a hostname to its IPv4 or IPv6 address, depending on user preference.'''
# First check if this is a UNIX socket.
if host.startswith("unix://"):
return int(socket.AF_UNIX), host[7:]
family = socket.AF_UNSPEC
if len(ip_version_preference) == 1:
family = socket.AF_INET if ip_version_preference[0] == 4 else socket.AF_INET6
@@ -767,7 +792,6 @@ class DHEat:
def _run(self) -> bool:
'''Where all the magic happens.'''
self.output()
if sys.platform == "win32":
self.output("%sWARNING:%s this feature has not been thoroughly tested on Windows. It may perform worse than on UNIX OSes." % (self.YELLOWB, self.CLEAR))
@@ -879,14 +903,16 @@ class DHEat:
self.num_connect_timeouts += thread_statistics['num_connect_timeouts']
self.num_read_timeouts += thread_statistics['num_read_timeouts']
self.num_socket_exceptions += thread_statistics['num_socket_exceptions']
self.num_openssh_throttled_connections += thread_statistics['num_openssh_throttled_connections']
self.num_em += thread_statistics['num_em']
self.num_naatt += thread_statistics['num_naatt']
self.num_invalid_banners += thread_statistics['num_invalid_banners']
except queue.Empty: # If Queue.get() timeout exceeded.
pass
now = time.time()
if (now - last_update) >= 1.0:
seconds_running = now - self.start_timer
print("%s%s%s TCP SYNs/sec: %s%u%s; Compl. conns/sec: %s%u%s; Bytes sent/sec: %s%s%s; DH kex/sec: %s%u%s \r" % (self.WHITEB, spinner[spinner_index], self.CLEAR, self.BLUEB, self.num_attempted_tcp_connections / seconds_running, self.CLEAR, self.BLUEB, self.num_successful_tcp_connections / seconds_running, self.CLEAR, self.BLUEB, DHEat.add_byte_units(self.num_bytes_written / seconds_running), self.CLEAR, self.PURPLEB, self.num_successful_dh_kex / seconds_running, self.CLEAR), end="")
print(f"{self.WHITEB}{spinner[spinner_index]}{self.CLEAR} TCP SYNs/sec: {self.BLUEB}{int(self.num_attempted_tcp_connections / seconds_running):,d}{self.CLEAR}; Compl. conns/sec: {self.BLUEB}{int(self.num_successful_tcp_connections / seconds_running):,d}{self.CLEAR}; Bytes sent/sec: {self.BLUEB}{DHEat.add_byte_units(self.num_bytes_written / seconds_running)}{self.CLEAR}; DH kex/sec: {self.PURPLEB}{self.num_successful_dh_kex / seconds_running:,.1f}{self.CLEAR} \r", end="")
last_update = now
spinner_index = (spinner_index + 1) % 4
@@ -934,13 +960,16 @@ class DHEat:
num_connect_timeouts = 0
num_read_timeouts = 0
num_socket_exceptions = 0
num_openssh_throttled_connections = 0
num_em = 0 # Number of "Exceeded MaxStartups" messages received.
num_naatt = 0 # Number of "Not allowed at this time" messages received.
num_invalid_banners = 0
num_loops_since_last_statistics_sync = 0
while True:
num_loops_since_last_statistics_sync += 1
# Instead of flooding the parent process with statistics, report our stats only every 5 connections.
# TODO: this sends too quickly in the case of application-level rate limiting, resulting in client-side CPU exhaustion instead! :O Perhaps a dynamic number of loops should be skipped, based on what would result in 1 message per 2-3 seconds.
if num_loops_since_last_statistics_sync > 5:
num_loops_since_last_statistics_sync = 0
@@ -953,7 +982,9 @@ class DHEat:
'num_connect_timeouts': num_connect_timeouts,
'num_read_timeouts': num_read_timeouts,
'num_socket_exceptions': num_socket_exceptions,
'num_openssh_throttled_connections': num_openssh_throttled_connections,
'num_em': num_em,
'num_naatt': num_naatt,
'num_invalid_banners': num_invalid_banners
})
# Since we sent our statistics, reset them all back to zero.
@@ -965,7 +996,9 @@ class DHEat:
num_connect_timeouts = 0
num_read_timeouts = 0
num_socket_exceptions = 0
num_openssh_throttled_connections = 0
num_em = 0
num_naatt = 0
num_invalid_banners = 0
s = socket.socket(target_address_family, socket.SOCK_STREAM)
s.settimeout(connect_timeout)
@@ -1008,11 +1041,16 @@ class DHEat:
if banner.startswith(b"SSH-2.0-") or banner.startswith(b"SSH-1"):
num_successful_tcp_connections += 1
elif banner == b'Exceeded MaxStartups':
num_openssh_throttled_connections += 1
num_em += 1
_close_socket(s)
continue
elif banner == b'Not allowed at this time':
num_naatt += 1
_close_socket(s)
continue
else:
self.debug("Invalid banner received: %r" % banner)
num_invalid_banners += 1
# self.debug("Invalid banner received: %r" % banner)
_close_socket(s)
continue
+2 -2
View File
@@ -1,7 +1,7 @@
"""
The MIT License (MIT)
Copyright (C) 2017-2021 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2017-2026 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -32,7 +32,7 @@ class Fingerprint:
@property
def md5(self) -> str:
h = hashlib.md5(self.__fpd).hexdigest()
h = hashlib.md5(self.__fpd, usedforsecurity=False).hexdigest()
r = ':'.join(h[i:i + 2] for i in range(0, len(h), 2))
return 'MD5:{}'.format(r)
+16 -2
View File
@@ -30,6 +30,7 @@ from typing import Callable, Optional, Union, Any # noqa: F401
from ssh_audit.banner import Banner
from ssh_audit.kexdh import KexDHException, KexGroupExchange, KexGroupExchange_SHA1, KexGroupExchange_SHA256
from ssh_audit.software import Software
from ssh_audit.ssh2_kexdb import SSH2_KexDB
from ssh_audit.ssh2_kex import SSH2_Kex
from ssh_audit.ssh_socket import SSH_Socket
@@ -158,9 +159,22 @@ class GEXTest:
smallest_modulus, reconnect_failed = GEXTest._send_init(out, s, kex_group, kex, gex_alg, bits, bits, bits)
# If the smallest modulus is 2048 and the server is OpenSSH, then we may have triggered the fallback mechanism, which tends to happen in testing scenarios such as this but not in most real-world conditions (see X). To better test this condition, we will do an additional check to see if the server supports sizes between 2048 and 4096, and consider this the definitive result.
# If the banner exists, then parse it into a Software object. Next, if the target host is OpenSSH, check if its version is 9.9 or less; these versions are known to have a GEX fallback mechanism.
software = None if banner is None else Software.parse(banner)
is_openssh = False
has_fallback_mechanism = False
if (software is not None) and (software.product == "OpenSSH"):
is_openssh = True
try:
if float(software.version) <= 9.9:
has_fallback_mechanism = True
out.d(f"Found version of OpenSSH that includes GEX fallback mechanism: {software}")
except ValueError:
pass
# If the smallest modulus is 2048 and the server is OpenSSH v9.9 or less, then we may have triggered the fallback mechanism, which tends to happen in testing scenarios such as this but not in most real-world conditions. To better test this condition, we will do an additional check to see if the server supports sizes between 2048 and 4096, and consider this the definitive result.
openssh_test_updated = False
if (smallest_modulus == 2048) and (banner is not None) and (banner.software is not None) and (banner.software.find('OpenSSH') != -1):
if (smallest_modulus == 2048) and is_openssh and has_fallback_mechanism:
out.d('First pass found a minimum GEX modulus of 2048 against OpenSSH server. Performing a second pass to get a more accurate result...')
smallest_modulus, _ = GEXTest._send_init(out, s, kex_group, kex, gex_alg, 2048, 3072, 4096)
out.d('Modulus size returned by server during second pass: %d bits' % smallest_modulus, write_now=True)
+3 -3
View File
@@ -1,7 +1,7 @@
"""
The MIT License (MIT)
Copyright (C) 2017-2024 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2017-2026 Joe Testa (jtesta@positronsecurity.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -22,10 +22,10 @@
THE SOFTWARE.
"""
# The version to display.
VERSION = 'v3.4.0-dev'
VERSION = 'v3.9.0'
# SSH software to impersonate
SSH_HEADER = 'SSH-{0}-OpenSSH_8.2'
SSH_HEADER = 'SSH-{0}-OpenSSH_10.3'
# The URL to the Github issues tracker.
GITHUB_ISSUES_URL = 'https://github.com/jtesta/ssh-audit/issues'
+774
View File
@@ -0,0 +1,774 @@
"""
The MIT License (MIT)
Copyright (C) 2025-2026 Joe Testa (jtesta@positronsecurity.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
from typing import Any, Dict
from ssh_audit.outputbuffer import OutputBuffer
class Hardening_Guides:
HARDENING_GUIDES: Dict[str, Any] = {
"Amazon Linux 2023": [
{
"server_guide": True,
"version": 3,
"version_date": "2024-10-01",
"change_log": "Re-ordered host keys to prioritize ED25519 due to efficiency. Re-ordered cipher list to prioritize larger key sizes as a countermeasure to quantum attacks.",
"notes": "all commands below are to be executed as the root user.",
"commands": [
{
"heading": "Re-generate the RSA and ED25519 keys",
"comment": "",
"command": "rm -f /etc/ssh/ssh_host_*\nssh-keygen -t rsa -b 4096 -f /etc/ssh/ssh_host_rsa_key -N \"\"\nssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N \"\""
},
{
"heading": "Enable the ED25519 and RSA keys",
"comment": "Enable the ED25519 and RSA HostKey directives in the /etc/ssh/sshd_config file:",
"command": "echo -e \"\\nHostKey /etc/ssh/ssh_host_ed25519_key\\nHostKey /etc/ssh/ssh_host_rsa_key\" >> /etc/ssh/sshd_config"
},
{
"heading": "Remove small Diffie-Hellman moduli",
"comment": "",
"command": "awk '$5 >= 3071' /etc/ssh/moduli > /etc/ssh/moduli.safe\nmv -f /etc/ssh/moduli.safe /etc/ssh/moduli"
},
{
"heading": "Restrict supported key exchange, cipher, and MAC algorithms",
"comment": "",
"command": "echo -e \"# Restrict key exchange, cipher, and MAC algorithms, as per sshaudit.com hardening guide.\\nKexAlgorithms sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org,gss-curve25519-sha256-,diffie-hellman-group16-sha512,gss-group16-sha512-,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256\\n\\nCiphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-gcm@openssh.com,aes128-ctr\\n\\nMACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com\\n\\nHostKeyAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\nCASignatureAlgorithms sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\nGSSAPIKexAlgorithms gss-curve25519-sha256-,gss-group16-sha512-\\n\\nHostbasedAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256\\n\\nPubkeyAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\n\" > /etc/crypto-policies/back-ends/opensshserver.config"
},
{
"heading": "Restart OpenSSH server",
"comment": "",
"command": "systemctl restart sshd"
},
{
"heading": "Implement connection rate throttling",
"comment": "Connection rate throttling is needed in order to protect against the DHEat denial-of-service attack. A complete and flexible solution is to use iptables to allow up to 10 connections every 10 seconds from any one source address. An alternate solution is to set OpenSSH's PerSourceMaxStartups directive to 1 (note, however, that this can cause incomplete results during ssh-audit scans, as well as other client failures when bursts of connections are made).",
"command": "dnf install -y iptables\niptables -I INPUT -p tcp --dport 22 -m state --state NEW -m recent --set\niptables -I INPUT -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 10 --hitcount 10 -j DROP\nip6tables -I INPUT -p tcp --dport 22 -m state --state NEW -m recent --set\nip6tables -I INPUT -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 10 --hitcount 10 -j DROP\ndnf install -y iptables-services\niptables-save > /etc/sysconfig/iptables\nip6tables-save > /etc/sysconfig/ip6tables\nsystemctl enable iptables\nsystemctl enable ip6tables\nsystemctl start iptables\nsystemctl start ip6tables"
},
]
},
{
"server_guide": True,
"version": 2,
"version_date": "2024-04-22",
"change_log": "Added connection throttling instructions to counteract the DHEat denial-of-service attack.",
"notes": "",
"commands": [] # Commands for this older version are not tracked here.
},
{
"server_guide": True,
"version": 1,
"version_date": "2024-03-15",
"change_log": "Initial revision.",
"notes": "",
"commands": [] # Commands for this older version are not tracked here.
},
{
"server_guide": False,
"version": 3,
"version_date": "2024-10-01",
"change_log": "Re-ordered cipher list to prioritize larger key sizes as a countermeasure to quantum attacks.",
"notes": "",
"commands": [
{
"heading": "Run the following in a terminal to harden the SSH client for the local user:",
"comment": "",
"command": "mkdir -p -m 0700 ~/.ssh; echo -e \"\\nHost *\\n Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-gcm@openssh.com,aes128-ctr\\n\\n KexAlgorithms sntrup761x25519-sha512@openssh.com,gss-curve25519-sha256-,curve25519-sha256,curve25519-sha256@libssh.org,gss-group16-sha512-,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256\\n\\n MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com\\n\\n HostKeyAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\n CASignatureAlgorithms sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\n GSSAPIKexAlgorithms gss-curve25519-sha256-,gss-group16-sha512-\\n\\n HostbasedAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256\\n\\n PubkeyAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256\\n\\n\" >> ~/.ssh/config"
},
]
},
{
"server_guide": False,
"version": 2,
"version_date": "2024-04-22",
"change_log": "added connection throttling instructions to counteract the DHEat denial-of-service attack.",
"notes": "",
"commands": [] # Commands for this older version are not tracked here.
},
{
"server_guide": False,
"version": 1,
"version_date": "2024-03-15",
"change_log": "Initial revision.",
"notes": "",
"commands": [] # Commands for this older version are not tracked here.
},
],
"Debian 11": [
{
"server_guide": True,
"version": 1,
"version_date": "2021-09-17",
"change_log": "Latest version.",
"notes": "all commands below are to be executed as the root user.",
"commands": [
{
"heading": "Re-generate the RSA and ED25519 keys",
"comment": "",
"command": "rm -f /etc/ssh/ssh_host_*\nssh-keygen -t rsa -b 4096 -f /etc/ssh/ssh_host_rsa_key -N \"\"\nssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N \"\""
},
{
"heading": "Enable the RSA and ED25519 keys",
"comment": "Enable the RSA and ED25519 HostKey directives in the /etc/ssh/sshd_config file:",
"command": "sed -i 's/^\\#HostKey \\/etc\\/ssh\\/ssh_host_\\(rsa\\|ed25519\\)_key$/HostKey \\/etc\\/ssh\\/ssh_host_\\1_key/g' /etc/ssh/sshd_config"
},
{
"heading": "Remove small Diffie-Hellman moduli",
"comment": "",
"command": "awk '$5 >= 3071' /etc/ssh/moduli > /etc/ssh/moduli.safe\nmv -f /etc/ssh/moduli.safe /etc/ssh/moduli"
},
{
"heading": "Restrict supported key exchange, cipher, and MAC algorithms",
"comment": "",
"command": "echo -e \"\\n# Restrict key exchange, cipher, and MAC algorithms, as per sshaudit.com\\n# hardening guide.\\nKexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256\\nCiphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr\\nMACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com\\nHostKeyAlgorithms ssh-ed25519,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,sk-ssh-ed25519-cert-v01@openssh.com,rsa-sha2-256,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com\" > /etc/ssh/sshd_config.d/ssh-audit_hardening.conf"
},
{
"heading": "Restart OpenSSH server",
"comment": "",
"command": "service ssh restart"
},
]
},
],
"Debian 12": [
{
"server_guide": True,
"version": 3,
"version_date": "2025-04-18",
"change_log": "Added sntrup761x25519-sha512 to KexAlgorithms.",
"notes": "all commands below are to be executed as the root user.",
"commands": [
{
"heading": "Re-generate the RSA and ED25519 keys",
"comment": "",
"command": "rm /etc/ssh/ssh_host_*\nssh-keygen -t rsa -b 4096 -f /etc/ssh/ssh_host_rsa_key -N \"\"\nssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N \"\""
},
{
"heading": "Enable the ED25519 and RSA keys",
"comment": "Enable the ED25519 and RSA HostKey directives in the /etc/ssh/sshd_config file:",
"command": "echo -e \"\\nHostKey /etc/ssh/ssh_host_ed25519_key\\nHostKey /etc/ssh/ssh_host_rsa_key\" >> /etc/ssh/sshd_config"
},
{
"heading": "Remove small Diffie-Hellman moduli",
"comment": "",
"command": "awk '$5 >= 3071' /etc/ssh/moduli > /etc/ssh/moduli.safe\nmv /etc/ssh/moduli.safe /etc/ssh/moduli"
},
{
"heading": "Restrict supported key exchange, cipher, and MAC algorithms",
"comment": "",
"command": "echo -e \"# Restrict key exchange, cipher, and MAC algorithms, as per sshaudit.com\\n# hardening guide.\\n KexAlgorithms sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org,gss-curve25519-sha256-,diffie-hellman-group16-sha512,gss-group16-sha512-,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256\\n\\nCiphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-gcm@openssh.com,aes128-ctr\\n\\nMACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com\\n\\nHostKeyAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\nRequiredRSASize 3072\\n\\nCASignatureAlgorithms sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\nGSSAPIKexAlgorithms gss-curve25519-sha256-,gss-group16-sha512-\\n\\nHostbasedAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256\\n\\nPubkeyAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256\\n\\n\" > /etc/ssh/sshd_config.d/ssh-audit_hardening.conf"
},
{
"heading": "Restart OpenSSH server",
"comment": "",
"command": "service ssh restart"
},
{
"heading": "Implement connection rate throttling",
"comment": "Connection rate throttling is needed in order to protect against the DHEat denial-of-service attack. A complete and flexible solution is to use iptables to allow up to 10 connections every 10 seconds from any one source address. An alternate solution is to set OpenSSH's PerSourceMaxStartups directive to 1 (note, however, that this can cause incomplete results during ssh-audit scans, as well as other client failures when bursts of connections are made).",
"command": "apt update\nDEBIAN_FRONTEND=noninteractive apt install -q -y iptables netfilter-persistent iptables-persistent\niptables -I INPUT -p tcp --dport 22 -m state --state NEW -m recent --set\niptables -I INPUT -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 10 --hitcount 10 -j DROP\nip6tables -I INPUT -p tcp --dport 22 -m state --state NEW -m recent --set\nip6tables -I INPUT -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 10 --hitcount 10 -j DROP\nservice netfilter-persistent save"
},
]
},
{
"server_guide": True,
"version": 2,
"version_date": "2024-10-01",
"change_log": "Re-ordered host keys to prioritize ED25519 due to efficiency. Re-ordered cipher list to prioritize larger key sizes as a countermeasure to quantum attacks.",
"notes": "",
"commands": [] # Commands for this older version are not tracked here.
},
{
"server_guide": True,
"version": 1,
"version_date": "2024-04-24",
"change_log": "Added connection throttling instructions to counteract the DHEat denial-of-service attack.",
"notes": "",
"commands": [] # Commands for this older version are not tracked here.
},
{
"server_guide": False,
"version": 2,
"version_date": "2024-10-01",
"change_log": "Added RequiredRSASize directive to enforce a minimum of 3072-bit user and host-based authentication keys. Re-ordered cipher list to prioritize larger key sizes as a countermeasure to quantum attacks.",
"notes": "",
"commands": [
{
"heading": "Run the following in a terminal to harden the SSH client for the local user:",
"comment": "",
"command": "mkdir -p -m 0700 ~/.ssh; echo -e \"\\nHost *\\n Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-gcm@openssh.com,aes128-ctr\\n\\n KexAlgorithms sntrup761x25519-sha512@openssh.com,gss-curve25519-sha256-,curve25519-sha256,curve25519-sha256@libssh.org,gss-group16-sha512-,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256\\n\\n MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com\\n\\n RequiredRSASize 3072\\n\\n HostKeyAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\n CASignatureAlgorithms sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\n GSSAPIKexAlgorithms gss-curve25519-sha256-,gss-group16-sha512-\\n\\n HostbasedAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256\\n\\n PubkeyAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256\\n\\n\" >> ~/.ssh/config"
},
]
},
{
"server_guide": False,
"version": 1,
"version_date": "2024-03-15",
"change_log": "Initial revision.",
"notes": "",
"commands": [] # Commands for this older version are not tracked here.
},
],
"Debian 13": [
{
"server_guide": True,
"version": 1,
"version_date": "2025-09-01",
"change_log": "Initial revision. As compared to the Debian 12 guide, the following changes were made: 1.) all non-post-quantum key exchanges were removed, and mlkem768x25519-sha256 and sntrup761x25519-sha512 were added, 2.) editing the /etc/ssh/moduli file is no longer done, as we no longer use any group-exchange algorithms (none of them currently protect against quantum attacks), 3.) editing the sshd_config file is no longer done, in order to make system upgrades easier for users, 4.) network-level connection throttling is no longer done, as OpenSSH prevents the DHEat attack by default now.",
"notes": "all commands below are to be executed as the root user.",
"commands": [
{
"heading": "Re-generate the ED25519 and RSA keys",
"comment": "",
"command": "rm /etc/ssh/ssh_host_*\nssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N \"\"\nssh-keygen -t rsa -b 4096 -f /etc/ssh/ssh_host_rsa_key -N \"\""
},
{
"heading": "Restrict supported key exchange, cipher, and MAC algorithms",
"comment": "",
"command": "echo -e \"# Restrict key exchange, cipher, and MAC algorithms, as per sshaudit.com\\n# hardening guide.\\nKexAlgorithms mlkem768x25519-sha256,sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com\\n\\nCiphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-gcm@openssh.com,aes128-ctr\\n\\nMACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com\\n\\nRequiredRSASize 3072\\n\\nHostKeyAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\nCASignatureAlgorithms sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\nGSSAPIKexAlgorithms gss-curve25519-sha256-,gss-group16-sha512-\\n\\nHostbasedAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\nPubkeyAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\" > /etc/ssh/sshd_config.d/debian13_ssh-audit_hardening.conf"
},
{
"heading": "Restart OpenSSH server",
"comment": "",
"command": "service ssh restart"
},
]
},
{
"server_guide": False,
"version": 1,
"version_date": "2025-09-01",
"change_log": "Initial revision. As compared to the Debian 12 guide, the following changes were made: all non-post-quantum key exchanges were removed.",
"notes": "",
"commands": [
{
"heading": "Run the following in a terminal to harden the SSH client for the local user:",
"comment": "",
"command": "mkdir -p -m 0700 ~/.ssh; echo -e \"\\nHost *\\n Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-gcm@openssh.com,aes128-ctr\\n\\n KexAlgorithms mlkem768x25519-sha256,sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com\\n\\n MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com\\n\\n RequiredRSASize 3072\\n\\n HostKeyAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\n CASignatureAlgorithms sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\n GSSAPIKexAlgorithms gss-curve25519-sha256-,gss-group16-sha512-\\n\\n HostbasedAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256\\n\\n PubkeyAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256\\n\\n\" >> ~/.ssh/config"
},
]
},
],
"Rocky Linux 9": [
{
"server_guide": True,
"version": 2,
"version_date": "2024-10-01",
"change_log": "Re-ordered host keys to prioritize ED25519 due to efficiency. Re-ordered cipher list to prioritize larger key sizes as a countermeasure to quantum attacks.",
"notes": "all commands below are to be executed as the root user.",
"commands": [
{
"heading": "Re-generate the RSA and ED25519 keys",
"comment": "",
"command": "rm -f /etc/ssh/ssh_host_*\nssh-keygen -t rsa -b 4096 -f /etc/ssh/ssh_host_rsa_key -N \"\"\nssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N \"\""
},
{
"heading": "Enable the ED25519 and RSA keys",
"comment": "Enable the ED25519 and RSA HostKey directives in the /etc/ssh/sshd_config file:",
"command": "echo -e \"\\nHostKey /etc/ssh/ssh_host_ed25519_key\\nHostKey /etc/ssh/ssh_host_rsa_key\" >> /etc/ssh/sshd_config"
},
{
"heading": "Remove small Diffie-Hellman moduli",
"comment": "",
"command": "awk '$5 >= 3071' /etc/ssh/moduli > /etc/ssh/moduli.safe\nmv -f /etc/ssh/moduli.safe /etc/ssh/moduli"
},
{
"heading": "Restrict supported key exchange, cipher, and MAC algorithms",
"comment": "",
"command": "echo -e \"# Restrict key exchange, cipher, and MAC algorithms, as per sshaudit.com\\n# hardening guide.\\nKexAlgorithms sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org,gss-curve25519-sha256-,diffie-hellman-group16-sha512,gss-group16-sha512-,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256\\n\\nCiphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-gcm@openssh.com,aes128-ctr\\n\\nMACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com\\n\\nHostKeyAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\nRequiredRSASize 3072\\n\\nCASignatureAlgorithms sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\nGSSAPIKexAlgorithms gss-curve25519-sha256-,gss-group16-sha512-\\n\\nHostbasedAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256\\n\\nPubkeyAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\n\" > /etc/crypto-policies/back-ends/opensshserver.config"
},
{
"heading": "Restart OpenSSH server",
"comment": "",
"command": "systemctl restart sshd"
},
{
"heading": "Implement connection rate throttling",
"comment": "Connection rate throttling is needed in order to protect against the DHEat denial-of-service attack. A complete and flexible solution is to use iptables/firewalld to allow up to 10 connections every 10 seconds from any one source address. An alternate solution is to set OpenSSH's PerSourceMaxStartups directive to 1 (note, however, that this can cause incomplete results during ssh-audit scans, as well as other client failures when bursts of connections are made).",
"command": "firewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 0 -p tcp --dport 22 -m state --state NEW -m recent --set\nfirewall-cmd --permanent --direct --add-rule ipv4 filter INPUT 1 -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 10 --hitcount 10 -j DROP\nfirewall-cmd --permanent --direct --add-rule ipv6 filter INPUT 0 -p tcp --dport 22 -m state --state NEW -m recent --set\nfirewall-cmd --permanent --direct --add-rule ipv6 filter INPUT 1 -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 10 --hitcount 10 -j DROP\nsystemctl reload firewalld"
},
]
},
{
"server_guide": True,
"version": 1,
"version_date": "2024-04-24",
"change_log": "Added connection throttling instructions to counteract the DHEat denial-of-service attack.",
"notes": "",
"commands": [] # Commands for this older version are not tracked here.
},
{
"server_guide": False,
"version": 2,
"version_date": "2024-10-01",
"change_log": "Added RequiredRSASize directive to enforce a minimum of 3072-bit user and host-based authentication keys. Re-ordered cipher list to prioritize larger key sizes as a countermeasure to quantum attacks.",
"notes": "",
"commands": [
{
"heading": "Run the following in a terminal to harden the SSH client for the local user:",
"comment": "",
"command": "mkdir -p -m 0700 ~/.ssh; echo -e \"\\nHost *\\n Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-gcm@openssh.com,aes128-ctr\\n\\n KexAlgorithms sntrup761x25519-sha512@openssh.com,gss-curve25519-sha256-,curve25519-sha256,curve25519-sha256@libssh.org,gss-group16-sha512-,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256\\n\\n MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com\\n\\n RequiredRSASize 3072\\n\\n HostKeyAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\n CASignatureAlgorithms sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\n GSSAPIKexAlgorithms gss-curve25519-sha256-,gss-group16-sha512-\\n\\n HostbasedAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256\\n\\n PubkeyAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256\\n\\n\" >> ~/.ssh/config"
},
]
},
{
"server_guide": False,
"version": 1,
"version_date": "2024-03-15",
"change_log": "Initial revision.",
"notes": "",
"commands": [] # Commands for this older version are not tracked here.
},
],
"Rocky Linux 10": [
{
"server_guide": True,
"version": 1,
"version_date": "2026-06-17",
"change_log": "Initial revision. In comparison to the Rocky Linux 9 hardening guide, the following changes were made: the key exchange list was set to include post-quantum algorithms only.",
"notes": "all commands below are to be executed as the root user.",
"commands": [
{
"heading": "Disable RedHat Crypto Policies",
"comment": "",
"command": "rm -f /etc/ssh/sshd_config.d/40-redhat-crypto-policies.conf"
},
{
"heading": "Re-generate the RSA and ED25519 keys",
"comment": "",
"command": "rm -f /etc/ssh/ssh_host_*\nssh-keygen -t rsa -b 4096 -f /etc/ssh/ssh_host_rsa_key -N \"\"\nssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N \"\""
},
{
"heading": "Restrict supported key exchange, cipher, and MAC algorithms",
"comment": "",
"command": "echo -e \"# Restrict key exchange, cipher, and MAC algorithms, as per sshaudit.com\\n# hardening guide.\\nHostKey /etc/ssh/ssh_host_ed25519_key\\nHostKey /etc/ssh/ssh_host_rsa_key\\n\\nKexAlgorithms mlkem768x25519-sha256,sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com\\n\\nCiphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-gcm@openssh.com,aes128-ctr\\n\\nMACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com\\n\\nHostKeyAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\nRequiredRSASize 3072\\n\\nCASignatureAlgorithms sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\nGSSAPIKexAlgorithms gss-curve25519-sha256-,gss-group16-sha512-\\n\\nHostbasedAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256\\n\\nPubkeyAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\n\" > /etc/ssh/sshd_config.d/rocky10_ssh-audit_hardening.conf"
},
{
"heading": "Restart OpenSSH server",
"comment": "",
"command": "systemctl restart sshd"
},
]
},
{
"server_guide": False,
"version": 1,
"version_date": "2026-06-17",
"change_log": "Initial revision. In comparison to the Rocky Linux 9 hardening guide, the following changes were made: the key exchange list was set to include post-quantum algorithms only.",
"notes": "",
"commands": [
{
"heading": "Run the following in a terminal to harden the SSH client for the local user:",
"comment": "",
"command": "mkdir -p -m 0700 ~/.ssh; echo -e \"\\nHost *\\n Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-gcm@openssh.com,aes128-ctr\\n\\n KexAlgorithms mlkem768x25519-sha256,sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com\\n\\n MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com\\n\\n RequiredRSASize 3072\\n\\n HostKeyAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\n CASignatureAlgorithms sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\n GSSAPIKexAlgorithms gss-curve25519-sha256-,gss-group16-sha512-\\n\\n HostbasedAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256\\n\\n PubkeyAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256\\n\\n\" >> ~/.ssh/config"
},
]
},
],
"Ubuntu 22.04": [
{
"server_guide": True,
"version": 2,
"version_date": "2024-10-01",
"change_log": "Re-ordered host keys to prioritize ED25519 due to efficiency. Re-ordered cipher list to prioritize larger key sizes as a countermeasure to quantum attacks.",
"notes": "all commands below are to be executed as the root user.",
"commands": [
{
"heading": "Re-generate the RSA and ED25519 keys",
"comment": "",
"command": "rm /etc/ssh/ssh_host_*\nssh-keygen -t rsa -b 4096 -f /etc/ssh/ssh_host_rsa_key -N \"\"\nssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N \"\""
},
{
"heading": "Enable the ED25519 and RSA keys",
"comment": "Enable the ED25519 and RSA HostKey directives in the /etc/ssh/sshd_config file:",
"command": "echo -e \"\\nHostKey /etc/ssh/ssh_host_ed25519_key\\nHostKey /etc/ssh/ssh_host_rsa_key\" >> /etc/ssh/sshd_config"
},
{
"heading": "Remove small Diffie-Hellman moduli",
"comment": "",
"command": "awk '$5 >= 3071' /etc/ssh/moduli > /etc/ssh/moduli.safe\nmv /etc/ssh/moduli.safe /etc/ssh/moduli"
},
{
"heading": "Restrict supported key exchange, cipher, and MAC algorithms",
"comment": "",
"command": "echo -e \"# Restrict key exchange, cipher, and MAC algorithms, as per sshaudit.com\\n# hardening guide.\\nKexAlgorithms sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org,gss-curve25519-sha256-,diffie-hellman-group16-sha512,gss-group16-sha512-,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256\\n\\nCiphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-gcm@openssh.com,aes128-ctr\\n\\nMACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com\\n\\nHostKeyAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\nCASignatureAlgorithms sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\nGSSAPIKexAlgorithms gss-curve25519-sha256-,gss-group16-sha512-\\n\\nHostbasedAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256\\n\\nPubkeyAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256\" > /etc/ssh/sshd_config.d/ssh-audit_hardening.conf"
},
{
"heading": "Implement connection rate throttling",
"comment": "Connection rate throttling is needed in order to protect against the DHEat denial-of-service attack. A complete and flexible solution is to use iptables to allow up to 10 connections every 10 seconds from any one source address. An alternate solution is to set OpenSSH's PerSourceMaxStartups directive to 1 (note, however, that this can cause incomplete results during ssh-audit scans, as well as other client failures when bursts of connections are made).",
"command": "iptables -I INPUT -p tcp --dport 22 -m state --state NEW -m recent --set\niptables -I INPUT -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 10 --hitcount 10 -j DROP\nip6tables -I INPUT -p tcp --dport 22 -m state --state NEW -m recent --set\nip6tables -I INPUT -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 10 --hitcount 10 -j DROP\nDEBIAN_FRONTEND=noninteractive apt install -q -y netfilter-persistent iptables-persistent\nservice netfilter-persistent save"
},
{
"heading": "Restart OpenSSH server",
"comment": "",
"command": "service ssh restart"
},
]
},
{
"server_guide": True,
"version": 1,
"version_date": "2024-04-22",
"change_log": "Added connection throttling instructions to counteract the DHEat denial-of-service attack.",
"notes": "",
"commands": [] # Commands for this older version are not tracked here.
},
{
"server_guide": False,
"version": 1,
"version_date": "2024-10-01",
"change_log": "Re-ordered cipher list to prioritize larger key sizes as a countermeasure to quantum attacks.",
"notes": "",
"commands": [
{
"heading": "Run the following in a terminal to harden the SSH client for the local user:",
"comment": "",
"command": "mkdir -p -m 0700 ~/.ssh; echo -e \"\\nHost *\\n Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-gcm@openssh.com,aes128-ctr\\n\\n KexAlgorithms sntrup761x25519-sha512@openssh.com,gss-curve25519-sha256-,curve25519-sha256,curve25519-sha256@libssh.org,gss-group16-sha512-,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256\\n\\n MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com\\n\\n HostKeyAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\n CASignatureAlgorithms sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\n GSSAPIKexAlgorithms gss-curve25519-sha256-,gss-group16-sha512-\\n\\n HostbasedAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256\\n\\n PubkeyAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256\\n\\n\" >> ~/.ssh/config"
},
]
},
],
"Ubuntu 24.04": [
{
"server_guide": True,
"version": 3,
"version_date": "2025-09-01",
"change_log": "Removed commands directly editing sshd_config to make system updates easier for users.",
"notes": "all commands below are to be executed as the root user.",
"commands": [
{
"heading": "Re-generate the ED25519 and RSA keys",
"comment": "",
"command": "rm /etc/ssh/ssh_host_*\nssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N \"\"\nssh-keygen -t rsa -b 4096 -f /etc/ssh/ssh_host_rsa_key -N \"\""
},
{
"heading": "Remove small Diffie-Hellman moduli",
"comment": "",
"command": "awk '$5 >= 3071' /etc/ssh/moduli > /etc/ssh/moduli.safe\nmv /etc/ssh/moduli.safe /etc/ssh/moduli"
},
{
"heading": "Restrict supported key exchange, cipher, and MAC algorithms",
"comment": "",
"command": "echo -e \"# Restrict key exchange, cipher, and MAC algorithms, as per sshaudit.com\\n# hardening guide.\\nKexAlgorithms sntrup761x25519-sha512@openssh.com,gss-curve25519-sha256-,curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256,gss-group16-sha512-,diffie-hellman-group16-sha512\\n\\nCiphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-gcm@openssh.com,aes128-ctr\\n\\nMACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com\\n\\nRequiredRSASize 3072\\n\\nHostKeyAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\nCASignatureAlgorithms sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\nGSSAPIKexAlgorithms gss-curve25519-sha256-,gss-group16-sha512-\\n\\nHostbasedAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\nPubkeyAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\" > /etc/ssh/sshd_config.d/ssh-audit_hardening.conf"
},
{
"heading": "Restart OpenSSH server",
"comment": "",
"command": "service ssh restart"
},
{
"heading": "Implement connection rate throttling",
"comment": "Connection rate throttling is needed in order to protect against the DHEat denial-of-service attack. A complete and flexible solution is to use iptables to allow up to 10 connections every 10 seconds from any one source address. An alternate solution is to set OpenSSH's PerSourceMaxStartups directive to 1 (note, however, that this can cause incomplete results during ssh-audit scans, as well as other client failures when bursts of connections are made).",
"command": "iptables -I INPUT -p tcp --dport 22 -m state --state NEW -m recent --set\niptables -I INPUT -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 10 --hitcount 10 -j DROP\nip6tables -I INPUT -p tcp --dport 22 -m state --state NEW -m recent --set\nip6tables -I INPUT -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 10 --hitcount 10 -j DROP\nDEBIAN_FRONTEND=noninteractive apt install -q -y netfilter-persistent iptables-persistent\nservice netfilter-persistent save"
},
]
},
{
"server_guide": True,
"version": 2,
"version_date": "2024-10-01",
"change_log": "Added RequiredRSASize directive to enforce a minimum of 3072-bit user and host-based authentication keys.",
"notes": "all commands below are to be executed as the root user.",
"commands": [
{
"heading": "Re-generate the ED25519 and RSA keys",
"comment": "",
"command": "rm /etc/ssh/ssh_host_*\nssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N \"\"\nssh-keygen -t rsa -b 4096 -f /etc/ssh/ssh_host_rsa_key -N \"\""
},
{
"heading": "Remove small Diffie-Hellman moduli",
"comment": "",
"command": "awk '$5 >= 3071' /etc/ssh/moduli > /etc/ssh/moduli.safe\nmv /etc/ssh/moduli.safe /etc/ssh/moduli"
},
{
"heading": "Enable the ED25519 and RSA keys",
"comment": "Enable the ED25519 and RSA HostKey directives in the /etc/ssh/sshd_config file:",
"command": "echo -e \"\\nHostKey /etc/ssh/ssh_host_ed25519_key\\nHostKey /etc/ssh/ssh_host_rsa_key\" >> /etc/ssh/sshd_config"
},
{
"heading": "Restrict supported key exchange, cipher, and MAC algorithms",
"comment": "",
"command": "echo -e \"# Restrict key exchange, cipher, and MAC algorithms, as per sshaudit.com\\n# hardening guide.\\nKexAlgorithms sntrup761x25519-sha512@openssh.com,gss-curve25519-sha256-,curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256,gss-group16-sha512-,diffie-hellman-group16-sha512\\n\\nCiphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-gcm@openssh.com,aes128-ctr\\n\\nMACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com\\n\\nRequiredRSASize 3072\\n\\nHostKeyAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\nCASignatureAlgorithms sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\nGSSAPIKexAlgorithms gss-curve25519-sha256-,gss-group16-sha512-\\n\\nHostbasedAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\nPubkeyAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\" > /etc/ssh/sshd_config.d/ssh-audit_hardening.conf"
},
{
"heading": "Restart OpenSSH server",
"comment": "",
"command": "service ssh restart"
},
{
"heading": "Implement connection rate throttling",
"comment": "Connection rate throttling is needed in order to protect against the DHEat denial-of-service attack. A complete and flexible solution is to use iptables to allow up to 10 connections every 10 seconds from any one source address. An alternate solution is to set OpenSSH's PerSourceMaxStartups directive to 1 (note, however, that this can cause incomplete results during ssh-audit scans, as well as other client failures when bursts of connections are made).",
"command": "iptables -I INPUT -p tcp --dport 22 -m state --state NEW -m recent --set\niptables -I INPUT -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 10 --hitcount 10 -j DROP\nip6tables -I INPUT -p tcp --dport 22 -m state --state NEW -m recent --set\nip6tables -I INPUT -p tcp --dport 22 -m state --state NEW -m recent --update --seconds 10 --hitcount 10 -j DROP\nDEBIAN_FRONTEND=noninteractive apt install -q -y netfilter-persistent iptables-persistent\nservice netfilter-persistent save"
},
]
},
{
"server_guide": True,
"version": 1,
"version_date": "2024-04-29",
"change_log": "Initial revision. In comparison to Ubuntu 22.04 LTS guide, the following changes were made: 1.) For key exchanges, diffie-hellman-group18-sha512 and diffie-hellman-group-exchange-sha256 were prioritized over diffie-hellman-group16-sha512 due to greater security strength; GSS algorithms were prioritized over their non-GSS equivalents in order to match the client guide, 2.) For ciphers, 256-bit AES ciphers were prioritized over 192 and 128-bit AES ciphers due to their increased resistence against quantum computing attacks (previously, weaker GCM ciphers had priority over CTR ciphers), 3.) The HostbasedAcceptedAlgorithms and PubkeyAcceptedAlgorithms settings are now the same as HostKeyAlgorithms setting, 4.) The hmac-sha2-512-etm@openssh.com MAC was increased in priority due to its increased resistence against quantum computing attacks, and 5.) The ED25519 host keys were given priority over RSA host keys due to their greater efficiency.",
"notes": "",
"commands": [] # Commands for this older version are not tracked here.
},
{
"server_guide": False,
"version": 2,
"version_date": "2024-10-01",
"change_log": "Added RequiredRSASize directive to enforce a minimum of 3072-bit user and host-based authentication keys.",
"notes": "",
"commands": [
{
"heading": "Run the following in a terminal to harden the SSH client for the local user:",
"comment": "",
"command": "mkdir -p -m 0700 ~/.ssh; echo -e \"\\nHost *\\n Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-gcm@openssh.com,aes128-ctr\\n\\n KexAlgorithms sntrup761x25519-sha512@openssh.com,gss-curve25519-sha256-,curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256,gss-group16-sha512-,diffie-hellman-group16-sha512\\n\\n MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com\\n\\n RequiredRSASize 3072\\n\\n HostKeyAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\n CASignatureAlgorithms sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\n GSSAPIKexAlgorithms gss-curve25519-sha256-,gss-group16-sha512-\\n\\n HostbasedAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\n PubkeyAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\n\" >> ~/.ssh/config"
},
]
},
{
"server_guide": False,
"version": 1,
"version_date": "2024-04-29",
"change_log": "Initial revision. In comparison to Ubuntu 22.04 LTS Client guide, the following changes were made: 1.) For key exchanges, diffie-hellman-group18-sha512 and diffie-hellman-group-exchange-sha256 were prioritized over diffie-hellman-group16-sha512 due to greater security strength, 2.) For ciphers, 256-bit AES ciphers were prioritized over 192 and 128-bit AES ciphers due to their increased resistence against quantum computing attacks (previously, weaker GCM ciphers had priority over CTR ciphers), 3.) The HostbasedAcceptedAlgorithms and PubkeyAcceptedAlgorithms settings are now the same as HostKeyAlgorithms setting, and 4.) The hmac-sha2-512-etm@openssh.com MAC was increased in priority due to its increased resistence against quantum computing attacks.",
"notes": "",
"commands": [] # Commands for this older version are not tracked here.
},
],
"Ubuntu 26.04": [
{
"server_guide": True,
"version": 1,
"version_date": "2026-06-14",
"change_log": "Initial revision.",
"notes": "all commands below are to be executed as the root user.",
"commands": [
{
"heading": "Re-generate the ED25519 and RSA keys",
"comment": "",
"command": "rm /etc/ssh/ssh_host_*\nssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N \"\"\nssh-keygen -t rsa -b 4096 -f /etc/ssh/ssh_host_rsa_key -N \"\""
},
{
"heading": "Restrict supported key exchange, cipher, and MAC algorithms",
"comment": "",
"command": "echo -e \"# Restrict key exchange, cipher, and MAC algorithms, as per sshaudit.com\\n# hardening guide.\\nKexAlgorithms mlkem768x25519-sha256,sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com\\n\\nCiphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-gcm@openssh.com,aes128-ctr\\n\\nMACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com\\n\\nRequiredRSASize 3072\\n\\nHostKeyAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\nCASignatureAlgorithms sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\nGSSAPIKexAlgorithms gss-curve25519-sha256-,gss-group16-sha512-\\n\\nHostbasedAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\nPubkeyAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\" > /etc/ssh/sshd_config.d/ssh-audit_hardening.conf"
},
{
"heading": "Restart OpenSSH server",
"comment": "",
"command": "service ssh restart"
},
]
},
{
"server_guide": False,
"version": 1,
"version_date": "2026-06-14",
"change_log": "Initial revision.",
"notes": "",
"commands": [
{
"heading": "Run the following in a terminal to harden the SSH client for the local user:",
"comment": "",
"command": "mkdir -p -m 0700 ~/.ssh; echo -e \"\\nHost *\\n Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-gcm@openssh.com,aes128-ctr\\n\\n KexAlgorithms mlkem768x25519-sha256,sntrup761x25519-sha512,sntrup761x25519-sha512@openssh.com\\n\\n MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com\\n\\n RequiredRSASize 3072\\n\\n HostKeyAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\n CASignatureAlgorithms sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\n GSSAPIKexAlgorithms gss-curve25519-sha256-,gss-group16-sha512-\\n\\n HostbasedAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256\\n\\n PubkeyAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256\\n\\n\" >> ~/.ssh/config"
},
]
},
],
"Linux Mint 21": [
{
"server_guide": False,
"version": 1,
"version_date": "2024-10-01",
"change_log": "Re-ordered cipher list to prioritize larger key sizes as a countermeasure to quantum attacks.",
"notes": "",
"commands": [
{
"heading": "Run the following in a terminal to harden the SSH client for the local user:",
"comment": "",
"command": "mkdir -p -m 0700 ~/.ssh; echo -e \"\\nHost *\\n Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-gcm@openssh.com,aes128-ctr\\n\\n KexAlgorithms sntrup761x25519-sha512@openssh.com,gss-curve25519-sha256-,curve25519-sha256,curve25519-sha256@libssh.org,gss-group16-sha512-,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256\\n\\n MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com\\n\\n HostKeyAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\n CASignatureAlgorithms sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\n GSSAPIKexAlgorithms gss-curve25519-sha256-,gss-group16-sha512-\\n\\n HostbasedAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256\\n\\n PubkeyAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-512,rsa-sha2-256-cert-v01@openssh.com,rsa-sha2-256\\n\\n\" >> ~/.ssh/config"
},
]
},
],
"Linux Mint 22": [
{
"server_guide": False,
"version": 2,
"version_date": "2024-10-01",
"change_log": "Added RequiredRSASize directive to enforce a minimum of 3072-bit user and host-based authentication keys.",
"notes": "",
"commands": [
{
"heading": "Run the following in a terminal to harden the SSH client for the local user:",
"comment": "",
"command": "mkdir -p -m 0700 ~/.ssh; echo -e \"\\nHost *\\n Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-gcm@openssh.com,aes128-ctr\\n\\n KexAlgorithms sntrup761x25519-sha512@openssh.com,gss-curve25519-sha256-,curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group18-sha512,diffie-hellman-group-exchange-sha256,gss-group16-sha512-,diffie-hellman-group16-sha512\\n\\n MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com\\n\\n RequiredRSASize 3072\\n\\n HostKeyAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\n CASignatureAlgorithms sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\n GSSAPIKexAlgorithms gss-curve25519-sha256-,gss-group16-sha512-\\n\\n HostbasedAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\n PubkeyAcceptedAlgorithms sk-ssh-ed25519-cert-v01@openssh.com,ssh-ed25519-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,sk-ssh-ed25519@openssh.com,ssh-ed25519,rsa-sha2-512,rsa-sha2-256\\n\\n\" >> ~/.ssh/config"
},
]
},
{
"server_guide": False,
"version": 1,
"version_date": "2024-04-29",
"change_log": "Initial revision. In comparison to Ubuntu 22.04 LTS Client guide, the following changes were made: 1.) For key exchanges, diffie-hellman-group18-sha512 and diffie-hellman-group-exchange-sha256 were prioritized over diffie-hellman-group16-sha512 due to greater security strength, 2.) For ciphers, 256-bit AES ciphers were prioritized over 192 and 128-bit AES ciphers due to their increased resistence against quantum computing attacks (previously, weaker GCM ciphers had priority over CTR ciphers), 3.) The HostbasedAcceptedAlgorithms and PubkeyAcceptedAlgorithms settings are now the same as HostKeyAlgorithms setting, and 4.) The hmac-sha2-512-etm@openssh.com MAC was increased in priority due to its increased resistence against quantum computing attacks.",
"notes": "",
"commands": [] # Commands for this older version are not tracked here.
},
],
}
@staticmethod
def list_guides(out: OutputBuffer, verbose: bool) -> None:
'''Print all the server and client hardening guides.'''
server_guide_names = []
client_guide_names = []
# Iterate through the guides, and record a list of server guide names, along with a separate list for client guide names.
for name, guides in Hardening_Guides.HARDENING_GUIDES.items():
for guide in guides:
version = guide["version"]
version_date = guide["version_date"]
change_log = guide["change_log"]
if guide["server_guide"]:
full_name = f"{name} Server" if not verbose else f"{name} Server (version {version}): {version_date}: {change_log}"
if full_name not in server_guide_names:
server_guide_names.append(full_name)
else:
full_name = f"{name} Client" if not verbose else f"{name} Client (version {version}): {version_date}: {change_log}"
if full_name not in client_guide_names:
client_guide_names.append(full_name)
# Sort the names.
server_guide_names.sort()
client_guide_names.sort()
# Print the lists.
out.head("\nServer hardening guides:\n")
out.info(" * %s" % "\n * ".join(server_guide_names))
out.head("\nClient hardening guides:\n")
out.info(" * %s" % "\n * ".join(client_guide_names))
out.info("\n")
if not verbose:
out.info("Hint: add -v to --list-hardening-guides in order to see change log messages and prior versions. Prior versions of hardening guides can be retrieved as well with --get-hardening-guide (i.e.: --get-hardening-guide \"Ubuntu 24.04 Server (version 1)\").\n")
out.write()
@staticmethod
def print_hardening_guide(out: OutputBuffer, platform: str) -> None:
'''Prints a hardening guide for the specified platform.'''
platform_orig = platform
invalid_guide_name_error = "Invalid guide name. Run --list-hardening-guides to see list of valid guide names."
# If the user provided a version with the platform name, parse the version number they're interested in.
use_latest_version = True
use_version = 0
pos = platform.find(" (version ")
if pos != -1:
use_latest_version = False
end_pos = platform.find(")", pos)
try:
use_version = int(platform[pos + 10:end_pos])
except ValueError:
out.fail(invalid_guide_name_error, write_now=True)
return
platform = platform[0:pos]
last_space_pos = platform.rfind(" ")
if last_space_pos == -1:
out.fail(invalid_guide_name_error, write_now=True)
return
# From input such as "Ubuntu 24.04 Server", parse the OS name ("Ubuntu 24.04") and last word ("Server").
os_name = platform[0:last_space_pos]
last_word = platform[last_space_pos + 1:]
# Determine if this is a server or client guide.
is_server = False
if last_word == "Server":
is_server = True
elif last_word != "Client":
out.fail(invalid_guide_name_error, write_now=True)
return
# Ensure that this OS exists in the database.
if os_name not in Hardening_Guides.HARDENING_GUIDES:
out.fail(invalid_guide_name_error, write_now=True)
return
# Pull all guides for this OS name.
guides = Hardening_Guides.HARDENING_GUIDES[os_name]
# Iterate over guides until we find the type (server/client) we need, as well as the version of the guide we need.
selected_guide = None
latest_version = 0
for guide in guides:
if guide["server_guide"] == is_server:
version = guide["version"]
if use_latest_version and (version > latest_version):
selected_guide = guide
latest_version = version
elif use_latest_version is False and use_version == version:
selected_guide = guide
# Ensure we found a guide from above.
if selected_guide is None:
out.fail(invalid_guide_name_error, write_now=True)
return
# Now print the guide.
version_header = f"\n#\n# Hardening guide for {platform_orig}\n#\n" if not use_latest_version else f"\n#\n# Hardening guide for {platform_orig} (version {latest_version})\n#\n"
out.info(version_header)
commands = selected_guide["commands"]
for command_dict in commands:
heading = command_dict["heading"]
comment = command_dict["comment"]
command = command_dict["command"]
if heading != "":
out.info(f"# {heading}")
if comment != "":
out.info(f"# {comment}")
out.info(f"{command}")
out.info(s="")
out.write()
+22 -7
View File
@@ -1,7 +1,7 @@
"""
The MIT License (MIT)
Copyright (C) 2020-2024 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2020-2025 Joe Testa (jtesta@positronsecurity.com)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -55,6 +55,7 @@ class Policy:
self._dh_modulus_sizes: Optional[Dict[str, int]] = None
self._server_policy = True
self._allow_algorithm_subset_and_reordering = False
self._allow_hostkey_subset_and_reordering = False
self._allow_larger_keys = False
self._errors: List[Any] = []
self._updated_builtin_policy_available = False # If True, an outdated built-in policy was loaded.
@@ -116,7 +117,7 @@ class Policy:
key = key.strip()
val = val.strip()
if key not in ['name', 'version', 'banner', 'compressions', 'host keys', 'optional host keys', 'key exchanges', 'ciphers', 'macs', 'client policy', 'host_key_sizes', 'dh_modulus_sizes', 'allow_algorithm_subset_and_reordering', 'allow_larger_keys'] and not key.startswith('hostkey_size_') and not key.startswith('cakey_size_') and not key.startswith('dh_modulus_size_'):
if key not in ['name', 'version', 'banner', 'compressions', 'host keys', 'optional host keys', 'key exchanges', 'ciphers', 'macs', 'client policy', 'host_key_sizes', 'dh_modulus_sizes', 'allow_algorithm_subset_and_reordering', 'allow_hostkey_subset_and_reordering', 'allow_larger_keys'] and not key.startswith('hostkey_size_') and not key.startswith('cakey_size_') and not key.startswith('dh_modulus_size_'):
raise ValueError("invalid field found in policy: %s" % line)
if key in ['name', 'banner']:
@@ -211,6 +212,9 @@ class Policy:
self._server_policy = False
elif key == 'allow_algorithm_subset_and_reordering' and val.lower() == 'true':
self._allow_algorithm_subset_and_reordering = True
self._allow_hostkey_subset_and_reordering = True # We force subsets/reorderings of the host keys as well in this case.
elif key == 'allow_hostkey_subset_and_reordering' and val.lower() == 'true':
self._allow_hostkey_subset_and_reordering = True
elif key == 'allow_larger_keys' and val.lower() == 'true':
self._allow_larger_keys = True
@@ -300,9 +304,12 @@ name = "Custom Policy (based on %s on %s)"
# The version of this policy (displayed in the output during scans). Not parsed, and may be any value, including strings.
version = 1
# When false, host keys, kex, ciphers, and MAC lists must match exactly. When true, the target host may support a subset of the specified algorithms and/or algorithms may appear in a different order; this feature is useful for specifying a baseline and allowing some hosts the option to implement stricter controls.
# When false, host keys, kex, ciphers, and MAC lists must match exactly. When true, the target host may support a subset of the specified algorithms and/or algorithms may appear in a different order; this feature is useful for specifying a baseline and allowing some hosts the option to implement stricter controls. A value of true automatically sets "allow_hostkey_subset_and_reordering" to true as well.
allow_algorithm_subset_and_reordering = false
# When false, the host key list must match exactly. When true, the target host may support a subset of the specified host keys and/or the keys may appear in a different order.
allow_hostkey_subset_and_reordering = true
# When false, host keys, CA keys, and Diffie-Hellman key sizes must exactly match what's specified in this policy. When true, target systems are allowed to have larger keys; this feature is useful for specifying a baseline and allowing some hosts the option to implement stricter controls.
allow_larger_keys = false
@@ -358,7 +365,7 @@ macs = %s
# Check host keys.
if self._host_keys is not None:
# If the policy allows subsets and re-ordered algorithms...
if self._allow_algorithm_subset_and_reordering:
if self._allow_hostkey_subset_and_reordering:
for hostkey_t in kex.key_algorithms:
if hostkey_t not in self._host_keys:
ret = False
@@ -467,11 +474,18 @@ macs = %s
def _get_errors(self) -> Tuple[List[Any], str]:
'''Returns the list of errors, along with the string representation of those errors.'''
subset_and_reordering_semicolon = "; subset and/or reordering allowed" if self._allow_algorithm_subset_and_reordering else "; exact match"
subset_and_reordering_parens = " (subset and/or reordering allowed)" if self._allow_algorithm_subset_and_reordering else ""
all_subset_and_reordering_semicolon = "; subset and/or reordering allowed" if self._allow_algorithm_subset_and_reordering else "; exact match"
all_subset_and_reordering_parens = " (subset and/or reordering allowed)" if self._allow_algorithm_subset_and_reordering else ""
hostkey_subset_and_reordering_semicolon = "; subset and/or reordering allowed" if self._allow_hostkey_subset_and_reordering else "; exact match"
hostkey_subset_and_reordering_parens = " (subset and/or reordering allowed)" if self._allow_hostkey_subset_and_reordering else ""
error_list = []
spacer = ''
for e in self._errors:
subset_and_reordering_semicolon = hostkey_subset_and_reordering_semicolon if e["mismatched_field"] == "Host keys" else all_subset_and_reordering_semicolon
subset_and_reordering_parens = hostkey_subset_and_reordering_parens if e["mismatched_field"] == "Host keys" else all_subset_and_reordering_parens
e_str = " * %s did not match.\n" % e['mismatched_field']
if ('expected_optional' in e) and (e['expected_optional'] != ['']):
@@ -579,6 +593,7 @@ macs = %s
p._dh_modulus_sizes = cast(Optional[Dict[str, int]], policy_struct['dh_modulus_sizes']) # pylint: disable=protected-access
p._server_policy = cast(bool, policy_struct['server_policy']) # pylint: disable=protected-access
p._name_and_version = "%s (version %s)" % (p._name, p._version) # pylint: disable=protected-access
p._allow_hostkey_subset_and_reordering = cast(bool, policy_struct['allow_hostkey_subset_and_reordering']) if 'allow_hostkey_subset_and_reordering' in policy_struct else False # pylint: disable=protected-access
# Ensure this struct has all the necessary fields.
p._normalize_hostkey_sizes() # pylint: disable=protected-access
@@ -646,7 +661,7 @@ macs = %s
if self._dh_modulus_sizes is not None:
dh_modulus_sizes_str = str(self._dh_modulus_sizes)
return "Name: %s\nVersion: %s\nAllow Algorithm Subset and/or Reordering: %r\nBanner: %s\nCompressions: %s\nHost Keys: %s\nOptional Host Keys: %s\nKey Exchanges: %s\nCiphers: %s\nMACs: %s\nHost Key Sizes: %s\nDH Modulus Sizes: %s\nServer Policy: %r" % (name, version, self._allow_algorithm_subset_and_reordering, banner, compressions_str, host_keys_str, optional_host_keys_str, kex_str, ciphers_str, macs_str, hostkey_sizes_str, dh_modulus_sizes_str, self._server_policy)
return "Name: %s\nVersion: %s\nAllow Algorithm Subset and/or Reordering: %r\nAllow Host Key Subset and/or Reordering: %r\nBanner: %s\nCompressions: %s\nHost Keys: %s\nOptional Host Keys: %s\nKey Exchanges: %s\nCiphers: %s\nMACs: %s\nHost Key Sizes: %s\nDH Modulus Sizes: %s\nServer Policy: %r" % (name, version, self._allow_algorithm_subset_and_reordering, self._allow_hostkey_subset_and_reordering, banner, compressions_str, host_keys_str, optional_host_keys_str, kex_str, ciphers_str, macs_str, hostkey_sizes_str, dh_modulus_sizes_str, self._server_policy)
def __getstate__(self) -> Dict[str, Any]:
+23 -4
View File
@@ -72,10 +72,29 @@ class Software:
oversion, opatch = mx.group(1), mx.group(2).strip()
else:
oversion, opatch = other, ''
if self.version < oversion:
return -1
elif self.version > oversion:
return 1
# Attempt to parse the versions into floats.
float_parse_error = False
selfversion_float = 0.0
oversion_float = 0.0
try:
selfversion_float = float(self.version)
oversion_float = float(oversion)
except ValueError:
float_parse_error = True
# If the versions could not be parsed into floats, use the old string compare method. This returns incorrect results when comparing modern OpenSSH versions ("9.6" > "10.4" evaluates to True!), but removing this may trigger other bugs, and we have no other good options here, soooo...
if float_parse_error:
if self.version < oversion:
return -1
elif self.version > oversion:
return 1
else: # Float parsing worked, so we'll use this instead. That way (9.6 > 10.4) properly evaluates to False.
if selfversion_float < oversion_float: # pylint: disable=else-if-used
return -1
elif selfversion_float > oversion_float:
return 1
spatch = self.patch or ''
if self.product == Product.DropbearSSH:
if not re.match(r'^test\d.*$', opatch):
+17 -1
View File
@@ -1,7 +1,7 @@
"""
The MIT License (MIT)
Copyright (C) 2017-2025 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2017-2026 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -81,6 +81,11 @@ class SSH2_KexDB: # pylint: disable=too-few-public-methods
INFO_EXTENSION_NEGOTIATION = 'pseudo-algorithm that denotes the peer supports RFC8308 extensions'
INFO_STRICT_KEX = 'pseudo-algorithm that denotes the peer supports a stricter key exchange method as a counter-measure to the Terrapin attack (CVE-2023-48795)'
# NIST PQC security levels: https://blog.cloudflare.com/pq-2025/
INFO_NIST_PQC_LEVEL_2 = 'rated at NIST PQC level 2 (at least as hard to break as SHA256)'
INFO_NIST_PQC_LEVEL_3 = 'rated at NIST PQC level 3 (at least as hard to break as AES-192)'
INFO_NIST_PQC_LEVEL_5 = 'rated at NIST PQC level 5 (at least as hard to break as AES-256)'
# Maintains a dictionary per calling thread that yields its own copy of MASTER_DB. This prevents results from one thread polluting the results of another thread.
DB_PER_THREAD: Dict[int, Dict[str, Dict[str, List[List[Optional[str]]]]]] = {}
@@ -230,6 +235,9 @@ class SSH2_KexDB: # pylint: disable=too-few-public-methods
'ecdsa-sha2-nistt571': [[], [FAIL_UNPROVEN], [WARN_RNDSIG_KEY]],
'eddsa-e382-shake256@libassh.org': [[], [FAIL_UNPROVEN]],
'eddsa-e521-shake256@libassh.org': [[], [FAIL_UNPROVEN]],
'mldsa-44': [[], [], [], [INFO_NIST_PQC_LEVEL_2]],
'mldsa-65': [[], [], [], [INFO_NIST_PQC_LEVEL_3]],
'mldsa-87': [[], [], [], [INFO_NIST_PQC_LEVEL_5]],
'null': [[], [FAIL_PLAINTEXT]],
'pgp-sign-dss': [[], [FAIL_1024BIT_MODULUS]],
'pgp-sign-rsa': [[], [FAIL_1024BIT_MODULUS]],
@@ -259,6 +267,13 @@ class SSH2_KexDB: # pylint: disable=too-few-public-methods
'ssh-gost2001': [[], [FAIL_UNTRUSTED]],
'ssh-gost2012-256': [[], [FAIL_UNTRUSTED]],
'ssh-gost2012-512': [[], [FAIL_UNTRUSTED]],
'ssh-mldsa-44': [[], [], [], [INFO_NIST_PQC_LEVEL_2]],
'ssh-mldsa-65': [[], [], [], [INFO_NIST_PQC_LEVEL_3]],
'ssh-mldsa-87': [[], [], [], [INFO_NIST_PQC_LEVEL_5]],
'ssh-mldsa44': [[], [], [], [INFO_NIST_PQC_LEVEL_2]],
'ssh-mldsa44-ed25519@openssh.com': [['10.4'], [], [], [INFO_NIST_PQC_LEVEL_2]],
'ssh-mldsa65': [[], [], [], [INFO_NIST_PQC_LEVEL_3]],
'ssh-mldsa87': [[], [], [], [INFO_NIST_PQC_LEVEL_5]],
'ssh-rsa1': [[], [FAIL_SHA1]],
'ssh-rsa': [['2.5.0,d0.28,l10.2'], [FAIL_SHA1], [], [INFO_DEPRECATED_IN_OPENSSH88]],
'ssh-rsa-cert-v00@openssh.com': [['5.4', '6.9'], [FAIL_SHA1], [], [INFO_REMOVED_IN_OPENSSH70]],
@@ -272,6 +287,7 @@ class SSH2_KexDB: # pylint: disable=too-few-public-methods
'ssh-xmss-cert-v01@openssh.com': [['7.7'], [WARN_EXPERIMENTAL]],
'ssh-xmss@openssh.com': [['7.7'], [WARN_EXPERIMENTAL]],
'webauthn-sk-ecdsa-sha2-nistp256@openssh.com': [['8.3'], [FAIL_NSA_BACKDOORED_CURVE]],
'webauthn-sk-ecdsa-sha2-nistp256-cert-v01@openssh.com': [['10.3'], [FAIL_NSA_BACKDOORED_CURVE]],
'x509v3-ecdsa-sha2-1.3.132.0.10': [[], [FAIL_UNKNOWN]],
'x509v3-ecdsa-sha2-nistp256': [[], [FAIL_NSA_BACKDOORED_CURVE]],
'x509v3-ecdsa-sha2-nistp384': [[], [FAIL_NSA_BACKDOORED_CURVE]],
+33 -12
View File
@@ -2,7 +2,7 @@
"""
The MIT License (MIT)
Copyright (C) 2017-2025 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2017-2026 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -53,6 +53,7 @@ from ssh_audit.gextest import GEXTest
from ssh_audit.hostkeytest import HostKeyTest
from ssh_audit.outputbuffer import OutputBuffer
from ssh_audit.policy import Policy
from ssh_audit.hardening_guides import Hardening_Guides
from ssh_audit.product import Product
from ssh_audit.protocol import Protocol
from ssh_audit.software import Software
@@ -332,7 +333,7 @@ def output_info(out: OutputBuffer, software: Optional['Software'], client_audit:
# If any warnings or failures were given, print a link to the hardening guides.
if any_problems:
out.warn('(nfo) For hardening guides on common OSes, please see: <https://www.ssh-audit.com/hardening_guides.html>')
out.warn('(nfo) For hardening guides on common OSes, a built-in list can be viewed with --list-hardening-guides, or an online list can be found at: <https://www.ssh-audit.com/hardening_guides.html>')
# Add any additional notes.
for additional_note in additional_notes:
@@ -789,11 +790,15 @@ def process_commandline(out: OutputBuffer, args: List[str]) -> 'AuditConf': # p
# Add long options to the parser
parser.add_argument("--conn-rate-test", action="store", dest="conn_rate_test", metavar="N[:max_rate]", type=str, default=None, help="perform a connection rate test (useful for collecting metrics related to susceptibility of the DHEat vuln). Testing is conducted with N concurrent sockets with an optional maximum rate of connections per second")
parser.add_argument("--dheat", action="store", dest="dheat", metavar="N[:kex[:e_len]]", type=str, default=None, help="continuously perform the DHEat DoS attack (CVE-2002-20001) against the target using N concurrent sockets. Optionally, a specific key exchange algorithm can be specified instead of allowing it to be automatically chosen. Additionally, a small length of the fake e value sent to the server can be chosen for a more efficient attack (such as 4).")
parser.add_argument("--get-hardening-guide", action="store", metavar="platform", dest="get_hardening_guide", type=str, default=None, help="retrieves the hardening guide for the specified platform name (use --list-hardening-guides to see list of available guides).")
parser.add_argument("--list-hardening-guides", action="store_true", dest="list_hardening_guides", default=False, help="list all official, built-in hardening guides for common systems. Their full names can then be passed to --get-hardening-guide. Add -v to this option to view hardening guide change logs and prior versions.")
parser.add_argument("--lookup", action="store", dest="lookup", metavar="alg1[,alg2,...]", type=str, default=None, help="looks up an algorithm(s) without connecting to a server.")
parser.add_argument("--skip-rate-test", action="store_true", dest="skip_rate_test", default=False, help="skip the connection rate test during standard audits (used to safely infer whether the DHEat attack is viable)")
parser.add_argument("--socks5", action="store", dest="socks5_proxy", metavar="host:port", type=str, default=None, help="connect via a SOCKS5 proxy (implies --skip-rate-test)")
parser.add_argument("--threads", action="store", dest="threads", metavar="N", type=int, default=32, help="number of threads to use when scanning multiple targets (-T/--targets) (default: %(default)s)")
# The mandatory target option. Or rather, mandatory when -L, -T, or --lookup are not used.
# The mandatory target option. Or rather, mandatory when -L, -T, --lookup or --print-config are not used.
parser.add_argument("host", nargs="?", action="store", type=str, default="", help="target hostname or IPv4/IPv6 address")
# If no arguments were given, print the help and exit.
@@ -813,11 +818,17 @@ def process_commandline(out: OutputBuffer, args: List[str]) -> 'AuditConf': # p
aconf.list_policies = argument.list_policies
aconf.manual = argument.manual
aconf.skip_rate_test = argument.skip_rate_test
aconf.socks5_proxy = argument.socks5_proxy
oport = argument.oport
if argument.batch is True:
aconf.batch = True
# Skip the rate test if we're using a SOCKS5 proxy.
if aconf.socks5_proxy is not None:
out.d("Disabling rate test since SOCKS5 proxy is set.", write_now=True)
aconf.skip_rate_test = True
# If one -j was given, turn on JSON output. If -jj was given, enable indentation.
aconf.json = argument.json > 0
if argument.json > 1:
@@ -884,13 +895,13 @@ def process_commandline(out: OutputBuffer, args: List[str]) -> 'AuditConf': # p
aconf.verbose = True
out.verbose = True
except argparse.ArgumentError as err:
out.fail(str(err), write_now=True)
except (argparse.ArgumentError, ValueError) as err:
out.fail(f"Error: {str(err)}", write_now=True)
parser.print_help()
sys.exit(exitcodes.UNKNOWN_ERROR)
if argument.host == "" and argument.client_audit is False and argument.targets is None and argument.list_policies is False and argument.lookup is None and argument.manual is False:
out.fail("target host must be specified, unless -c, -m, -L, -T, or --lookup are used", write_now=True)
if argument.host == "" and argument.client_audit is False and argument.targets is None and argument.list_policies is False and argument.lookup is None and argument.manual is False and argument.list_hardening_guides is False and argument.get_hardening_guide is None:
out.fail("target host must be specified, unless -c, -m, -L, -T, --lookup or --list-hardening-guides are used", write_now=True)
sys.exit(exitcodes.UNKNOWN_ERROR)
if aconf.manual:
@@ -903,19 +914,29 @@ def process_commandline(out: OutputBuffer, args: List[str]) -> 'AuditConf': # p
list_policies(out, aconf.verbose)
sys.exit(exitcodes.GOOD)
# Print a list of the hardening guides, or the specific guide requested by the user.
if argument.list_hardening_guides is True:
Hardening_Guides.list_guides(out, aconf.verbose)
sys.exit(exitcodes.GOOD)
elif argument.get_hardening_guide is not None:
Hardening_Guides.print_hardening_guide(out, argument.get_hardening_guide)
sys.exit(exitcodes.GOOD)
# If we're doing a server audit against a single target.
if aconf.client_audit is False and aconf.target_file is None:
if oport is not None:
host = argument.host
else:
host = argument.host
if oport is None:
host, port = Utils.parse_host_and_port(argument.host)
if not host and aconf.target_file is None:
out.fail("target host is not specified", write_now=True)
sys.exit(exitcodes.UNKNOWN_ERROR)
if oport is None and aconf.client_audit: # The default port to listen on during a client audit is 2222.
# For client audits, if an explicit port isn't set, default to 2222/tcp.
if aconf.client_audit and oport is None:
port = 2222
# If an explicit port was set by the user, parse it.
if oport is not None:
port = Utils.parse_int(oport)
if port < 1 or port > 65535:
@@ -1132,7 +1153,7 @@ def audit(out: OutputBuffer, aconf: AuditConf, print_target: bool = False) -> in
out.debug = aconf.debug
out.level = aconf.level
out.use_colors = aconf.colors
s = SSH_Socket(out, aconf.host, aconf.port, aconf.ip_version_preference, aconf.timeout, aconf.timeout_set)
s = SSH_Socket(out, aconf.host, aconf.port, aconf.ip_version_preference, aconf.timeout, aconf.timeout_set, aconf.socks5_proxy)
if aconf.client_audit:
out.v("Listening for client connection on port %d..." % aconf.port, write_now=True)
+133 -4
View File
@@ -1,7 +1,7 @@
"""
The MIT License (MIT)
Copyright (C) 2017-2025 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2017-2026 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -51,7 +51,7 @@ class SSH_Socket(ReadBuf, WriteBuf):
SM_BANNER_SENT = 1
def __init__(self, outputbuffer: 'OutputBuffer', host: Optional[str], port: int, ip_version_preference: List[int] = [], timeout: Union[int, float] = 5, timeout_set: bool = False) -> None: # pylint: disable=dangerous-default-value
def __init__(self, outputbuffer: 'OutputBuffer', host: Optional[str], port: int, ip_version_preference: List[int] = [], timeout: Union[int, float] = 5, timeout_set: bool = False, socks5_proxy: Optional[str] = None) -> None: # pylint: disable=dangerous-default-value
super(SSH_Socket, self).__init__()
self.__outputbuffer = outputbuffer
self.__sock: Optional[socket.socket] = None
@@ -72,6 +72,8 @@ class SSH_Socket(ReadBuf, WriteBuf):
self.__timeout_set = timeout_set
self.client_host: Optional[str] = None
self.client_port = None
self.__socks5_proxy = socks5_proxy # SOCKS5 proxy in "host:port" format, or None
def _resolve(self) -> Iterable[Tuple[int, Tuple[Any, ...]]]:
"""Resolves a hostname into a list of IPs
@@ -95,11 +97,13 @@ class SSH_Socket(ReadBuf, WriteBuf):
if socktype == socket.SOCK_STREAM:
yield af, addr
# Listens on a server socket and accepts one connection (used for
# auditing client connections).
def listen_and_accept(self) -> None:
'''Listens on a server socket and accepts one connection (used for auditing client connections).'''
try:
self.__outputbuffer.d(f"Listening on 0.0.0.0:{self.__port}...", write_now=True)
# Socket to listen on all IPv4 addresses.
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
@@ -110,6 +114,8 @@ class SSH_Socket(ReadBuf, WriteBuf):
print("Warning: failed to listen on any IPv4 interfaces: %s" % str(e), file=sys.stderr)
try:
self.__outputbuffer.d(f"Listening on [::]:{self.__port}...", write_now=True)
# Socket to listen on all IPv6 addresses.
s = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
@@ -150,11 +156,27 @@ class SSH_Socket(ReadBuf, WriteBuf):
c.settimeout(self.__timeout)
self.__sock = c
def connect(self) -> Optional[str]:
'''Returns None on success, or an error string.'''
err = None
s = None
try:
# If we're connecting to a UNIX socket.
if self.__host.startswith("unix://"):
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.settimeout(self.__timeout)
s.connect(self.__host[7:])
self.__sock = s
return None
# If we're connecting through a SOCKS5 proxy.
if self.__socks5_proxy is not None:
self.__sock = self._connect_via_socks5()
return None
# We're connecting to an Internet host.
for af, addr in self._resolve():
s = socket.socket(af, socket.SOCK_STREAM)
s.settimeout(self.__timeout)
@@ -162,16 +184,112 @@ class SSH_Socket(ReadBuf, WriteBuf):
s.connect(addr)
self.__sock = s
return None
except socket.error as e:
err = e
self._close_socket(s)
if err is None:
errm = 'host {} has no DNS records'.format(self.__host)
else:
errt = (self.__host, self.__port, err)
errm = 'cannot connect to {} port {}: {}'.format(*errt)
return '[exception] {}'.format(errm)
def _connect_via_socks5(self) -> socket.socket:
'''Connect to the target host:port via a SOCKS5 proxy. Returns a socket on success, or raises an exception.'''
def __socks5_recv_exact(s: socket.socket, n: int) -> Optional[bytes]:
'''Read exactly n bytes from socket s, returning None on EOF.'''
buf = b''
while len(buf) < n:
chunk = s.recv(n - len(buf))
if not chunk:
return None
buf += chunk
return buf
# Parse the "host:port" string into its parts.
proxy_host, proxy_port = Utils.parse_host_and_port(self.__socks5_proxy) if self.__socks5_proxy is not None else ("", 0)
self.__outputbuffer.d("Connecting to SOCKS5 proxy %s:%d..." % (proxy_host, proxy_port), write_now=True)
s = socket.create_connection((proxy_host, proxy_port), timeout=self.__timeout)
# SOCKS5 greeting: version=5, nmethods=1, method=0 (no auth)
s.sendall(b'\x05\x01\x00')
resp = __socks5_recv_exact(s, 2)
if resp is None:
raise socket.error("no response from SOCKS5 proxy during handshake")
if resp[0] != 5:
raise socket.error("SOCKS5 proxy returned unexpected version: {}".format(resp[0]))
if resp[1] == 0xff:
raise socket.error("SOCKS5 proxy rejected all authentication methods")
if resp[1] != 0:
raise socket.error("SOCKS5 proxy requires authentication (method {:d}), but only no-auth is supported".format(resp[1]))
# Set the type and host encoding appropriately, depending on if we're sending a hostname, IPv4, or IPv6 address. The ATYP field is 3 when the client is sending a hostname.
atyp = 3
_enc_host = self.__host.encode('idna')
dst_addr = struct.pack('!B', len(_enc_host)) + _enc_host
if Utils.is_ipv4_address(self.__host):
atyp = 1
dst_addr = socket.inet_pton(socket.AF_INET, self.__host)
elif Utils.is_ipv6_address(self.__host):
atyp = 4
dst_addr = socket.inet_pton(socket.AF_INET6, self.__host)
# SOCKS5 connect request: version=5, cmd=1 (connect), rsv=0, atyp
request = struct.pack('!BBBB', 5, 1, 0, atyp) + dst_addr + struct.pack('!H', self.__port)
self.__outputbuffer.d("Requesting SOCKS5 proxy to connect to %s:%d..." % (self.__host, self.__port), write_now=True)
s.sendall(request)
# Read the fixed part of the response (4 bytes: ver, rep, rsv, atyp)
hdr = __socks5_recv_exact(s, 4)
if hdr is None:
raise socket.error("no response from SOCKS5 proxy during connect")
server_version = hdr[0]
reply = hdr[1]
# rsv = hdr[2] # Reserved, set to 0 as per RFC1928.
atyp = hdr[3]
if server_version != 5:
raise socket.error("SOCKS5 proxy returned unexpected version in connect response: {}".format(hdr[0]))
if reply != 0:
socks5_errors = {
1: "general SOCKS server failure",
2: "connection not allowed by ruleset",
3: "network unreachable",
4: "host unreachable",
5: "connection refused",
6: "TTL expired",
7: "command not supported",
8: "address type not supported",
}
err = socks5_errors[reply] if reply in socks5_errors else f"unknown error: {reply}"
raise socket.error("SOCKS5 proxy connect failed: {}".format(err))
# Read and discard the bound address from the response
if atyp == 1: # IPv4
__socks5_recv_exact(s, 4 + 2)
elif atyp == 4: # IPv6
__socks5_recv_exact(s, 16 + 2)
elif atyp == 3: # domain name
alen_data = __socks5_recv_exact(s, 1)
if alen_data is None:
raise socket.error("truncated SOCKS5 response")
__socks5_recv_exact(s, alen_data[0] + 2)
else:
raise socket.error("SOCKS5 proxy returned unknown address type: {}".format(atyp))
self.__outputbuffer.d("Successfully established SOCKS5 connection.", write_now=True)
return s
def get_banner(self) -> Tuple[Optional['Banner'], List[str], Optional[str]]:
self.__outputbuffer.d('Getting banner...', write_now=True)
@@ -201,6 +319,7 @@ class SSH_Socket(ReadBuf, WriteBuf):
return self.__banner, self.__header, e
def recv(self, size: int = 2048) -> Tuple[int, Optional[str]]:
if self.__sock is None:
return -1, 'not connected'
@@ -221,6 +340,7 @@ class SSH_Socket(ReadBuf, WriteBuf):
self._buf.seek(pos, 0)
return len(data), None
def send(self, data: bytes) -> Tuple[int, Optional[str]]:
if self.__sock is None:
return -1, 'not connected'
@@ -243,16 +363,19 @@ class SSH_Socket(ReadBuf, WriteBuf):
kex.write(self)
self.send_packet()
def send_banner(self, banner: str) -> None:
self.send(banner.encode() + b'\r\n')
self.__state = max(self.__state, self.SM_BANNER_SENT)
def ensure_read(self, size: int) -> None:
while self.unread_len < size:
s, e = self.recv()
if s < 0:
raise SSH_Socket.InsufficientReadException(e)
def read_packet(self) -> Tuple[int, bytes]:
try:
header = WriteBuf()
@@ -284,6 +407,7 @@ class SSH_Socket(ReadBuf, WriteBuf):
e = ex.args[0].encode('utf-8')
return -1, e
def send_packet(self) -> Tuple[int, Optional[str]]:
payload = self.write_flush()
padding = -(len(payload) + 5) % 8
@@ -294,10 +418,12 @@ class SSH_Socket(ReadBuf, WriteBuf):
data = struct.pack('>Ib', plen, padding) + payload + pad_bytes
return self.send(data)
def is_connected(self) -> bool:
"""Returns true if this Socket is connected, False otherwise."""
return self.__sock is not None
def close(self) -> None:
self.__cleanup()
self.reset()
@@ -305,6 +431,7 @@ class SSH_Socket(ReadBuf, WriteBuf):
self.__header = []
self.__banner = None
def _close_socket(self, s: Optional[socket.socket]) -> None:
try:
if s is not None:
@@ -313,9 +440,11 @@ class SSH_Socket(ReadBuf, WriteBuf):
except Exception:
pass
def __del__(self) -> None:
self.__cleanup()
def __cleanup(self) -> None:
self._close_socket(self.__sock)
for sock in self.__sock_map.values():
+16 -1
View File
@@ -1,7 +1,7 @@
"""
The MIT License (MIT)
Copyright (C) 2017-2020 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2017-2026 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -134,6 +134,10 @@ class Utils:
host = host_and_port
port = default_port
# If we have a UNIX socket path, do no further processing.
if host.startswith("unix://"):
return host, 1
mx = re.match(r'^\[([^\]]+)\](?::(\d+))?$', host_and_port)
if mx is not None:
host = mx.group(1)
@@ -149,6 +153,17 @@ class Utils:
return host, port
@staticmethod
def is_ipv4_address(address: str) -> bool:
'''Returns True if address is an IPv4 address, otherwise False.'''
is_ipv4 = True
try:
ipaddress.IPv4Address(address)
except ipaddress.AddressValueError:
is_ipv4 = False
return is_ipv4
@staticmethod
def is_ipv6_address(address: str) -> bool:
'''Returns True if address is an IPv6 address, otherwise False.'''
+19 -3
View File
@@ -1,4 +1,4 @@
.TH SSH-AUDIT 1 "July 26, 2025"
.TH SSH-AUDIT 1 "June 20, 2026"
.SH NAME
\fBssh-audit\fP \- SSH server & client configuration auditor
.SH SYNOPSIS
@@ -51,6 +51,11 @@ Enable debug output.
.br
Run the DHEat DoS attack (CVE-2002-20001) against the target server (which will consume all available CPU resources). The number of concurrent sockets, N, needed to achieve this effect will be highly dependent on the CPU resources available on the target, as well as the latency between the source and target machines. The key exchange is automatically chosen based on which would cause maximum effect, unless explicitly chosen in the second field. Lastly, an (experimental) option allows the length in bytes of the fake e value sent to the server to be specified in the third field. Normally, the length of e is roughly the length of the modulus of the Diffie-Hellman exchange (hence, an 8192-bit / 1024-byte value of e is sent in each connection when targeting the diffie-hellman-group18-sha512 algorithm). Instead, it was observed that many SSH implementations accept small values, such as 4 bytes; this results in a much more network-efficient attack.
.TP
.B \-\-get\-hardening\-guide=<platform_name>
.br
Retrieves the hardening guide for the specified platform name (use \-\-list\-hardening\-guides to see list of available guides).
.TP
.B -g, \-\-gex-test=<x[,y,...] | min1:pref1:max1[,min2:pref2:max2,...] | x-y[:step]>
.br
@@ -91,10 +96,15 @@ Output results in JSON format. Specify twice (-jj) to enable indent printing (u
.br
Specify the minimum output level. Default is info.
.TP
.B \-\-list-hardening-guides
.br
List all official, built-in hardening guides for common systems. Their full names can then be passed to \-\-get\-hardening\-guide. Add \-v to this option to view hardening guide change logs and prior versions.
.TP
.B -L, \-\-list-policies
.br
List all official, built-in policies for common systems. Their full names can then be passed to -P/--policy. Add \-v to \-L to view policy change logs.
List all official, built-in policies for common systems. Their full names can then be passed to \-P/\-\-policy. Add \-v to \-L to view policy change logs.
.TP
.B \-\-lookup=<alg1,alg2,...>
@@ -131,6 +141,11 @@ Runs a policy audit against a target using the specified policy (see \fBPOLICY A
.br
Skips the connection rate test during standard audits. By default, a few dozen TCP connections are created with the target host to see if connection throttling is implemented (this can safely infer whether the target is vulnerable to the DHEat attack; see CVE-2002-20001).
.TP
.B \-\-socks5 host:port
.br
Connects through a SOCKS5 proxy. Rate tests are automatically skipped (i.e.: implies \-\-skip\-rate\-test).
.TP
.B -t, \-\-timeout=<secs>
.br
@@ -139,7 +154,7 @@ The timeout, in seconds, for creating connections and reading data from the sock
.TP
.B -T, \-\-targets=<hosts.txt>
.br
A file containing a list of target hosts. Each line must have one host, in the format of HOST[:PORT]. Use -p/--port to set the default port for all hosts. Use --threads to control concurrent scans.
A file containing a list of target hosts. Each line must have one host, in the format of HOST[:PORT] (or for UNIX sockets, use "unix://path/to/socket"). Use -p/--port to set the default port for all hosts. Use --threads to control concurrent scans.
.TP
.B \-\-threads=<threads>
@@ -178,6 +193,7 @@ ssh-audit 127.0.0.1
ssh-audit 127.0.0.1:222
ssh-audit ::1
ssh-audit [::1]:222
ssh-audit unix:///run/ssh-unix-local/socket
.fi
.RE
@@ -93,5 +93,5 @@
(rec) -kexguess2@matt.ucc.asn.au -- kex algorithm to remove 
# additional info
(nfo) For hardening guides on common OSes, please see: <https://www.ssh-audit.com/hardening_guides.html>
(nfo) For hardening guides on common OSes, a built-in list can be viewed with --list-hardening-guides, or an online list can be found at: <https://www.ssh-audit.com/hardening_guides.html>
@@ -107,5 +107,5 @@
(rec) -aes256-cbc -- enc algorithm to remove 
# additional info
(nfo) For hardening guides on common OSes, please see: <https://www.ssh-audit.com/hardening_guides.html>
(nfo) For hardening guides on common OSes, a built-in list can be viewed with --list-hardening-guides, or an online list can be found at: <https://www.ssh-audit.com/hardening_guides.html>
@@ -117,5 +117,5 @@
(rec) -umac-64@openssh.com -- mac algorithm to remove 
# additional info
(nfo) For hardening guides on common OSes, please see: <https://www.ssh-audit.com/hardening_guides.html>
(nfo) For hardening guides on common OSes, a built-in list can be viewed with --list-hardening-guides, or an online list can be found at: <https://www.ssh-audit.com/hardening_guides.html>
@@ -118,5 +118,5 @@
(rec) -umac-64@openssh.com -- mac algorithm to remove 
# additional info
(nfo) For hardening guides on common OSes, please see: <https://www.ssh-audit.com/hardening_guides.html>
(nfo) For hardening guides on common OSes, a built-in list can be viewed with --list-hardening-guides, or an online list can be found at: <https://www.ssh-audit.com/hardening_guides.html>
@@ -117,5 +117,5 @@
(rec) -umac-64@openssh.com -- mac algorithm to remove 
# additional info
(nfo) For hardening guides on common OSes, please see: <https://www.ssh-audit.com/hardening_guides.html>
(nfo) For hardening guides on common OSes, a built-in list can be viewed with --list-hardening-guides, or an online list can be found at: <https://www.ssh-audit.com/hardening_guides.html>
@@ -116,5 +116,5 @@
(rec) -umac-64@openssh.com -- mac algorithm to remove 
# additional info
(nfo) For hardening guides on common OSes, please see: <https://www.ssh-audit.com/hardening_guides.html>
(nfo) For hardening guides on common OSes, a built-in list can be viewed with --list-hardening-guides, or an online list can be found at: <https://www.ssh-audit.com/hardening_guides.html>
@@ -115,5 +115,5 @@
(rec) -umac-64@openssh.com -- mac algorithm to remove 
# additional info
(nfo) For hardening guides on common OSes, please see: <https://www.ssh-audit.com/hardening_guides.html>
(nfo) For hardening guides on common OSes, a built-in list can be viewed with --list-hardening-guides, or an online list can be found at: <https://www.ssh-audit.com/hardening_guides.html>
@@ -0,0 +1,8 @@
{
"errors": [],
"host": "localhost",
"passed": true,
"policy": "Docker policy: test18 (version 1)",
"port": 2222,
"warnings": []
}
@@ -0,0 +1,3 @@
Host: localhost:2222
Policy: Docker policy: test18 (version 1)
Result: ✔ Passed
@@ -104,5 +104,5 @@
(rec) -umac-64@openssh.com -- mac algorithm to remove 
# additional info
(nfo) For hardening guides on common OSes, please see: <https://www.ssh-audit.com/hardening_guides.html>
(nfo) For hardening guides on common OSes, a built-in list can be viewed with --list-hardening-guides, or an online list can be found at: <https://www.ssh-audit.com/hardening_guides.html>
@@ -96,5 +96,5 @@
(rec) -umac-64@openssh.com -- mac algorithm to remove 
# additional info
(nfo) For hardening guides on common OSes, please see: <https://www.ssh-audit.com/hardening_guides.html>
(nfo) For hardening guides on common OSes, a built-in list can be viewed with --list-hardening-guides, or an online list can be found at: <https://www.ssh-audit.com/hardening_guides.html>
@@ -45,5 +45,5 @@
(rec) -curve25519-sha256@libssh.org -- kex algorithm to remove 
# additional info
(nfo) For hardening guides on common OSes, please see: <https://www.ssh-audit.com/hardening_guides.html>
(nfo) For hardening guides on common OSes, a built-in list can be viewed with --list-hardening-guides, or an online list can be found at: <https://www.ssh-audit.com/hardening_guides.html>
+17
View File
@@ -0,0 +1,17 @@
#
# Docker policy: test18
#
name = "Docker policy: test18"
version = 1
allow_algorithm_subset_and_reordering = false
allow_hostkey_subset_and_reordering = true
allow_larger_keys = false
banner = "SSH-2.0-OpenSSH_8.0"
compressions = none, zlib@openssh.com
host keys = ssh-rsa, rsa-sha2-256, rsa-sha2-512, ecdsa-sha2-nistp256, ssh-ed25519, x
key exchanges = curve25519-sha256, curve25519-sha256@libssh.org, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, diffie-hellman-group-exchange-sha256, diffie-hellman-group16-sha512, diffie-hellman-group18-sha512, diffie-hellman-group14-sha256, diffie-hellman-group14-sha1
ciphers = chacha20-poly1305@openssh.com, aes128-ctr, aes192-ctr, aes256-ctr, aes128-gcm@openssh.com, aes256-gcm@openssh.com
macs = umac-64-etm@openssh.com, umac-128-etm@openssh.com, hmac-sha2-256-etm@openssh.com, hmac-sha2-512-etm@openssh.com, hmac-sha1-etm@openssh.com, umac-64@openssh.com, umac-128@openssh.com, hmac-sha2-256, hmac-sha2-512, hmac-sha1
host_key_sizes = {"ssh-rsa": {"hostkey_size": 3072}, "rsa-sha2-256": {"hostkey_size": 3072}, "rsa-sha2-512": {"hostkey_size": 3072}, "ssh-ed25519": {"hostkey_size": 256}}
dh_modulus_sizes = {"diffie-hellman-group-exchange-sha256": 4096}
+16
View File
@@ -165,3 +165,19 @@ class TestAuditConf:
self._test_conf(conf, host='localhost', level='fail')
with pytest.raises(SystemExit):
conf = c('-l something localhost')
def test_audit_conf_process_commandline_socks5_proxy(self):
c = lambda x: self.process_commandline(self.OutputBuffer, x.split()) # noqa
assert c('--socks5 127.0.0.1:1080 localhost').socks5_proxy == '127.0.0.1:1080'
assert c('--socks5 [fe80::aaaa:bbbb:cccc:dddd]:1080 localhost').socks5_proxy == '[fe80::aaaa:bbbb:cccc:dddd]:1080'
assert c('--socks5 somehost.lol:1080 localhost').socks5_proxy == 'somehost.lol:1080'
with pytest.raises(SystemExit):
c('--socks5 localhost localhost')
with pytest.raises(SystemExit):
c('--socks5 localhost:0 localhost')
with pytest.raises(SystemExit):
c('--socks5 localhost:65536 localhost')
+81
View File
@@ -0,0 +1,81 @@
import pytest
from ssh_audit.hardening_guides import Hardening_Guides
class TestHardeningGuides:
@pytest.fixture(autouse=True)
def init(self, ssh_audit):
self.OutputBuffer = ssh_audit.OutputBuffer()
def test_hardening_guides_consistency(self):
'''Ensure that the HARDENING_GUIDES struct is consistent.'''
# Required keys in each guide dict.
required_guide_fields = ["server_guide", "version", "version_date", "change_log", "notes", "commands"]
# Required keys in the commands dict.
required_command_fields = ["heading", "comment", "command"]
for name, guides in Hardening_Guides.HARDENING_GUIDES.items():
# Ensure the key (guide name) is a string.
assert type(name) is str
# Ensure the value (guides) is a list.
assert type(guides) is list
for guide in guides:
# Ensure each guide is a dict.
assert type(guide) is dict
# Ensure each required key is in this guide.
for required_guide_field in required_guide_fields:
assert required_guide_field in guide
# Check the guide values are the correct type.
assert type(guide["server_guide"]) is bool
assert type(guide["version"]) is int
assert type(guide["version_date"]) is str
assert type(guide["change_log"]) is str
assert type(guide["notes"]) is str
assert type(guide["commands"]) is list
# The version must be creater than zero.
assert guide["version"] > 0
# Ensure the format is "YYYY-MM-DD".
version_date = guide["version_date"]
date_fields = version_date.split("-")
assert len(date_fields) == 3
# Check that the year is 4 digits and greater than 0.
year = date_fields[0]
assert len(year) == 4
assert int(year) > 0
# Check that the month is 2 digits and between 1 and 12.
month = date_fields[1]
assert len(month) == 2
assert 1 <= int(month) <= 12
# Check that the day is 2 digits and between 1 and 31.
day = date_fields[2]
assert len(day) == 2
assert 1 <= int(day) <= 31
# Check that the change log is filled in.
assert len(guide["change_log"]) > 0
commands = guide["commands"]
for command in commands:
# Ensure each required key is in this command list.
for required_command_field in required_command_fields:
assert required_command_field in command
# Check that these fields are not empty.
assert len(command["heading"]) > 0
assert len(command["command"]) > 0
+2 -5
View File
@@ -60,9 +60,6 @@ class TestPolicy:
except ValueError:
assert False, "version field of %s policy is not parseable as an integer." % policy_name
# Ensure no extra fields are present.
assert len(required_fields) == len(BUILTIN_POLICIES[policy_name])
# Ensure that the changelog field is a string and non-empty.
assert type(BUILTIN_POLICIES[policy_name]['changelog']) is str
assert len(BUILTIN_POLICIES[policy_name]['changelog']) > 0
@@ -158,7 +155,7 @@ ciphers = cipher_alg1, cipher_alg2, cipher_alg3
macs = mac_alg1, mac_alg2, mac_alg3'''
policy = self.Policy(policy_data=policy_data)
assert str(policy) == "Name: [Test Policy]\nVersion: [1]\nAllow Algorithm Subset and/or Reordering: False\nBanner: {undefined}\nCompressions: comp_alg1\nHost Keys: key_alg1\nOptional Host Keys: {undefined}\nKey Exchanges: kex_alg1, kex_alg2\nCiphers: cipher_alg1, cipher_alg2, cipher_alg3\nMACs: mac_alg1, mac_alg2, mac_alg3\nHost Key Sizes: {undefined}\nDH Modulus Sizes: {undefined}\nServer Policy: True"
assert str(policy) == "Name: [Test Policy]\nVersion: [1]\nAllow Algorithm Subset and/or Reordering: False\nAllow Host Key Subset and/or Reordering: False\nBanner: {undefined}\nCompressions: comp_alg1\nHost Keys: key_alg1\nOptional Host Keys: {undefined}\nKey Exchanges: kex_alg1, kex_alg2\nCiphers: cipher_alg1, cipher_alg2, cipher_alg3\nMACs: mac_alg1, mac_alg2, mac_alg3\nHost Key Sizes: {undefined}\nDH Modulus Sizes: {undefined}\nServer Policy: True"
def test_policy_invalid_1(self):
@@ -305,7 +302,7 @@ macs = mac_alg1, mac_alg2, mac_alg3'''
pol_data = pol_data.replace(date.today().strftime('%Y/%m/%d'), '[todays date]')
# Instead of writing out the entire expected policy--line by line--just check that it has the expected hash.
assert hashlib.sha256(pol_data.encode('ascii')).hexdigest() == 'fb84bce442cff2bce9bf653d6373a8a938e3bfcfbd1e876f51a08c1842df3cff'
assert hashlib.sha256(pol_data.encode('ascii')).hexdigest() == '3862e56ea60c1ecba6dffbf724216c4391b6fe00a790b0075617477d4addf761'
def test_policy_evaluate_passing_1(self):
+8 -9
View File
@@ -1,7 +1,6 @@
[tox]
envlist =
py{py3}-{test,pylint,flake8}
py{38,39,310,311,312,313}-{test,mypy,pylint,flake8}
py{310,311,312,313,314}-{test,mypy,pylint,flake8}
cov
skip_missing_interpreters = true
@@ -9,10 +8,10 @@ skip_missing_interpreters = true
deps =
test: pytest
test,cov: {[testenv:cov]deps}
test,py{38,39,310,311,312,313}-{type,mypy}: colorama
py{38,39,310,311,312,313}-{type,mypy}: {[testenv:mypy]deps}
py{py3,38,39,310,311,312,313}-{lint,pylint},lint: {[testenv:pylint]deps}
py{py3,38,39,310,311,312,313}-{lint,flake8},lint: {[testenv:flake8]deps}
test,py{310,311,312,313,314}-{type,mypy}: colorama
py{310,311,312,313,314}-{type,mypy}: {[testenv:mypy]deps}
py{310,311,312,313,314}-{lint,pylint},lint: {[testenv:pylint]deps}
py{310,311,312,313,314}-{lint,flake8},lint: {[testenv:flake8]deps}
setenv =
SSHAUDIT = {toxinidir}/src
test: COVERAGE_FILE = {toxinidir}/.coverage.{envname}
@@ -24,9 +23,9 @@ commands =
test: coverage combine
test: coverage report --show-missing
test: coverage html -d {toxinidir}/reports/html/coverage.{envname}
py{38,39,310,311,312,313}-{type,mypy}: {[testenv:mypy]commands}
py{py3,38,39,310,311,312,313}-{lint,pylint},lint: {[testenv:pylint]commands}
py{py3,38,39,310,311,312,313}-{lint,flake8},lint: {[testenv:flake8]commands}
py{310,311,312,313,314}-{type,mypy}: {[testenv:mypy]commands}
py{310,311,312,313,314}-{lint,pylint},lint: {[testenv:pylint]commands}
py{310,311,312,313,314}-{lint,flake8},lint: {[testenv:flake8]commands}
#ignore_outcome =
# type: true