以用户而非守护进程身份运行 VPN 连接后动作
写着 USER=alice 的环境变量并不会改变 hook 以哪个账户运行。把事件、触发器和动作拆开,然后证明 UID。
VPN 守护进程上的连接 hook 以守护进程身份运行,这通常意味着它们以 root 身份运行。一旦其中某个 hook 写入某人的主目录或重启桌面辅助程序,你就把“作为任意本地用户行动”的能力交给了网络事件。惯常的捷径——从环境变量里读出用户名再往下传——并不能解决这个问题。它只是给动作换了个标签,并没有改变由哪个账户执行。真正有效的是三段式交接:有特权的步骤说明发生了什么,另一个单元以正确身份执行动作。
环境中的用户名不是身份开关
Unix 依据进程的有效用户 ID 决定它能触碰什么,而不是依据环境里的字符串。设置 USER=alice 或导出 LOGNAME 只改变粗心软件在输出里打印的内容;文件属主检查毫无变化。以 root 运行且带着这个变量的 hook,仍然以 root 打开 Alice 的文件。
名字也不是同一个名字。远程访问登录名、本地 Unix 账户和证书通用名是三个标识符,它们常常重合,有时并不重合。以 a.smith 认证的用户可能根本没有本地账户,也可能通过目录服务映射到 asmith。任何假设一个字符串同时充当三种角色的设计,都会在第一个外包人员身上出问题。
所以第一个问题不是“哪个用户连上了”,而是“哪个本地账户被允许执行这个动作”。请从你自己的账户数据库回答,而不是从客户端送来的内容回答:
id -u alice
getent passwd alice | cut -d: -f1,3,6
stat -c '%U:%G %a %n' /home/alice/.local/state/vpn-status.json
第三条命令是人们常跳过的那条。如果目标文件属于 root,因为上一个 hook 创建了它,那么配置正确的用户动作会因权限错误而失败,这个错误看起来像 systemd 问题,其实不是。
守护进程原地不动
把 VPN 服务自己的 User= 改成 Alice 是错的杠杆。它会改变守护进程如何绑定接口、读取密钥、写状态文件和管理路由,并把所有这些风险搬进一个原本正常工作的单元。守护进程需要它的特权;后续动作不需要。
动它之前先看看这个单元到底在做什么。两条命令会告诉你执行身份和生效的准确命令行,包括任何覆盖文件加进来的内容:
systemctl show -p User -p Group -p ExecStart openvpn-server@server
systemctl cat openvpn-server@server
在默认软件包安装中,守护进程是 root,--up 脚本继承这一点。要确认而不是假设,因为加固过的发行版可能已经把它移到专用账户,而手写的 drop-in 可能通过规则不同的包装器来运行 hook。
三个命名部分,一个很窄的触发器
经得起维护的安排包含事件、触发器和动作。守护进程报告某个命名会话已上线。触发器接受这个小事实,并请求一个固定动作。动作在拥有其所触碰文件的本地账户下运行。链条中没有任何一环接受任意命令路径。
动作本身是一个普通服务单元,身份就是其中的一条指令:
[Service]
Type=oneshot
User=alice
Group=alice
ExecStart=/usr/local/bin/refresh-vpn-status --slot %i
注意 User=alice 作用于整个单元,而 systemd 记录了一个值得知道的例外:带 + 前缀的命令无论如何都以完整特权运行。那个前缀对单一步特权操作是正当工具,也是撤销你刚建立的分隔的便捷方式,所以该单元里任何 + 都当作评审时必须说明理由的东西。
让触发器的输入保持最小且不含机密。会话标识、映射账户和时间戳通常就是所需的全部。绝不要把 shell 片段、可执行文件路径或客户端提供的环境块越过边界,因为接收方无法区分合法值与注入值。
核实真正运行的身份
配置评审不是证据。在一次获批的测试连接之后,确认触碰文件的 UID 以及守护进程仍在使用的 UID:
stat -c '%U %G %y %n' /home/alice/.local/state/vpn-status.json
ps -o user=,pid=,cmd= -p "$(pgrep -o -f 'openvpn --daemon')"
systemd-run --uid=alice --pipe /usr/local/bin/refresh-vpn-status --dry-run
第一行应当显示 Alice 以及落在你测试窗口内的时间戳。第二行应当仍显示 root 或软件包的服务账户,这证明守护进程没有移动。第三行是无需等待真实重连就测试动作权限的最便宜办法。
如果文件属主是 Alice,但它的组或模式在多次运行之间发生变化,就去找 UMask= 或目录权限差异,而不是逻辑错误。如果时间戳完全没动,触发器就没触发过,有用的下一步是针对动作单元运行 journalctl -u,而不是再让客户端重连一次。
连接事件仍然证明不了什么
已建立的隧道只是一个事实。它并不意味着桌面会话已解锁、会话总线可达,或者动作想要的私有资源已经可用。Alice 的 UID 下的 oneshot 单元拥有她的身份,却没有她会话中的任何东西,所以任何需要图形总线的操作都必须走受支持的用户会话机制,并定义她登出时会发生什么。
重连也会重复。在火车上休眠的笔记本一小时内可以产生十几次连接事件,而每次都追加一行的 hook 会把状态文件变成没人能读的日志。让动作幂等,或者让它替换按会话作为键的单条记录。
最后,把爆炸半径限制在单元自身:状态目录之外只读的文件系统视图、不新增特权,并设置明确的超时。守护进程与用户动作之间的分隔本身就有价值,而当动作除了你批准的那一件事之外什么都做不了时,它的价值要大得多。