OpenVPN 从终端读取密码,而不是从 stdin
OpenVPN 实际从哪里读取密码、stdin 上的文件为什么失败,以及 Linux 和 macOS 上受支持的三种脚本登录入口。
OpenVPN 在控制终端上提示输入用户名和密码,而重定向到标准输入的文件并不是那个终端。正是这一处细节,解释了为什么密码明明躺在管道里,脚本却卡住不动。提示由客户端绘制,读取发生在终端设备上,因此重定向 stdin 没有任何作用。用客户端自己文档化的某一个输入入口喂给它数据,同一个脚本第一次就能连上。
三种入口几乎能覆盖所有情况,而每一种都有各自的失败方式。请按客户端实际显示的提示来选,而不是按最容易写进脚本的选项来选。
提示并不来自 stdin
当 OpenVPN 需要凭据时,它会打开控制终端并从那里读取。这就解释了为什么交互式测试看起来正常,而同一条命令在 cron 或 unit 文件下却毫无动静,提示被写进没人查看的日志里。有些构建在没有终端时会退回标准输入,这种不一致比直接失败更糟,因为它把问题藏到部署那一刻。
写任何代码之前先检查环境:用调度器或服务管理器将来会使用的那同一个用户、同一份 stdin 去运行客户端。如果提示从未出现在日志里,进程也毫无进展,那么有东西正在等待一个并不存在的终端。
入口一:两行的凭据文件
认证选项接收一个路径,文件里恰好两行,先是用户名,再是密码,不能有别的内容。请显式给出路径,而不要依赖默认文件名,因为工作目录里一只走失的凭据文件,正是错误账号被启用的路径。把权限设为 0600,并把属主交给运行客户端的那份账号。
客户端在启动时只读一次该文件,所以轮换密码意味着重启进程。这通常正是你想要的行为,它应当写进 runbook,放在轮换流程旁边。
入口二:用于密钥口令的 askpass
受口令保护的私钥会抛出第二个不同的提示,而它有自己基于文件的答案:askpass 选项从文件的第一行读取口令。对于基于 SSH 的隧道,等价物是客户端执行的 askpass 程序,它的标准输出就是那个秘密。
两种形式都不会把秘密放进参数列表,两者都让它以文件或会把它打印出来的可执行程序形式存在。保护靠的是文件权限和进程属主,而不是晦涩。一个只会回显硬编码字符串的辅助程序,是多了几步的秘密。
入口三:门户客户端自带的 CLI
企业客户端通常提供一个命令行入口,完全不涉及窗口地连接到指定门户,这才是受支持的路径。在 Linux 上,GlobalProtect 客户端通过自己的 CLI 提供连接与导入证书两项操作,而使用证书认证的门户必须先完成导入,自动化连接才可能生效。请优先选它而不是模拟按键:你能拿到真实的退出状态,不依赖窗口焦点,而且锁屏时也能存活。
如果第一次连接仍然需要人来接受一张证书,那么自动化并没有完成。请在安装阶段就完成导入,并把它记录为前置条件。
当流程需要伪终端时
先问密码、再要一次性验证码的流程,没有哪一个文件可以读取。像 Expect 这样的终端驱动才是合适的工具,因为它会依次等待每个模式,而且必须先启动进程,它才可能匹配到任何东西。
两类失败最常见。不启动客户端却等待提示的脚本会永远等下去;而 Tcl 并不是 shell,所以写在 expect 块里的 shell 算术不会按读到的样子求值。构建模式期间请保持调试开关打开,完成后再关掉。
会泄漏的秘密,以及它们藏在哪里
任何作为参数传进去的东西都会出现在进程列表里,所以凭据应当放在文件里或辅助程序背后,绝不要放在命令行上。一次性验证码按设计会被消耗,每次尝试都必须重新生成。失败日志常常带着用户名,有时还带更多内容,因此在发往任何地方之前先做脱敏,并把秘密留在配置之外——因为内嵌密码的配置文件会被粘进工单和代码仓库。到那时再轮换账号,就意味着要追查每一份副本,而这正是这种布局所避免的代价。
审计你选定的入口,然后把它们接到真正会运行客户端的 unit 文件或调度器上。隧道的寿命,恰好就是那个进程的寿命。