Skip to main content

sshd_lint

A zero-dependency static analyzer for OpenSSH sshd_config files. Audits configs offline โ€” no root, no network, no live server needed.

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.


๐Ÿ’ก Motivation

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.

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.


๐Ÿค– AI-Assisted Project

This tool was conceptualized and developed with the assistance of Artificial Intelligence.
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.


โœจ Features

  • Zero Runtime Dependencies โ€” Built entirely on the Python standard library. Installs with a single pip install, then runs with nothing else โ€” no third-party packages pulled in.
  • Context-Aware Parsing โ€” Understands OpenSSH semantics: Match blocks, Match All reset, directive shadowing, and Include glob expansion.
  • Scoped Match Block Findings โ€” Distinguishes between global misconfigurations and risks that apply only to specific users, addresses, or groups.
  • Duplicate Directive Detection โ€” Warns when a directive appears more than once globally, since sshd silently uses only the first occurrence.
  • Detailed, Actionable Reports โ€” Explains what is wrong, why it matters, and references the relevant standard.
  • CI/CD Ready โ€” Structured JSON output, and exit codes that separate a security verdict from an operational failure.
  • Version-Aware Rules โ€” Adjusts expectations based on target OpenSSH version.

๐Ÿ“ฆ Requirements

  • Python 3.9+
  • No external packages โ€” only the standard library

โš™๏ธ Installation

Via pip (recommended)

pip install sshd-lint

This installs the sshd-lint command on your PATH, so you can run it from anywhere:

sshd-lint /etc/ssh/sshd_config

From source (for development)

Clone the repository and install it in editable mode:

git clone https://github.com/capitan0n/sshd-lint.git
cd sshd-lint
pip install -e .
sshd-lint /etc/ssh/sshd_config

Or run it as a module without installing, straight from the source tree:

git clone https://github.com/capitan0n/sshd-lint.git
cd sshd-lint/src
python -m sshd_lint /etc/ssh/sshd_config

๐Ÿ“‹ Sample Output

$ sshd-lint /etc/ssh/sshd_config --severity medium --compact

sshd_lint 1.4.0 โ€” /etc/ssh/sshd_config
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
Findings: 5  CRITICAL: 1  HIGH: 1  MEDIUM: 3
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

[CRITICAL] PermitRootLogin (line 2)
  Current value : yes
  Issue         : Root login over SSH is permitted.

[HIGH] PasswordAuthentication (line 3)
  Current value : yes
  Issue         : Password authentication is enabled.

[MEDIUM] MaxAuthTries (line 5)
  Current value : 8
  Issue         : MaxAuthTries is 8 โ€” recommended โ‰ค 4.

[MEDIUM] AllowUsers / AllowGroups
  Current value : <not set>
  Issue         : No user or group allowlist is defined.

[MEDIUM] X11Forwarding (line 4)
  Current value : yes
  Issue         : X11 forwarding is enabled.

Without --compact, each finding also carries a Why it matters explanation and the standards it references. Colors are enabled automatically when writing to a terminal and disabled when piping or redirecting.


๐Ÿš€ Usage

Analyze the default system SSH config:

sshd-lint

Analyze a specific file:

sshd-lint /path/to/sshd_config

Filter by severity (only HIGH and above):

sshd-lint --severity high

Compact output (hides explanations โ€” useful for quick scans):

sshd-lint --compact

JSON output for pipeline integration:

sshd-lint --format json

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:

{
  "exit_code": 2,
  "summary": {
    "CRITICAL": 1,
    "HIGH": 1,
    "MEDIUM": 0,
    "LOW": 0,
    "INFO": 0
  },
  "findings": [
    {
      "severity": "HIGH",
      "directive": "PasswordAuthentication",
      "value": "yes",
      "line": 3,
      "scope": "global",
      "message": "Password authentication is enabled.",
      "detail": "Password authentication is vulnerable to brute-force and credential-stuffing attacks. Disable it and use public-key authentication exclusively: 'PasswordAuthentication no'.",
      "references": ["CIS Benchmark for Linux", "Mozilla OpenSSH Guidelines"]
    }
  ]
}

