启动 OpenVPN 却始终无法连接的脚本,通常不是网络问题:客户端正停在一个提示符上,却没有终端可以把它画出来。OpenVPN 恰好有两种受支持的方式可以在无人值守时回答该提示,即 auth-user-pass 背后的凭据文件和管理套接字,两者不能互换。凭据文件只需一条指令,负责回答一个提示;套接字回答所有提示,并给任何能打开 loopback 端口的程序一条无需认证的控制通道。哪一种是你的部署该用的,取决于真正卡住的是哪个提示,以及你能负担把哪个秘密留在静止状态。

先给提示起名,再选机制

四种提示在终端上看起来一模一样。账号对来自服务器的 PAM 或 auth-user-pass-verify 配置,由 auth-user-pass 回答。私钥口令属于密钥文件,通过 --askpass 读取或手工输入。PKCS#12 包会要求自己的密码。代理认证使用的是 --http-proxy 接受的 auth 文件,而不是账号凭据。

当 OpenVPN 作为 systemd、PHP 或 Node 的子进程运行时没有终端,因此其中任何一种都会静默阻塞。进程就停在那里,日志在加密行之后中断,单元最终超时;人们宁可加一个重试循环或 sleep 也不去看哪个提示在等待,原因就是这份沉默。只有管理套接字能回答全部四种。凭据文件只回答第一种,别的都不行。

auth-user-pass:一个文件,一个提示

文件第一行是用户名,第二行是密码,别无其他:没有引号、没有键名、没有注释。把权限设为 600,并把属主设为运行 openvpn 的账号;对服务来说这通常是专用用户,而不是你的登录账号。/root 下属于 root 的文件,就是在你的 shell 里能跑、和在开机时单元失败之间的差别。

两个细节会把正确的文件变成阻塞。以 CRLF 保存的编辑器会把一个回车符粘在密码后面,于是服务器拒绝一个在文件里看起来正确的密码;用 cat -A 或 od -c 检查,而不是用眼睛。而配置文件里不带参数的 auth-user-pass 按设计就是「询问用户」,命令行上的第二条指令未必能覆盖它,因为配置文件是在它所在的位置被解析的。

选项顺序是第三个陷阱,而且与版本有关。部分 OpenVPN 2.4 构建在 --auth-user-pass 位于 --config 之前时会忽略它,所以同样的两个选项换个顺序就能连上。把该指令放进配置文件,或在命令行上放在 --config 之后,并在改动其他任何东西之前用 grep 找出配置里的每一处出现。

auth-nocache 属于这套机制,但不提供任何东西。它限制客户端在内存里保留凭据的时间,崩溃之后有用,文件缺失时毫无用处。服务器端会话超时后的重连会重新读取该文件,所以开机时能用的凭据仍然可能在一天中途过期。

管理套接字:所有提示,更多暴露面

带监听器启动客户端,并明确声明提示会报告在它上面,使用 OpenVPN 2.6 手册中记录的指令:

management 127.0.0.1 3030
management-query-passwords

在 bash 中把连接放在一个空闲文件描述符上,按提示名称而不是位置应答,因为重连可能以不同的顺序提问:

exec 3<>/dev/tcp/127.0.0.1/3030
while read -r -u 3 line; do
  case "$line" in
    *"Need 'Auth' username/password"*)
      echo 'username "Auth" alice' >&3
      echo 'password "Auth" secret' >&3 ;;
    *"Need 'Private Key' password"*)
      echo 'password "Private Key" secret' >&3 ;;
  esac
done

边界是传输方式。管理协议是 loopback 上 TCP 的明文,所以监听器不应发布在 LAN 或隧道地址上,好让远程控制器够得着;远程控制器意味着 SSH 转发,而不是更宽的绑定。社区版守护进程一次只接受一个管理客户端,所以 GUI 或监视器无法与你的脚本共用这个套接字。第二个连接会被拒绝而不是排队,失败看起来就像一个聋掉的脚本。

那个套接字同时也是一条控制通道。没有管理密码,任何能连上 127.0.0.1:3030 的东西都能读取状态、杀掉隧道,或自己回答一个提示。管理密码和凭据文件是同一类秘密,两者都值得同样的谨慎。

哪一个适合你的部署

当唯一的提示就是账号对、客户端作为有稳定属主的服务运行、并且你能把 600 文件放在你无法控制的备份之外时,选凭据文件。当涉及密钥口令或 PKCS#12 密码、包装器已经在监管该进程、或者调试重连时需要观察状态与提示时,选管理套接字。

两个条件会改变判断。如果包装器是 /bin/sh、直接调用二进制文件的 systemd 单元,或极简容器,那么仅限 bash 的套接字胶水就不可用,文件是更便宜的答案。如果秘密经常轮换,两个文件都不是它的好归宿:从一个小包装器查询密钥环,让单元调用该包装器,这样轮换只留在一处,密码也不会进入 shell 历史。

进程替换介于两者之间,既不属于任何一方。下面的一行命令之所以有效,是因为 bash 在 openvpn 启动前就展开了它,而不是因为客户端理解这种语法:

openvpn --config client.ovpn --auth-user-pass <(printf "%s
%s
" "$USER" "$PASS")

它在 /bin/sh 下、在绕过 shell 的单元里、以及在没有 bash 或 /dev/fd 的镜像中都会失效。留作交互测试,绝不要当成服务所依赖的契约。

先保护秘密,再从日志验证

两种机制都会留下一个 OpenVPN 进程可读的秘密。绝不要把密码放在命令行或单元文件里,那里 ps 和 systemctl 会把它展示给每个本地用户;传文件路径,而不是里面的值。在证书可以取代账号对的地方,这会去掉这一类提示,而不是把它藏起来。

正确的运行会显示提示序列走完,会话到达「Initialization Sequence Completed」。如果客户端停在同一处提示,说明匹配失败:打印出套接字发送的确切行,检查引号和提示名称,修的是模式而不是加 sleep。报告成功却不传输任何东西的隧道,是路由或 push 的问题,不是凭据问题。

然后再测重连,而不只是第一次启动。断开服务器端会话,或等它超时;客户端会再次提问,正是在那里,一个开机时看起来正确的文件才能显示它对重连的账号是否仍然可读。

相关文章