Linux服务器SSH安全实战:从sshd:notty日志分析到自动化封禁

📅 2026/7/8 5:35:35
Linux服务器SSH安全实战:从sshd:notty日志分析到自动化封禁
1. 项目概述当你的Linux服务器开始“说话”如果你管理过暴露在公网的Linux服务器尤其是那些开了SSH服务的那么对/var/log/secure或/var/log/auth.log里刷屏的登录失败记录一定不会陌生。但有一种情况它不像“Failed password”那样直接宣告失败也不像“Accepted password”那样代表成功登录它更像一个沉默的观察者记录着一种“非典型”的会话——这就是sshd:notty。这个标题“sshd:notty告警分析从日志识别到自动化封禁的Linux防御实战”精准地指向了一个从被动监控到主动防御的完整闭环。它不仅仅是看日志而是从海量的、看似无害的notty记录中提炼出攻击者的行为指纹并最终通过自动化手段将其拒之门外。对于任何一位服务器管理员或安全运维人员来说这都是一项从“消防员”转向“城市规划师”的关键技能。你不再只是疲于奔命地处理一个个已发生的攻击而是开始构建一套能够自动识别威胁、并提前布防的智能体系。简单来说sshd:notty日志条目意味着有人通过SSH连接到了你的服务器但并没有分配一个交互式的终端tty。这本身是一个中性事件可能是正常的SCP文件传输、SFTP连接、或者通过ssh远程执行单条命令如ssh userhost ‘ls -l’。然而在攻击者的剧本里notty会话常常是自动化扫描和暴力破解的“标准动作”。攻击工具如Hydra, Medusa在尝试爆破时通常不会请求交互式终端以节省资源和避免留下完整的会话痕迹。因此高频、规律、来自单一源IP的大量notty连接尝试就是非常清晰的恶意信号。本实战指南就是带你一步步拆解这个信号。我们将从读懂一行notty日志背后的每一个字段开始到编写脚本从日志海洋中精准捕捞恶意IP最后构建一个自动化的“观察-判定-封禁”系统。整个过程我会穿插我多年来在运维一线踩过的坑和总结的技巧目标是让你不仅能复现这个方案更能理解其设计背后的“为什么”从而灵活应用到更广阔的安全运维场景中。2. 核心思路与架构设计为什么是“日志-情报-动作”面对安全威胁一个健壮的防御体系不应该依赖于人工的实时响应。人的反应有延迟会疲劳会疏忽。自动化防御的核心思路是将安全运维人员的经验、策略和判断沉淀为可重复、可扩展、且7x24小时无休的程序逻辑。我们针对sshd:notty告警的自动化封禁方案正是这一思路的典型实践。整个架构可以清晰地划分为三个层次数据采集层、分析决策层和执行响应层。这三层环环相扣构成了一个完整的“OODA循环”观察、定向、决策、行动。2.1 数据采集层不仅仅是收集日志这一层的目标是获取原始、完整的安全事件数据。对于sshd:notty数据源就是SSH服务的日志。在RHEL/CentOS等系统中它通常位于/var/log/secure在Debian/Ubuntu中则是/var/log/auth.log。注意很多新手会直接去tail -f这个文件然后写个grep脚本就开始了。这在小规模或临时分析时没问题但对于构建一个可持续的自动化系统这是不够的。你需要考虑日志轮转logrotate、历史数据回溯、以及多台服务器的日志集中化。为什么需要集中化日志单台服务器的视角是狭隘的。攻击者往往采用“低慢小”的策略对同一内网或同一业务集群的多台服务器进行分布式、低速率的扫描。如果每台服务器独立分析单个IP的尝试频率可能达不到阈值从而被放过。但将几十台服务器的日志汇集起来看这个IP的恶意行为就无所遁形了。因此在生产环境中我强烈建议使用rsyslog或syslog-ng将关键日志实时转发到一个中央日志服务器或者直接接入ELKElasticsearch, Logstash, Kibana、Graylog等日志管理平台。我们本次实战为了聚焦核心逻辑会以单机日志文件为例但我会在关键步骤指出如何适配集中化日志架构。2.2 分析决策层从噪声中提取信号这是整个系统的“大脑”也是最体现技术含量的部分。它的任务是从采集到的原始日志中识别出恶意的notty连接模式。分析决策的核心是制定检测规则。一个简单的规则可能是“过去5分钟内来自同一源IP的notty连接失败次数超过10次”。但这太粗糙了。在实际对抗中我们需要更精细的策略行为模式识别恶意扫描不仅仅是“失败”。它可能表现为高频次短时间内大量连接尝试。规律性固定时间间隔如每秒一次的连接。用户枚举针对同一个IP尝试不同的系统用户名root, admin, test, ubuntu等。协议级特征连接建立后立即断开没有进行任何实际的身份验证交互这可以从日志的时间戳和进程号变化判断。关联分析结合其他日志条目。一个IP如果同时产生了大量的notty连接和Failed password记录那它的恶意概率就极高。我们的分析脚本应该能关联这些信息。阈值与时间窗口的动态调整固定的阈值如10次/5分钟可能误伤正常的管理操作比如自动化部署工具也可能漏过高明的慢速扫描。一个更优的策略是使用滑动时间窗口并可能引入更复杂的算法如基于历史基线动态调整阈值或者使用简单的评分系统不同行为赋予不同权重总分超过阈值则判定为恶意。在本实战中我们将从一个经典且有效的规则开始“在最近10分钟内来自同一IP的sshd[PID]: Received disconnect from ...: 11: Bye Bye [preauth]这类notty预认证断开日志超过15条”。这个规则捕捉了攻击工具最常见的“连接-探测-断开”行为。2.3 执行响应层干净利落的处置一旦分析层判定某个IP为恶意执行层就需要采取行动。最直接有效的响应就是在网络层进行封禁使其后续的所有连接尝试在到达SSH服务之前就被丢弃。在Linux上网络层封禁的标配工具是iptables或它的现代替代品nftables。我们可以通过添加一条DROP规则来封禁IP。但是直接让分析脚本去执行iptables -A INPUT -s 恶意IP -j DROP存在风险和管理上的不便规则管理规则会不断累积需要定期清理过期封禁。持久化iptables规则重启后失效需要保存到配置中。脚本权限执行iptables命令通常需要root权限这提高了脚本的安全风险。更优雅的方案是使用ipset。ipset是iptables的扩展它允许你创建一个IP地址集合然后一条iptables规则引用整个集合。这样做的好处是高效无论集合里有10个还是10000个IPiptables规则链中只占一条匹配速度极快。易管理封禁时只需ipset add 集合名 IP解封时ipset del。可以轻松编写脚本进行动态增删。支持超时ipset支持为每个条目设置超时时间如timeout 3600一小时后自动移除非常适合临时性封禁。我们的自动化系统最终将采用分析脚本Python-生成恶意IP列表-通过ipset动态封禁的路径。同时我们会将封禁记录写入本地日志或数据库用于后续审计和策略优化。3. 日志深度解析读懂一行sshd:notty的“潜台词”在动手写代码之前我们必须成为日志的“翻译官”。一行典型的sshd:notty相关日志可能长这样Jun 24 15:33:22 server-hostname sshd[12345]: Received disconnect from 203.0.113.100 port 54321:11: Bye Bye [preauth]或者Jun 24 15:33:25 server-hostname sshd[12346]: pam_unix(sshd:auth): authentication failure; logname uid0 euid0 ttyssh ruser rhost203.0.113.100 userroot我们来拆解第一个例子这是最核心的notty扫描特征日志Jun 24 15:33:22: 时间戳。这是分析时间窗口的基础。server-hostname: 主机名。在集中化日志中用于区分源主机。sshd[12345]: 进程名和进程IDPID。注意每次新的TCP连接sshd都会fork一个子进程来处理所以PID会变化。连续的、PID快速变化的sshd子进程记录来自同一个源IP是自动化扫描的强信号。Received disconnect from 203.0.113.100 port 54321: 关键信息源IP地址203.0.113.100和源端口54321。IP是我们的追踪目标源端口通常随机意义不大。11: Bye Bye: 断开原因代码和描述。11通常代表协议错误或客户端主动断开。在[preauth]预认证阶段的Bye Bye基本意味着客户端在完成任何认证步骤之前就断开了连接——这是扫描工具探测服务是否存活的典型行为。[preauth]:黄金标签。这表示断开发生在用户认证之前。对于暴力破解工具它们往往会在尝试一个密码失败后立即断开连接并重试。因此大量[preauth]的断开日志几乎可以和恶意扫描划等号。第二行日志是认证失败日志它提供了更多上下文pam_unix(sshd:auth): authentication failure: 通过PAM模块认证失败。rhost203.0.113.100: 远程主机IP与上一条对应。userroot: 尝试登录的用户名。这里攻击者在尝试root账户。观察一段时间内一个IP是否尝试了多个不同用户名root, admin, test, oracle等是判断其为“用户枚举”攻击的重要依据。实操心得日志格式的陷阱不同Linux发行版、不同sshd版本、以及不同的syslog配置可能会导致日志格式有细微差别。例如有的日志可能没有[preauth]标签或者IP地址的格式略有不同。在编写分析脚本时使用正则表达式regex来提取字段必须足够健壮。一个过于严格的正则式可能会漏掉一部分日志。我建议在开发初期先抽取几千条真实日志样本进行测试确保你的解析规则能覆盖所有常见格式。一个更稳妥的方法是直接解析/var/log/secure或auth.log的journalctl输出journalctl -u sshd --since “1 hour ago”其格式相对更规范。4. 实战构建自动化分析封禁系统现在我们将把理论付诸实践构建一个从日志分析到自动封禁的完整系统。我们将创建一个Python脚本它定期运行例如通过cron每5分钟一次执行“分析-封禁-记录”的全流程。4.1 环境准备与工具选型系统环境主流Linux发行版均可CentOS 7/8, Ubuntu 20.04/22.04等。确保你有root或sudo权限来执行封禁操作。核心工具Python 3我们的分析脚本语言。推荐3.6及以上版本。ipsetiptables封禁执行工具。大部分系统已预装若无则通过包管理器安装yum install ipset iptables或apt install ipset iptables-persistent。systemd或cron用于配置定时任务。第一步初始化ipset和iptables规则在编写脚本前我们需要先建立封禁的“基础设施”。以下操作需要root权限# 1. 创建一个名为ssh-blocklist的ipset集合类型为hash:ip并设置默认条目超时时间为1小时3600秒 ipset create ssh-blocklist hash:ip timeout 3600 comment # 参数解释 # hash:ip存储IP地址的哈希集合类型查询效率高。 # timeout 3600每个新加入的IP将在3600秒后自动从集合中移除。这避免了永久封禁可能误伤正常IP后需要手动清理的麻烦。你可以根据攻击强度调整这个时间。 # comment允许为每个IP条目添加注释方便记录封禁原因。 # 2. 创建一条iptables规则引用这个ipset对集合内的所有IP在INPUT链的早期进行DROP操作 iptables -I INPUT -m set --match-set ssh-blocklist src -j DROP # 参数解释 # -I INPUT在INPUT链的头部插入规则确保尽早丢弃恶意流量减轻后续规则链的匹配压力。 # -m set --match-set ssh-blocklist src使用set模块匹配源地址src是否在ssh-blocklist集合中。 # -j DROP丢弃匹配的数据包不回复任何信息比REJECT更隐蔽。 # 3. (重要) 保存iptables和ipset规则使其在重启后依然生效。 # 对于CentOS/RHEL (使用iptables-services) service iptables save ipset save ssh-blocklist -f /etc/sysconfig/ipset.conf # 对于Ubuntu/Debian (使用iptables-persistent和netfilter-persistent) netfilter-persistent save # 或者手动保存ipset: ipset save /etc/iptables/ipset.conf # 并确保重启脚本能加载它通常配置在/etc/rc.local或systemd service中。重要提示在生产环境操作iptables前务必确保你当前是通过控制台或不会被规则影响的网络连接如本地终端、专用管理网络登录的。错误规则可能导致你把自己锁在外面。一个安全做法是先添加一条允许当前管理IP的规则并设置一个cron任务在5分钟后清空所有规则作为救命稻草然后再进行其他操作。4.2 核心Python脚本编写我们将脚本命名为ssh_brute_defender.py。它的主要逻辑是读取指定时间窗口内的SSH日志。使用正则表达式提取[preauth]断开日志的源IP。统计每个IP的出现频率。将频率超过阈值的IP加入ssh-blocklistipset。记录封禁操作到本地日志文件。#!/usr/bin/env python3 SSH暴力破解自动化防御脚本 功能分析近期SSH日志封禁高频notty/preauth连接的源IP。 import re import sys import subprocess import logging from datetime import datetime, timedelta from collections import Counter import os # 配置区域 LOG_FILE ‘/var/log/secure‘ # SSH日志文件路径Ubuntu系统可能是‘/var/log/auth.log‘ ANALYSIS_WINDOW_MINUTES 10 # 分析过去多少分钟的日志 THRESHOLD 15 # 同一IP在时间窗口内出现多少次则触发封禁 IPSET_SET_NAME ‘ssh-blocklist‘ # ipset集合名称需与之前创建的名称一致 BLOCK_TIMEOUT 3600 # 封禁超时时间秒应与ipset创建时的timeout一致或更短 SCRIPT_LOG ‘/var/log/ssh_defender.log‘ # 本脚本的运行日志 # 编译正则表达式用于匹配 [preauth] 断开日志并提取IP # 示例日志Jun 24 15:33:22 host sshd[12345]: Received disconnect from 203.0.113.100 port 54321:11: Bye Bye [preauth] PREAUTH_PATTERN re.compile( r‘sshd\[\d\]: Received disconnect from (\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}) port \d:.*\[preauth\]$‘ ) # 配置结束 def setup_logging(): 配置脚本自身日志 logging.basicConfig( levellogging.INFO, format‘%(asctime)s - %(levelname)s - %(message)s‘, handlers[ logging.FileHandler(SCRIPT_LOG), logging.StreamHandler(sys.stdout) # 同时输出到控制台方便cron调试 ] ) return logging.getLogger(__name__) def parse_log_file(log_file, time_window_minutes): 解析日志文件提取指定时间窗口内的[preauth]断开日志的源IP。 返回一个Counter对象记录每个IP的出现次数。 ip_counter Counter() window_start datetime.now() - timedelta(minutestime_window_minutes) try: with open(log_file, ‘r‘) as f: for line in f: # 1. 快速过滤只处理包含‘[preauth]‘的行提升效率 if ‘[preauth]‘ not in line: continue # 2. 尝试匹配我们定义的正则表达式 match PREAUTH_PATTERN.search(line) if match: ip match.group(1) # 3. (可选) 简单的时间过滤。更精确的做法是解析日志行首时间戳。 # 这里为了简化假设日志是实时写入的我们读取的是最近产生的部分。 # 在生产环境中对于按时间分割的日志文件此方法基本准确。 # 如果需要精确按时间窗口过滤可以使用‘dateutil‘库解析时间戳。 ip_counter[ip] 1 except FileNotFoundError: logger.error(f“日志文件不存在: {log_file}“) return ip_counter except PermissionError: logger.error(f“没有权限读取日志文件: {log_file}“) return ip_counter except Exception as e: logger.error(f“读取日志文件时发生未知错误: {e}“) return ip_counter logger.info(f“日志解析完成。共处理了{sum(ip_counter.values())}条[preauth]记录涉及{len(ip_counter)}个独立IP。“) return ip_counter def block_ip_with_ipset(ip, timeout): 使用ipset命令将指定IP添加到封禁集合。 如果添加成功返回True如果IP已存在或添加失败返回False。 # 构建ipset add命令。exist选项确保如果IP已存在则重置其超时计时器。 cmd [‘ipset‘, ‘add‘, IPSET_SET_NAME, ip, ‘timeout‘, str(timeout), ‘-exist‘] try: result subprocess.run(cmd, capture_outputTrue, textTrue, checkFalse) if result.returncode 0: logger.info(f“成功封禁IP: {ip} (超时: {timeout}秒)“) return True else: # ipset add 返回非0可能是IP已存在但-exist应该处理了或其他错误 if “already added“ in result.stderr: logger.debug(f“IP {ip} 已存在于封禁列表中已更新超时。“) else: logger.warning(f“封禁IP {ip} 失败: {result.stderr}“) return False except subprocess.CalledProcessError as e: logger.error(f“执行ipset命令失败: {e}“) return False except FileNotFoundError: logger.error(“未找到‘ipset‘命令请确保已安装ipset。“) return False def main(): global logger logger setup_logging() logger.info(“ SSH防御脚本开始执行 “) # 步骤1解析日志统计恶意IP suspicious_ips parse_log_file(LOG_FILE, ANALYSIS_WINDOW_MINUTES) # 步骤2筛选并封禁达到阈值的IP blocked_count 0 for ip, count in suspicious_ips.items(): if count THRESHOLD: logger.warning(f“检测到可疑IP: {ip}, 在最近{ANALYSIS_WINDOW_MINUTES}分钟内出现{count}次[preauth]断开。“) if block_ip_with_ipset(ip, BLOCK_TIMEOUT): blocked_count 1 else: logger.debug(f“IP {ip} 出现{count}次未达到阈值{THRESHOLD}忽略。“) logger.info(f“本次执行封禁了 {blocked_count} 个IP地址。“) logger.info(“ SSH防御脚本执行结束 \n“) if __name__ ‘__main__‘: # 检查是否为root运行因为ipset命令需要root权限 if os.geteuid() ! 0: print(“错误此脚本需要root权限来执行ipset命令。“, filesys.stderr) sys.exit(1) main()4.3 部署与自动化执行脚本写好了接下来让它自动运行起来。1. 放置脚本并设置权限sudo cp ssh_brute_defender.py /usr/local/bin/ sudo chmod x /usr/local/bin/ssh_brute_defender.py sudo chown root:root /usr/local/bin/ssh_brute_defender.py2. 配置定时任务Cron我们让脚本每5分钟运行一次持续监控。sudo crontab -e在打开的编辑器中添加以下行# 每5分钟以root身份运行一次SSH防御脚本并将cron自身的输出追加到系统日志或丢弃 */5 * * * * /usr/bin/python3 /usr/local/bin/ssh_brute_defender.py /var/log/cron_ssh_defender.log 21保存并退出。现在你的防御系统已经开始自动工作了。3. 验证与监控查看脚本日志tail -f /var/log/ssh_defender.log观察脚本是否正常运行以及封禁了哪些IP。查看当前封禁列表sudo ipset list ssh-blocklist可以看到当前被禁的所有IP及其剩余超时时间。测试封禁效果可以从另一台机器确保不是你的管理IP尝试快速连接你的SSH端口多次观察是否很快被加入封禁列表并且后续连接超时。5. 进阶优化与问题排查基础的自动化系统已经搭建完成但它可能还不够健壮和智能。下面分享一些进阶优化思路和实际运维中会遇到的问题。5.1 系统优化与功能增强白名单机制绝对不能封禁自己或重要的管理IP、合作伙伴IP。可以在脚本开头定义一个白名单列表在封禁前进行过滤。WHITELIST_IPS {‘192.168.1.100‘, ‘10.0.0.1‘} if ip in WHITELIST_IPS: logger.info(f“IP {ip} 在白名单中跳过封禁。“) continue持久化封禁列表ipset的timeout特性会导致恶意IP一小时后自动解封。对于确认为高威胁的IP例如尝试了数百次可以将其加入一个永久的、无超时的封禁集合或者写入到/etc/hosts.deny如果SSH配置了TCP Wrappers或防火墙的持久化配置中。关联认证失败日志当前的规则只基于[preauth]断开。可以增强分析逻辑同时关联authentication failure日志。如果一个IP既有大量[preauth]断开又有很多认证失败记录可以赋予更高的“威胁分数”并可能使用更长的封禁时间。告警通知当封禁了新的IP特别是高频攻击IP时可以通过邮件、Slack、钉钉、企业微信等Webhook发送告警通知让管理员知晓攻击态势。import requests def send_alert(ip, count): webhook_url “YOUR_WEBHOOK_URL“ message {“text”: f“ SSH防御系统封禁IP: {ip}\n原因: {count}次[preauth]连接 (阈值{THRESHOLD})“} try: requests.post(webhook_url, jsonmessage, timeout5) except Exception as e: logger.error(f“发送告警失败: {e}“)使用更专业的工具对于大规模、复杂的环境可以考虑使用成熟的入侵检测/防御系统IDS/IPS如Fail2ban。Fail2ban本质上就是做了我们上面做的事情但功能更强大支持多种服务SSH, Apache, Nginx等配置更灵活社区活跃。我们的实战可以看作是理解Fail2ban工作原理的绝佳途径。知其然更知其所以然。5.2 常见问题与排查实录即使方案设计得再完美在实际部署中总会遇到各种问题。以下是我总结的几个典型问题和解决方法问题1脚本运行后ipset list里看不到封禁的IP或者很快消失了。可能原因1ipset集合的timeout参数设置过短。检查创建集合和添加IP时的timeout值是否一致且合理。在封禁函数中我们传递的BLOCK_TIMEOUT应与集合的默认timeout匹配或更短因为-exist会重置为传入的超时时间。可能原因2iptables规则没有正确引用ipset。使用sudo iptables -L INPUT -v -n查看规则匹配计数。如果封禁IP后对应规则的pkts和bytes计数没有增长说明规则可能未生效或顺序有误。确保DROP规则在ACCEPT相关规则之前。排查命令sudo ipset list ssh-blocklist # 查看集合内容及超时 sudo iptables -L INPUT -v -n --line-numbers | grep ssh-blocklist # 查看规则及匹配计数问题2Cron任务没有执行或者执行了但没效果。可能原因1Cron的环境变量与交互式Shell不同导致python3或ipset命令找不到。在Cron命令中使用绝对路径是最佳实践如我们使用了/usr/bin/python3。可能原因2脚本权限问题或Python模块缺失。查看Cron的日志/var/log/cron或journalctl -u cron和脚本自己的日志/var/log/ssh_defender.log获取错误信息。可能原因3脚本需要root权限但Cron任务不是以root身份运行的。使用sudo crontab -e来编辑root的cron。排查命令sudo tail -f /var/log/cron # 查看cron执行日志 sudo -u root /usr/local/bin/ssh_brute_defender.py # 手动以root身份测试脚本问题3误封了正常IP。可能原因阈值THRESHOLD设置得太低或者时间窗口ANALYSIS_WINDOW_MINUTES太短将一些正常的自动化工具如Ansible、监控Agent的检查或管理员的大量SCP操作判定为攻击。解决方案调整阈值根据你的服务器访问模式将阈值调高。例如从15次/10分钟调整为30次/10分钟。引入白名单将已知合法的管理IP、自动化工具所在的IP加入白名单。更精细的规则区分“连接断开”和“认证失败”。有些自动化工具会建立连接但立即断开产生[preauth]但不会尝试认证。可以修改规则只封禁那些既有[preauth]断开又有authentication failure的IP。设置封禁超时这正是我们使用ipset timeout的原因。误封的IP在一段时间后会自动解封将影响降到最低。问题4日志文件太大脚本分析耗时剧增。解决方案不要每次都从头读取整个日志文件。脚本可以记录上次分析到的日志文件位置行号或时间戳下次从该位置继续读取。或者更简单的方法是利用logrotate机制分析脚本只处理主日志文件如/var/log/secure而logrotate会定期将其压缩归档。只要脚本运行频率高于日志轮转频率就不会漏掉数据。构建这样一个自动化防御系统最大的收获不是写出了一个能跑的脚本而是建立起一种“以自动化应对自动化攻击”的思维模式。安全运维不再是手忙脚乱地查日志、加防火墙规则而是将重复性的、基于明确规则的响应工作交给系统让人能更专注于分析更高级、更隐蔽的威胁。从一行不起眼的sshd:notty日志开始到构建起一个7x24小时值守的自动化哨兵这个过程本身就是对“防御纵深”和“安全运营自动化”最好的实践和理解。