Note that exit_code in the JSON only ever carries the findings verdict (0, 1 or 2). Operational failures โ€” a bad flag or a missing config file โ€” produce no JSON at all, so a document that parses is always a real report.

Filter with jq:

# Only CRITICAL findings
sshd-lint --format json | jq '.findings[] | select(.severity == "CRITICAL")'

# Summary only
sshd-lint --format json | jq '.summary'

# Read exit code from JSON instead of $?
sshd-lint --format json | jq '.exit_code'

Audit a config copied from a remote server, resolving Includes from the live system:

scp user@server:/etc/ssh/sshd_config /tmp/audit/sshd_config
sshd-lint /tmp/audit/sshd_config --base-dir /etc/ssh

๐Ÿงฐ CLI Flags

Flag Short Description
config โ€” Path to sshd_config (default: /etc/ssh/sshd_config)
--severity -s Minimum severity: critical, high, medium, low, info (default: info)
--format -f Output format: text or json (default: text)
--compact -c Hide explanations and references for cleaner output
--no-color โ€” Disable ANSI colors
--openssh-version โ€” Target OpenSSH version (e.g. 8.9) for version-aware rule adjustments
--base-dir โ€” Base directory for Include resolution. Default: same directory as the config file
--help -h Show usage and exit
--version -V Show version and exit

-V is used for --version so that -v stays free for a future verbosity flag, following the common convention where -v means verbose.


๐Ÿšฆ Exit Codes

Exit codes fall into two groups. 0โ€“2 are the security verdict; 64 and 66 signal that the tool could not run at all. Keeping them separate means a typo in a flag can never be mistaken by a pipeline for a critical finding.

Code Meaning
0 No findings at or above the requested severity threshold
1 Findings exist, but none are HIGH or CRITICAL
2 At least one HIGH or CRITICAL finding โ€” pipeline should fail
64 Usage error โ€” unrecognized flag or invalid argument (EX_USAGE)
66 Config file not found or unreadable (EX_NOINPUT)

64 and 66 follow the conventional values from BSD sysexits.h.

When using --format json, the verdict is also embedded in the JSON output as exit_code, so the report is fully self-contained and readable by downstream tools without capturing $?.

GitHub Actions

CI systems treat any non-zero exit code as failure, which would collapse the distinction between 1 and 2. Translate the verdict explicitly:

- name: Install sshd-lint
  run: pip install sshd-lint

- name: Lint SSH config
  run: |
    code=0
    sshd-lint /etc/ssh/sshd_config --format json > report.json || code=$?
    cat report.json
    # Fail only on HIGH/CRITICAL. Exit 1 = minor findings, informational.
    if [ "$code" -ge 2 ]; then
      echo "::error::HIGH or CRITICAL findings in sshd_config"
      exit 1
    fi

|| code=$? does two jobs: it captures the exit code, and it stops bash -e (the default shell for run: steps) from aborting the script the moment the linter returns non-zero. cat must come after the capture, since $? only holds the status of the most recent command.


๐Ÿงช Rules Evaluated

Parse & File Handling

  • Rule 00: Include resolution problems โ€” unreadable files, an Include glob matching more than 500 files (refused outright), an Include glob matching 0 files, and lines that couldn't be parsed as a directive

Authentication

  • Rule 01: PermitRootLogin
  • Rule 02: PasswordAuthentication
  • Rule 03: PermitEmptyPasswords
  • Rule 04: ChallengeResponseAuthentication
  • Rule 05: PubkeyAuthentication
  • Rule 06: HostbasedAuthentication / IgnoreRhosts

Access Control

  • Rule 10: LoginGraceTime
  • Rule 11: MaxAuthTries
  • Rule 12: MaxSessions
  • Rule 13: MaxStartups
  • Rule 14: AllowUsers / AllowGroups

