选择哪种凭据来源,取决于三件事:控制台前是否坐着人、密钥能否留在磁盘上,以及是否必须由另一个进程来答应提示。选错之后,客户端会报告 stdin 问题,而密码其实完全正确,这会把大多数人无缘无故地推到服务器那边。先选路径;只有知道哪条路径产生了这些报错,提示才有意义。

决定它的三个条件

控制台提示是唯一需要人的路径,因此也是唯一会在 systemd 单元或容器里静默失败的路径。如果有人启动客户端并手动输入密码,就不涉及任何文件,敏感数据也不会落到磁盘上。如果客户端在开机时启动,同一个提示就会变成挂起,超时通常表现为一般性的连接失败,而不是凭据错误。

当进程无人值守启动、且密钥可以在受控权限下留在磁盘上时,凭据文件才是正确答案。当密钥根本不允许写入时——只有保险库策略、短期令牌——你需要客户端能打开的流,这在实际中意味着进程替换,或一个写入文件描述符的辅助程序。管理接口覆盖第四种情况:一个已经在管理隧道生命周期的控制器,可以通过套接字回答问题。

  • 控制台前有人、没有自动化:交互式提示。
  • 无人值守启动、密钥可以存放:权限为 0600 的凭据文件。
  • 无人值守启动、密钥不能存放:进程替换,或由密钥代理写入的 fd。
  • 客户端归另一个进程所有:管理接口。

当两个答案都可行时,选择把密钥留在 argv 和 shell 历史之外的方案。以程序参数形式传递的密码,任何本地用户都能通过进程列表读到,而这种暴露很难撤回。

每条输入路径实际做了什么

--auth-user-pass 单独使用时会在控制台询问用户名和密码。给它一个文件名,客户端就把第一行当作用户名、第二行当作密码;它不支持引号、注释或键值语法。这就是全部格式,所以按 INI 文件的预期去编辑它的人一定会弄坏它。内联的 <auth-user-pass> 块把同样的两行放进配置文件,这使配置文件本身成为密钥,并被归入与私钥同一类处理对象。

另外两条路径是流和套接字。OpenVPN 只需要一个可读的文件描述符,因此它能接受进程替换、由管道支撑的 /dev/fd 条目,或者一个实际上就是 FIFO 的文件名。管理接口的性质不同:客户端向监管进程询问这些值,而不是自己去读。这个差别在重连时才显出重要性:凭据文件、可被密钥辅助程序再次写入的 FIFO,以及管理接口都能在无人介入的情况下再次提供;进程替换不行,因为描述符会在第一次读取时被消耗掉。

文件必须正好两行

两种失败模式覆盖了大多数损坏的凭据文件。Windows 编辑器会留下 CRLF 行尾,回车符成为密码的一部分,服务器拒绝一个屏幕上看起来正确的值。第二行为空——把空行放在本该是密码的位置——会让客户端带着空密码去连接,服务器会拒绝它,即使屏幕上的值看起来正确。密码之后多一个换行是无害的。请从 shell 而不是从编辑器写这个文件,并检查字节:

install -m 600 /dev/null /etc/openvpn/client.creds
printf '%s\n%s\n' "$VPN_USER" "$VPN_PASS" > /etc/openvpn/client.creds
od -c /etc/openvpn/client.creds | head -3

路径和内容同样重要。相对路径是相对进程的工作目录解析的,而对一个服务来说,那个目录很少是你测试时所在的目录。用绝对路径,让文件属于运行客户端的账户,并保持 0600 权限,使同组和其他人都读不到。OpenVPN 2.6 手册列出了该指令接受的形式;在自创第三种之前请先对照它。

在把责任推给服务器之前先测试文件。运行 openvpn --config client.ovpn --auth-user-pass /etc/openvpn/client.creds --verb 4 会打印文件是否被打开,以及随后凭据是否被接受,这告诉你下一步该改哪一层。

流、stdin 以及它们产生的报错

