OpenVPN username-as-common-name:为什么你的 CN 校验是空的
比较 common_name 与 username 的 client-connect 脚本,可能是在拿同一个名字和自己比较。请在替换发生之前把证书绑定到账号。
你添加了一个 client-connect 脚本,拒绝任何 common_name 与 username 不一致的连接,看起来像是把证书和账号绑定在一起。在运行 username-as-common-name 的服务器上,脚本执行时这种比较早已失去意义。两个值就是同一个字符串,因此这项检查对一张完全发给别人的证书也会通过。
这项校验之所以错误,并不是因为脚本写得糟糕。它之所以错误,是因为它读取的值在同一次连接中已经被替换过了。
这种比较看起来能证明什么
证书认证和密码认证是两套独立的证明,服务器可以同时要求两者,却不要求它们属于同一个人。这个缺口是真实存在的:一张有效的设备证书加任意有效账号,与同一张证书加某一个指定的账号,是两个不同的断言。拿 common_name 与 username 做比较的脚本,读起来像是两者之间缺失的那一环。如果 common_name 仍然表示证书所说的内容,它确实会是那一环。
是否如此取决于一个服务器选项,而这个选项通常是为了不相干的原因才开启的,比如让每个用户的配置文件更容易命名。
username-as-common-name 对名字做了什么
启用 username-as-common-name 后,OpenVPN 会在用户名和密码验证通过之后,用已认证的用户名替换证书的通用名。这项检查期间运行的验证钩子收到的仍然是证书的通用名,因为替换还没有发生。此后的一切,包括 client-connect,看到的都是用户名。于是后面的回调拿到的是同一个身份两次,无法与所出示的证书作比较。
| Stage | What common_name holds |
|---|---|
| Username and password verification hook | The certificate common name, for example bob-device |
| After the replacement | The authenticated account, alice |
| client-connect callback | The account again, alice |
追踪一个示意连接:一张发给 bob-device 的证书、一次以 alice 身份成功登录目录,以及开启的该选项。验证阶段仍然能看到两个不同的字符串。连接阶段看到的是 alice 和 alice。
这个例子说明了比较为什么是空的。它并不能说明连接本来是否会被接受,因为证书校验和密码验证仍在各自独立运行。
把绑定放在两个身份都还存在的那一步
这条规则必须在能同时看到证书身份和账号的阶段执行,实际中就是用户名与密码的验证钩子,或者在那里加载的插件。一个已经负责密码验证的受支持插件,通常就是加入配对规则的合适位置,因为若用名字比较取代它,密码检查会整个丢失。
在写代码之前先定义映射关系,因为相等很少是真正的策略。证书往往标识设备,而登录标识的是人,一台设备也可能合法地服务多个人。请把这种关系保存在签发过程由你掌控的形式里,并决定改名、重新签发以及带域名限定的名字时该怎么办。在不了解目录的情况下做小写化或去掉域名前缀,可能把两个本应分开的账号合并起来。
请记住,该选项也会改变哪一份按客户端配置生效,因为 client-config-dir 是按通用名索引的。
用两份有效凭据测试配对
先从策略允许的那一对开始,确认能够连接。然后用同一张有效证书搭配另一个同样有效的账号:这次尝试必须失败,而且必须是因为配对规则而失败,不是因为密码输错。错误的密码只能证明密码检查有效,对证书与账号之间的规则一无所知。
也要检查缺少证书和证书无效这两条路径,并确认服务器在按客户端设置中使用的是哪个身份。如果让比较生效的唯一办法是关闭 username-as-common-name,请先评估这一改动的代价,因为它会把每个客户端的配置查找都退回证书名字。