ssh-audit.py
Аудит конфигурации SSH-сервера: проверяет sshd_config на небезопасные настройки, слабые алгоритмы шифрования, устаревшие протоколы. Выдаёт отчёт с рекомендациями по hardening. Полезно после настройки нового сервера.
Зависимости
- Python 3.9+
- Доступ на чтение
/etc/ssh/sshd_config
Использование
# Аудит конфига по умолчанию sudo python3 ssh-audit.py # Аудит произвольного файла python3 ssh-audit.py --config /path/to/sshd_config # Вывод только проблем (без OK) sudo python3 ssh-audit.py --only-issues
Код
#!/usr/bin/env python3
"""ssh-audit.py — аудит конфигурации sshd_config."""
import argparse
import sys
from pathlib import Path
# Правила: (параметр, небезопасное_значение, рекомендация)
RULES = [
("PermitRootLogin", "yes", "Установите PermitRootLogin no или prohibit-password"),
("PasswordAuthentication", "yes", "Отключите пароли: PasswordAuthentication no"),
("PermitEmptyPasswords", "yes", "Установите PermitEmptyPasswords no"),
("X11Forwarding", "yes", "Отключите X11Forwarding, если не используется"),
("UsePAM", "no", "Рекомендуется UsePAM yes для корректной работы аутентификации"),
("MaxAuthTries", None, "Установите MaxAuthTries 3-5 для защиты от brute force"),
("LoginGraceTime", None, "Установите LoginGraceTime 30-60 секунд"),
("ClientAliveInterval", None, "Установите ClientAliveInterval 300 для отключения неактивных сессий"),
("AllowUsers", None, "Ограничьте доступ через AllowUsers или AllowGroups"),
]
WEAK_CIPHERS = ["3des-cbc", "aes128-cbc", "aes192-cbc", "aes256-cbc", "blowfish-cbc"]
WEAK_MACS = ["hmac-md5", "hmac-sha1", "umac-64"]
def parse_config(path: str) -> dict[str, str]:
"""Парсит sshd_config, возвращает {параметр: значение}."""
config = {}
for line in Path(path).read_text().splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
parts = line.split(None, 1)
if len(parts) == 2:
config[parts[0]] = parts[1]
return config
def main():
parser = argparse.ArgumentParser(description="Аудит sshd_config")
parser.add_argument("--config", default="/etc/ssh/sshd_config", help="Путь к конфигу")
parser.add_argument("--only-issues", action="store_true", help="Показать только проблемы")
args = parser.parse_args()
if not Path(args.config).exists():
print(f"Ошибка: файл {args.config} не найден", file=sys.stderr)
sys.exit(1)
config = parse_config(args.config)
issues = 0
print(f"\n ═══ SSH Audit: {args.config} ═══\n")
for param, bad_value, recommendation in RULES:
value = config.get(param)
if bad_value and value and value.lower() == bad_value.lower():
print(f" ✗ {param} = {value}")
print(f" → {recommendation}\n")
issues += 1
elif bad_value is None and value is None:
if not args.only_issues:
print(f" ⚠ {param} не задан")
print(f" → {recommendation}\n")
issues += 1
elif not args.only_issues:
print(f" ✓ {param} = {value or '(по умолчанию)'}")
# Проверка шифров
ciphers = config.get("Ciphers", "")
for weak in WEAK_CIPHERS:
if weak in ciphers.lower():
print(f" ✗ Слабый шифр: {weak}")
issues += 1
macs = config.get("MACs", "")
for weak in WEAK_MACS:
if weak in macs.lower():
print(f" ✗ Слабый MAC: {weak}")
issues += 1
print(f"\n ───────────────────────")
if issues == 0:
print(f" ✓ Проблем не обнаружено")
else:
print(f" Найдено проблем: {issues}")
if __name__ == "__main__":
main()