把密码通过管道送进去是大多数人最先尝试的做法,也是惊喜最多的一条路径。进程替换之所以有效,是因为客户端只需要一个可读的描述符:openvpn --config client.ovpn --verb 3 --auth-user-pass <(printf '%s\n%s\n' "$VPN_USER" "$VPN_PASS")。传 /dev/stdin 只在客户端发问的那一刻 stdin 仍然打开且可读时才有效,而在一条已经把它读掉的管道里,这个条件并不成立。

ERROR: could not read Auth username from stdin 有两种不同的成因。在部分 2.3.x 客户端上,这条消息来自 stdin 处理的一个缺陷,后来 2.3 的发布修正了它,所以那么旧的客户端只能靠升级改善。在当代客户端上,同样的措辞意味着客户端请求了控制台输入,而 stdin 不是终端——正是 systemd 单元、cron 任务或没有 -t 的容器里的情形。

auth-nocache 加了第三个循环,看起来像密码损坏。它在用完后把凭据从内存中丢弃,所以由网络变化触发的重连没有东西可以重放,提示会再次出现。这条指令的设计就是这样工作的。auth-nocache 改变的是客户端在内存里保留什么,而不是服务器或身份提供方如何看待这个账户,所以整个修正办法就是再次提供同一个文件,在重启时重新读取——而对于受管理的连接,这次提供应当来自经批准的密钥库,而不是脚本里的密码。

当提示归另一个进程所有时的管理接口

已经在监管隧道的控制器也可以回答它的问题。以 --management 127.0.0.1 7505 启动客户端,读取状态行,并留意 >PASSWORD:Need 'Auth' username/password。控制器用 username "Auth" alice 回应,随后在独立的命令行上用 password "Auth" ... 作答。'Auth' 标记会重复请求中的标识符,而且必须匹配,否则响应会被忽略,客户端继续等待。

代价是一个现在归你所有的状态机:连接顺序、客户端重启,以及一个本机其他用户不该碰到的本地套接字。你买到的是在运行时提供的凭据,磁盘上没有文件,也没有可能挂起的提示。如果你的栈里没有别的东西已经在讲这个协议,这条路径会多出一个需要维护的组件。

看起来像凭据问题的故障

始终完不成的 TLS 握手,或者 VERIFY ERROR: depth=0, error=certificate signature failure,属于信任问题,而且发生在读取任何用户名之前。指向 UDP 监听端的 proto tcp 客户端在同一处失败:数据包从未进入认证阶段,所以每个密码都因为这个关联而显得不对。在凭据被发出之前请保持证书校验开启;熟悉的提示并不能说明谁在接收密码。在动凭据之前,先检查证书链、通用名称和传输方式。

拼接配置文件的脚本里的 shell 错误是另一条假线索。[ ! -f "$file" ] 里少一个空格就会出现 command not found,脚本提前停止,配置文件再也不会重建,客户端带着上一版启动。这个错误属于 shell,不属于认证,重打多少次密码都不会消除它。

这条流程里的每个提示并不都是这份凭据。私钥口令、HTTP 代理登录和服务密码是三个阶段里被问到的三个不同密钥,而私钥口令通常由客户端或代理处理,而不是 auth-user-pass。如果提示在 TLS 会话建立之前就出现,你回答的是错误的问题,重置账户也帮不上忙。排除了错误的提示之后,剩下的检查从最早的一层开始:

  • 读第一条协议错误,而不是最后一行凭据:先 TLS,再认证,再 push。
  • 确认文件是两行、不含回车符,并且用绝对路径引用。
  • 确认配置文件和命令行在指令上没有分歧。
  • 把“凭据被接受”和“流量在流动”分开——路由和 DNS 是更晚的一层。

当客户端进入 push 阶段,就不要再盯着凭据,而要去读服务器下发的路由和 DNS 服务器。那是下一个问题,和你刚刚修好的密码没有任何关系。

相关文章