Forwarding & Tunneling

  • Rule 20: X11Forwarding
  • Rule 21: AllowTcpForwarding
  • Rule 22: AllowAgentForwarding
  • Rule 23: GatewayPorts
  • Rule 24: PermitTunnel

Logging & Auditing

  • Rule 30: LogLevel
  • Rule 31: PrintLastLog

Cryptography

  • Rule 40: Weak or deprecated Ciphers
  • Rule 41: Weak or deprecated MACs
  • Rule 42: Weak KexAlgorithms (key exchange)
  • Rule 43: Deprecated HostKeyAlgorithms
  • Rule 44: Deprecated PubkeyAcceptedAlgorithms

Rules 40-44 understand OpenSSH's +/-/^ default-set syntax (e.g. Ciphers +arcfour appends to the compiled-in default rather than replacing it) โ€” a weak algorithm is flagged whether it fully replaces the list or is merely appended to it.

Miscellaneous

  • Rule 50: Banner
  • Rule 51: StrictModes
  • Rule 52: Port (default port 22)
  • Rule 53: ClientAliveInterval / idle session timeout
  • Rule 54: UseDNS
  • Rule 55: Insecure directives inside Match blocks (scoped risk)
  • Rule 56: Duplicate global directives (shadowed by sshd)
  • Rule 57: PermitUserEnvironment

๐Ÿ” How sshd_lint differs from similar tools

Tool How it works Requires root / live system
Lynis Runs live on the system, audits many aspects Yes
ssh-audit Connects to a live SSH server, tests its responses Yes (network access)
sshd_lint Reads the config file statically, offline No

sshd_lint is designed for offline auditing, CI/CD pipelines, and reviewing configs from remote systems without needing access to the live server.


โš ๏ธ Limitations

  • Static analysis only โ€” does not connect to a live server or test actual behaviour.
  • 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.
  • Include resolution requires filesystem access โ€” unreadable files and Include globs matching over 500 files are reported as CRITICAL; a glob matching 0 files is reported as INFO (often benign โ€” e.g. an empty sshd_config.d/ โ€” but worth a glance).
  • Relative Include paths resolve against --base-dir / the config file's own directory, not real sshd's hardcoded /etc/ssh. For the common case of auditing the live /etc/ssh/sshd_config these are identical. When auditing a copy of only the main file (without its snippets alongside it), pass --base-dir /etc/ssh to resolve includes the way sshd itself would.
  • 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.
  • Compiled-in defaults are for OpenSSH 8.x โ€” behaviour on significantly older or newer versions may differ.

Author


๐Ÿ“œ License

MIT License

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sshd_lint-1.4.1.tar.gz (24.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sshd_lint-1.4.1-py3-none-any.whl (23.8 kB view details)

Uploaded Python 3

File details

Details for the file sshd_lint-1.4.1.tar.gz.

File metadata

  • Download URL: sshd_lint-1.4.1.tar.gz
  • Upload date:
  • Size: 24.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.6

File hashes

Hashes for sshd_lint-1.4.1.tar.gz
Algorithm Hash digest
SHA256 ab51dfaebead84154b9312c614df8e164014f7304a23a5f4fe5518946b259c3b
MD5 554ea82d48aaf83409ec46ad43d2c67f
BLAKE2b-256 ce9bb4f19d66663c31b852d9b91603d95037114f381e81ddcfee850bc2dea295

See more details on using hashes here.

File details

Details for the file sshd_lint-1.4.1-py3-none-any.whl.

File metadata

  • Download URL: sshd_lint-1.4.1-py3-none-any.whl
  • Upload date:
  • Size: 23.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.6

File hashes

Hashes for sshd_lint-1.4.1-py3-none-any.whl
Algorithm Hash digest
SHA256 3ec3570ccb23005ca373a39f527d82ed0ec2ad353087e9649726544d5dffd77b
MD5 ba48694e296856dad9f8adcb375265ce
BLAKE2b-256 dbdcf1bc4ebdd99efe06b9de188b7da02b68597e8b5372622b2703599af00811

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page