账户锁定与地址封禁:OpenVPN 到底在哪里计数
封禁来源和锁定账户统计的是不同的东西。在为基于 PAM 的 VPN 选择阈值之前,先找到持有失败计数的存储位置。
封禁一个地址和锁定一个账户是两种不同的机制,而且它们的失效方向正好相反。地址封禁只拦住一个来源,目标账户仍可从别处访问。账户锁定则在该身份出现的所有地方拦截,包括用户自己的笔记本电脑。团队常常部署其中一种,描述的却是另一种,直到发生事故时才发现真正受保护的对象错了。
对于把认证委托给 PAM 的 VPN,计数器是 PAM 栈实际到达的位置,而不是监听进程记录的内容。把这两者分开,才是第一个有用的步骤。
两种计数器,两种受害者
限速器观察来源,问最近从这个来源来了多少次尝试。锁定观察身份,问这个账户累计了多少次失败。前者保护可用性,后者保护凭据;两者不能互相替代。只做前者的部署根本没有保护账户。
所以连续失败五次就封 IP 这句话,并不是对撞库报告的答复。攻击者轮换来源,每个来源只拿到少量尝试,账户根本不会被计数。反过来,没有来源控制的锁定会让攻击者靠故意失败把账户无限期锁住。
两种控制的恢复方式也不同。地址封禁按一个没人需要关心的计时器到期;账户锁定可能需要管理员、有文档的等待期,或者自助解锁。而每一项都是在第一次锁定发生之前就必须定好的策略决定。
找到已经持有计数的存储
在添加任何东西之前,先弄清是否已经存在失败记录存储。本地 Linux 账户、目录服务和外部身份提供方各自维护自己的计数,而放在它们前面的 PAM 模块会另建一套,而不是扩展现有那套。
grep -rnE 'faillock|pam_tally|pam_unix' /etc/pam.d/
ls -l /etc/pam.d/ | head -20
读取 VPN 真正指定的服务文件,并跟踪它的 include 行。许多发行版里 VPN 服务会引入一个公共文件,这意味着你调整的计数同样作用于 SSH 和本地登录。这可能没问题,也可能与这次改动想要达到的效果正好相反。
如果目录已经强制执行锁定,有用的问题是 PAM 会话如何走到那个判断,以及 VPN 如何报告它。上面再加的本地计数不会变成目录的计数器,两套策略对账户何时解锁可能并不一致。
pam_faillock 有三个调用位置,而不是一个
该模块把一行复制粘贴容易被模糊掉的三件事分开:检查账户是否已被锁定、记录一次失败、成功后清除历史。它的手册把这些记录为不同的调用位置,由栈决定哪些调用会运行、以什么顺序运行。
从旧计数模块迁移时,出错通常就在这里。把能用的 pam_tally2 示例里的一行复制进 pam_faillock 栈并不是改个名字;有文档的配置并不相同,放错位置的调用可能只记录失败却从不检查。
faillock --user alice
faillock --user alice --reset
检查工具是判断计数器是否真实存在的最快方式:它显示某个账户已记录的失败,并能清除它们。如果一次受控的失败没有改变这个输出,调用位置就是错的。
专用服务名并不等于隔离
给 VPN 单独一份 PAM 服务文件能让预期策略可见,但本身并不限制影响范围。任何对公共文件的引入都会把那个文件的行为一并带进来,包括系统级的计数。
明确决定锁定只覆盖 VPN 访问,还是覆盖所有认证同一账户的服务。两种答案都站得住;真正的失误是以为选了一种,而栈实际执行的是另一种。把这个答案写在服务文件旁边,以免下一次改动悄悄把它推翻。
然后考虑代价。锁定可能由攻击者触发,而承受后果的是账户主人。阈值、等待期和管理员恢复路径都是策略的一部分,恢复不能依赖事件过程中重新配置 VPN。
接受顺序,而不是某一次拒绝
在批准的方案下用指定身份测试,并且测试一条链而不是单个事件。有效账户应能通过认证,故意写错的密码推动计数器,配置的锁定生效,有文档的恢复把访问还回来。
faillock --user test-vpn-01
journalctl -u openvpn-server@lab -n 30 --no-pager | grep -i 'auth\|pam'
成功路径也要像失败路径一样仔细检查。如果成功登录不清除历史,账户会在日常小错慢慢累积后被锁。如果它清掉所有服务的全部记录,计数器就比策略声称的更弱。
最后,在宣布胜利之前停掉无人值守的客户端。保存的凭据在后台重试,会在恢复几分钟后重新制造失败并再次锁住账户。基于来源的限速可以作为附加控制保留,但它不是账户策略。