sshd-lint 1.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sshd_lint-1.4.0/.gitignore +13 -0
- sshd_lint-1.4.0/LICENSE +22 -0
- sshd_lint-1.4.0/PKG-INFO +366 -0
- sshd_lint-1.4.0/README.md +343 -0
- sshd_lint-1.4.0/pyproject.toml +39 -0
- sshd_lint-1.4.0/samples/chaos.conf +62 -0
- sshd_lint-1.4.0/samples/hardened.conf +61 -0
- sshd_lint-1.4.0/samples/realistic.conf +40 -0
- sshd_lint-1.4.0/src/sshd_lint/__init__.py +1532 -0
- sshd_lint-1.4.0/src/sshd_lint/__main__.py +4 -0
sshd_lint-1.4.0/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 capitan0n
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
sshd_lint-1.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sshd-lint
|
|
3
|
+
Version: 1.4.0
|
|
4
|
+
Summary: Zero-dependency static analyzer for OpenSSH sshd_config files
|
|
5
|
+
Project-URL: Homepage, https://github.com/capitan0n/sshd-lint
|
|
6
|
+
Project-URL: Repository, https://github.com/capitan0n/sshd-lint
|
|
7
|
+
Project-URL: Issues, https://github.com/capitan0n/sshd-lint/issues
|
|
8
|
+
Author: capitan0n
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: cis,devsecops,hardening,linter,security,ssh,sshd,static-analysis
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Information Technology
|
|
15
|
+
Classifier: Intended Audience :: System Administrators
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Topic :: Security
|
|
20
|
+
Classifier: Topic :: System :: Systems Administration
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# sshd_lint
|
|
25
|
+
|
|
26
|
+
A zero-dependency static analyzer for OpenSSH `sshd_config` files. Audits configs
|
|
27
|
+
offline โ no root, no network, no live server needed.
|
|
28
|
+
|
|
29
|
+
`sshd_lint` evaluates your SSH server configuration against industry-standard security baselines, including the **CIS Benchmark for Linux**, **Mozilla OpenSSH Guidelines**, and **NIST SP 800-53**.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## ๐ก Motivation
|
|
34
|
+
|
|
35
|
+
This project was born out of a recurring practical need: quickly auditing an SSH server configuration whenever setting up a new test VM or reviewing a production host.
|
|
36
|
+
|
|
37
|
+
Existing tools either require root on the live system or an open network connection to the server. `sshd_lint` takes the opposite approach โ it reads the config file and nothing else, which means it works on a copy pulled from a machine you cannot log into, inside a container image, or in a CI pipeline where no SSH server is running at all.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## ๐ค AI-Assisted Project
|
|
42
|
+
|
|
43
|
+
This tool was conceptualized and developed with the assistance of Artificial Intelligence.
|
|
44
|
+
AI was used for code generation, logic refinement, and edge-case handling (such as cumulative directives and `Match` block scoping), under human direction and review.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## โจ Features
|
|
49
|
+
|
|
50
|
+
- **Zero Dependencies** โ Built entirely on the Python standard library. No `pip install`.
|
|
51
|
+
- **Context-Aware Parsing** โ Understands OpenSSH semantics: `Match` blocks, `Match All` reset, directive shadowing, and `Include` glob expansion.
|
|
52
|
+
- **Scoped Match Block Findings** โ Distinguishes between global misconfigurations and risks that apply only to specific users, addresses, or groups.
|
|
53
|
+
- **Duplicate Directive Detection** โ Warns when a directive appears more than once globally, since sshd silently uses only the first occurrence.
|
|
54
|
+
- **Detailed, Actionable Reports** โ Explains what is wrong, why it matters, and references the relevant standard.
|
|
55
|
+
- **CI/CD Ready** โ Structured JSON output, and exit codes that separate a security verdict from an operational failure.
|
|
56
|
+
- **Version-Aware Rules** โ Adjusts expectations based on target OpenSSH version.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## ๐ฆ Requirements
|
|
61
|
+
|
|
62
|
+
- Python **3.9+**
|
|
63
|
+
- No external packages
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## โ๏ธ Installation
|
|
68
|
+
|
|
69
|
+
Since there are no external dependencies, you can run it directly:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
wget https://raw.githubusercontent.com/capitan0n/sshd-lint/main/sshd_lint.py -O sshd_lint
|
|
73
|
+
chmod +x sshd_lint
|
|
74
|
+
./sshd_lint
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Or clone the repository:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
git clone https://github.com/capitan0n/sshd-lint.git
|
|
81
|
+
cd sshd-lint
|
|
82
|
+
python sshd_lint.py
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## ๐ Sample Output
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
$ sshd_lint /etc/ssh/sshd_config --severity medium --compact
|
|
91
|
+
|
|
92
|
+
sshd_lint 1.4.0 โ /etc/ssh/sshd_config
|
|
93
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
94
|
+
Findings: 5 CRITICAL: 1 HIGH: 1 MEDIUM: 3
|
|
95
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
96
|
+
|
|
97
|
+
[CRITICAL] PermitRootLogin (line 2)
|
|
98
|
+
Current value : yes
|
|
99
|
+
Issue : Root login over SSH is permitted.
|
|
100
|
+
|
|
101
|
+
[HIGH] PasswordAuthentication (line 3)
|
|
102
|
+
Current value : yes
|
|
103
|
+
Issue : Password authentication is enabled.
|
|
104
|
+
|
|
105
|
+
[MEDIUM] MaxAuthTries (line 5)
|
|
106
|
+
Current value : 8
|
|
107
|
+
Issue : MaxAuthTries is 8 โ recommended โค 4.
|
|
108
|
+
|
|
109
|
+
[MEDIUM] AllowUsers / AllowGroups
|
|
110
|
+
Current value : <not set>
|
|
111
|
+
Issue : No user or group allowlist is defined.
|
|
112
|
+
|
|
113
|
+
[MEDIUM] X11Forwarding (line 4)
|
|
114
|
+
Current value : yes
|
|
115
|
+
Issue : X11 forwarding is enabled.
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Without `--compact`, each finding also carries a **Why it matters** explanation and the
|
|
119
|
+
standards it references. Colors are enabled automatically when writing to a terminal and
|
|
120
|
+
disabled when piping or redirecting.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## ๐ Usage
|
|
125
|
+
|
|
126
|
+
Analyze the default system SSH config:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
python sshd_lint.py
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Analyze a specific file:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
python sshd_lint.py /path/to/sshd_config
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Filter by severity (only HIGH and above):
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
python sshd_lint.py --severity high
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Compact output (hides explanations โ useful for quick scans):
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
python sshd_lint.py --compact
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
JSON output for pipeline integration:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
python sshd_lint.py --format json
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
The JSON output is self-contained โ `exit_code` and a per-severity `summary` are included at the top level so consumers don't need to capture `$?` separately:
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"exit_code": 2,
|
|
161
|
+
"summary": {
|
|
162
|
+
"CRITICAL": 1,
|
|
163
|
+
"HIGH": 1,
|
|
164
|
+
"MEDIUM": 0,
|
|
165
|
+
"LOW": 0,
|
|
166
|
+
"INFO": 0
|
|
167
|
+
},
|
|
168
|
+
"findings": [
|
|
169
|
+
{
|
|
170
|
+
"severity": "HIGH",
|
|
171
|
+
"directive": "PasswordAuthentication",
|
|
172
|
+
"value": "yes",
|
|
173
|
+
"line": 3,
|
|
174
|
+
"scope": "global",
|
|
175
|
+
"message": "Password authentication is enabled.",
|
|
176
|
+
"detail": "Password authentication is vulnerable to brute-force and credential-stuffing attacks. Disable it and use public-key authentication exclusively: 'PasswordAuthentication no'.",
|
|
177
|
+
"references": ["CIS Benchmark for Linux", "Mozilla OpenSSH Guidelines"]
|
|
178
|
+
}
|
|
179
|
+
]
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Note that `exit_code` in the JSON only ever carries the findings verdict (`0`, `1` or `2`).
|
|
184
|
+
Operational failures โ a bad flag or a missing config file โ produce no JSON at all, so a
|
|
185
|
+
document that parses is always a real report.
|
|
186
|
+
|
|
187
|
+
Filter with `jq`:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
# Only CRITICAL findings
|
|
191
|
+
python sshd_lint.py --format json | jq '.findings[] | select(.severity == "CRITICAL")'
|
|
192
|
+
|
|
193
|
+
# Summary only
|
|
194
|
+
python sshd_lint.py --format json | jq '.summary'
|
|
195
|
+
|
|
196
|
+
# Read exit code from JSON instead of $?
|
|
197
|
+
python sshd_lint.py --format json | jq '.exit_code'
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Audit a config copied from a remote server, resolving Includes from the live system:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
scp user@server:/etc/ssh/sshd_config /tmp/audit/sshd_config
|
|
204
|
+
python sshd_lint.py /tmp/audit/sshd_config --base-dir /etc/ssh
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## ๐งฐ CLI Flags
|
|
210
|
+
|
|
211
|
+
| Flag | Short | Description |
|
|
212
|
+
|-----------------------|-------|-------------|
|
|
213
|
+
| `config` | โ | Path to sshd_config (default: `/etc/ssh/sshd_config`) |
|
|
214
|
+
| `--severity` | `-s` | Minimum severity: `critical`, `high`, `medium`, `low`, `info` (default: `info`) |
|
|
215
|
+
| `--format` | `-f` | Output format: `text` or `json` (default: `text`) |
|
|
216
|
+
| `--compact` | `-c` | Hide explanations and references for cleaner output |
|
|
217
|
+
| `--no-color` | โ | Disable ANSI colors |
|
|
218
|
+
| `--openssh-version` | โ | Target OpenSSH version (e.g. `8.9`) for version-aware rule adjustments |
|
|
219
|
+
| `--base-dir` | โ | Base directory for `Include` resolution. Default: same directory as the config file |
|
|
220
|
+
| `--help` | `-h` | Show usage and exit |
|
|
221
|
+
| `--version` | `-V` | Show version and exit |
|
|
222
|
+
|
|
223
|
+
> `-V` is used for `--version` so that `-v` stays free for a future verbosity flag,
|
|
224
|
+
> following the common convention where `-v` means verbose.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## ๐ฆ Exit Codes
|
|
229
|
+
|
|
230
|
+
Exit codes fall into two groups. `0`โ`2` are the **security verdict**; `64` and `66`
|
|
231
|
+
signal that the tool could not run at all. Keeping them separate means a typo in a flag
|
|
232
|
+
can never be mistaken by a pipeline for a critical finding.
|
|
233
|
+
|
|
234
|
+
| Code | Meaning |
|
|
235
|
+
|------|---------|
|
|
236
|
+
| `0` | No findings at or above the requested severity threshold |
|
|
237
|
+
| `1` | Findings exist, but none are HIGH or CRITICAL |
|
|
238
|
+
| `2` | At least one HIGH or CRITICAL finding โ pipeline should fail |
|
|
239
|
+
| `64` | Usage error โ unrecognized flag or invalid argument (`EX_USAGE`) |
|
|
240
|
+
| `66` | Config file not found or unreadable (`EX_NOINPUT`) |
|
|
241
|
+
|
|
242
|
+
`64` and `66` follow the conventional values from BSD `sysexits.h`.
|
|
243
|
+
|
|
244
|
+
When using `--format json`, the verdict is also embedded in the JSON output as
|
|
245
|
+
`exit_code`, so the report is fully self-contained and readable by downstream tools
|
|
246
|
+
without capturing `$?`.
|
|
247
|
+
|
|
248
|
+
### GitHub Actions
|
|
249
|
+
|
|
250
|
+
CI systems treat **any** non-zero exit code as failure, which would collapse the
|
|
251
|
+
distinction between `1` and `2`. Translate the verdict explicitly:
|
|
252
|
+
|
|
253
|
+
```yaml
|
|
254
|
+
- name: Lint SSH config
|
|
255
|
+
run: |
|
|
256
|
+
code=0
|
|
257
|
+
python sshd_lint.py /etc/ssh/sshd_config --format json > report.json || code=$?
|
|
258
|
+
cat report.json
|
|
259
|
+
# Fail only on HIGH/CRITICAL. Exit 1 = minor findings, informational.
|
|
260
|
+
if [ "$code" -ge 2 ]; then
|
|
261
|
+
echo "::error::HIGH or CRITICAL findings in sshd_config"
|
|
262
|
+
exit 1
|
|
263
|
+
fi
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
`|| code=$?` does two jobs: it captures the exit code, and it stops `bash -e` (the default
|
|
267
|
+
shell for `run:` steps) from aborting the script the moment the linter returns non-zero.
|
|
268
|
+
`cat` must come *after* the capture, since `$?` only holds the status of the most recent
|
|
269
|
+
command.
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## ๐งช Rules Evaluated
|
|
274
|
+
|
|
275
|
+
### Parse & File Handling
|
|
276
|
+
- Rule 00: Include resolution problems โ unreadable files, an Include glob matching
|
|
277
|
+
more than 500 files (refused outright), an Include glob matching 0 files, and lines
|
|
278
|
+
that couldn't be parsed as a directive
|
|
279
|
+
|
|
280
|
+
### Authentication
|
|
281
|
+
- Rule 01: `PermitRootLogin`
|
|
282
|
+
- Rule 02: `PasswordAuthentication`
|
|
283
|
+
- Rule 03: `PermitEmptyPasswords`
|
|
284
|
+
- Rule 04: `ChallengeResponseAuthentication`
|
|
285
|
+
- Rule 05: `PubkeyAuthentication`
|
|
286
|
+
- Rule 06: `HostbasedAuthentication` / `IgnoreRhosts`
|
|
287
|
+
|
|
288
|
+
### Access Control
|
|
289
|
+
- Rule 10: `LoginGraceTime`
|
|
290
|
+
- Rule 11: `MaxAuthTries`
|
|
291
|
+
- Rule 12: `MaxSessions`
|
|
292
|
+
- Rule 13: `MaxStartups`
|
|
293
|
+
- Rule 14: `AllowUsers` / `AllowGroups`
|
|
294
|
+
|
|
295
|
+
### Forwarding & Tunneling
|
|
296
|
+
- Rule 20: `X11Forwarding`
|
|
297
|
+
- Rule 21: `AllowTcpForwarding`
|
|
298
|
+
- Rule 22: `AllowAgentForwarding`
|
|
299
|
+
- Rule 23: `GatewayPorts`
|
|
300
|
+
- Rule 24: `PermitTunnel`
|
|
301
|
+
|
|
302
|
+
### Logging & Auditing
|
|
303
|
+
- Rule 30: `LogLevel`
|
|
304
|
+
- Rule 31: `PrintLastLog`
|
|
305
|
+
|
|
306
|
+
### Cryptography
|
|
307
|
+
- Rule 40: Weak or deprecated Ciphers
|
|
308
|
+
- Rule 41: Weak or deprecated MACs
|
|
309
|
+
- Rule 42: Weak KexAlgorithms (key exchange)
|
|
310
|
+
- Rule 43: Deprecated HostKeyAlgorithms
|
|
311
|
+
- Rule 44: Deprecated `PubkeyAcceptedAlgorithms`
|
|
312
|
+
|
|
313
|
+
Rules 40-44 understand OpenSSH's `+`/`-`/`^` default-set syntax (e.g. `Ciphers +arcfour`
|
|
314
|
+
appends to the compiled-in default rather than replacing it) โ a weak algorithm is flagged
|
|
315
|
+
whether it fully replaces the list or is merely appended to it.
|
|
316
|
+
|
|
317
|
+
### Miscellaneous
|
|
318
|
+
- Rule 50: `Banner`
|
|
319
|
+
- Rule 51: `StrictModes`
|
|
320
|
+
- Rule 52: `Port` (default port 22)
|
|
321
|
+
- Rule 53: `ClientAliveInterval` / idle session timeout
|
|
322
|
+
- Rule 54: `UseDNS`
|
|
323
|
+
- Rule 55: Insecure directives inside `Match` blocks (scoped risk)
|
|
324
|
+
- Rule 56: Duplicate global directives (shadowed by sshd)
|
|
325
|
+
- Rule 57: `PermitUserEnvironment`
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## ๐ How sshd_lint differs from similar tools
|
|
330
|
+
|
|
331
|
+
| Tool | How it works | Requires root / live system |
|
|
332
|
+
|------|--------------|-----------------------------|
|
|
333
|
+
| **Lynis** | Runs live on the system, audits many aspects | Yes |
|
|
334
|
+
| **ssh-audit** | Connects to a live SSH server, tests its responses | Yes (network access) |
|
|
335
|
+
| **sshd_lint** | Reads the config file statically, offline | No |
|
|
336
|
+
|
|
337
|
+
`sshd_lint` is designed for offline auditing, CI/CD pipelines, and reviewing configs from remote systems without needing access to the live server.
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## โ ๏ธ Limitations
|
|
342
|
+
|
|
343
|
+
- **Static analysis only** โ does not connect to a live server or test actual behaviour.
|
|
344
|
+
- **Match block conditions are not evaluated** โ the condition string (e.g. `User anoncvs`, `Address 10.0.0.0/8`) is recorded and reported, but sshd_lint cannot determine whether it applies to a given connection.
|
|
345
|
+
- **Include resolution requires filesystem access** โ unreadable files and Include
|
|
346
|
+
globs matching over 500 files are reported as CRITICAL; a glob matching 0 files is
|
|
347
|
+
reported as INFO (often benign โ e.g. an empty `sshd_config.d/` โ but worth a glance).
|
|
348
|
+
- **Relative `Include` paths resolve against `--base-dir` / the config file's own
|
|
349
|
+
directory, not real sshd's hardcoded `/etc/ssh`.** For the common case of auditing
|
|
350
|
+
the live `/etc/ssh/sshd_config` these are identical. When auditing a copy of only the
|
|
351
|
+
main file (without its snippets alongside it), pass `--base-dir /etc/ssh` to resolve
|
|
352
|
+
includes the way sshd itself would.
|
|
353
|
+
- **Version-aware rules are currently minimal** โ the `--openssh-version` flag adjusts a small number of known defaults; more version-specific rules may be added in future releases.
|
|
354
|
+
- **Compiled-in defaults are for OpenSSH 8.x** โ behaviour on significantly older or newer versions may differ.
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## Author
|
|
359
|
+
|
|
360
|
+
* **capitan0n** - [capitan0n](https://github.com/capitan0n)
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## ๐ License
|
|
365
|
+
|
|
366
|
+
MIT License
|