服务器上已经加好了用户,密码也输入正确,连接却仍然以 AUTH_FAILED 结束。通常插件那一行就解释了原因:插件路径后面的词是 PAM 服务名,它决定由哪一组模块来回答。它不是用户名,也不是指向某个密码文件的指针,OpenVPN 并不自己去读那种文件。

第二个词选择什么

配置行只接受一个插件路径和一个参数:

plugin /usr/lib/openvpn/plugins/openvpn-plugin-auth-pam.so openvpn

这个参数会作为服务名传给 PAM 库,因此实际运行的是 /etc/pam.d/openvpn 这一组。在那里写 login,得到的就是交互式登录策略,连同你发行版中该策略恰好包含的那些模块。

插件路径请用已安装软件包提供的那一个,不要从别的主机照抄。文件名和目录在不同发行版之间并不相同,路径写错会在启动时报插件加载错误,而不是等到认证时才报错。

这一组模块究竟从哪里读取账户

PAM 只是一个调度器。服务文件按顺序列出模块,第一个能作答的模块决定结果。调用 pam_unix 的那一组读取本地账户;调用 LDAP 或 RADIUS 模块的那一组则把凭据发到完全不同的地方。

读一读服务名解析到的那个文件,包括它引入的所有文件:

cat /etc/pam.d/openvpn
grep -H pam_ /etc/pam.d/openvpn /etc/pam.d/common-auth

如果这一组把判断交给目录服务,那么添加本地账户什么也改变不了。如果它读取本地账户,那么在目录里建用户同样改变不了什么。账户必须存在于这一组真正去查的地方,多数此类报告的全部诊断就在这里。

一次尝试,两份日志

把一次失败的连接在两侧对照起来看。OpenVPN 日志显示插件的结果和客户端的拒绝;系统认证日志显示哪个模块运行过、它作出了什么判断。只有把两者放在同一个时间窗口里读,才能把缺少服务文件和后端超时区分开。认证日志里的一段空白意味着根本没有走到这一组。

journalctl -u openvpn-server@server --since "-2 min"
journalctl -t sshd -t sudo --since "-2 min"

用一个能讲 PAM 的工具直接测试该服务,这样你就不必同时调试 VPN 和这一组。用同样的服务名和同样的账户直接测试成功,就把问题缩小到守护进程如何传递凭据,而不是账户本身。

pamtester openvpn alice authenticate

如果直接测试用控制台里可用的同一份凭据仍然失败,说明这一组读取的并不是你假设的那个账户来源。如果成功,就去看插件所运行的对话,以及客户端实际发送了什么。再用一个故意写错的密码跑一次,你就能看到拒绝与模块错误之间的差别。这两种结果在 VPN 日志里看起来一模一样,在这里却完全不同。

专用服务胜过改动 login

当 VPN 的要求与交互式登录不同时(通常都不同),请给它自己的服务文件。改动 login 或 common-auth 会改变所有引入它们的程序的策略,下一个排查无关登录问题的人就会继承你的改动。

把需要的模块复制进一个小服务文件,把顺序写清楚,并注明由谁维护。当密码重置看起来毫无效果时,该查的正是这个文件。

客户端发送什么,不发送什么

客户端通过它支持的提示或文件提供用户名和密码。服务端插件把这些值送进 PAM 对话;它并不从服务器配置里读取它们。服务端那种看起来像是在定义密码的行并没有做这件事,改配置也不会创建账户。

用户名加密码可以与客户端证书共存,启用 PAM 并不是停止验证证书的理由。保留部署原本要的层次,只修账户来源这一处。

验收是双向的:该通过的账户要能通过,该被拒绝的账户仍然要被拒绝。auth-pam 插件 README 记录了服务参数以及它能在前端接上的后端,那里正是确认你要搭建的配置的地方。

相关文章