IPsec 配置备忘 Part7 - 证书 II

接 Part2 与 Part6。尝试折腾一下除了“以证书 DN 作为 id”以外的证书使用姿势,如果有错误或者与本文解释相冲突的情况欢迎在评论指出。

IKEv2 证书认证

在 IKEv2 里,一方需要认证另一方基本只需要两样信息IDAUTH。ID 就是之前配置文件里反复出现的那个,AUTH 一般是使用证书私钥对某些数据的签名,具体细节请参考 RFC。证书则是把两者关联了起来:如果对方给出的 AUTH 能通过证书验证,那么对方就是这份证书所表示的那个人,同时这份证书又是颁发给 ID 的,那么对方就是 ID。

验证者要取得对方的证书有几种方式:

  • 管理员可以直接把证书塞进验证者的/etc目录里
  • 被验证者可以自己把证书发送给验证者(send_cert = always参数)
  • 验证者可以向被验证者请求证书(send_certreq = yes参数)。当然被验证者也可以选择不发送证书(send_cert = never参数),只是验证会失败就是了。

另外如果验证者既不询问证书(send_certreq = no),被验证者也不主动发送证书(send_cert = ifasked),验证一样会失败。

strongSwan 在尝试匹配 ID 和证书的时候会检查 Subject DN 和 SubjectAltName (SAN)。我们之前一直在使用 Subject DN,而 SAN 则允许我们使用域名甚至 IP 作为 ID。另外,虽然我一直称呼“域名”或是“IP”,但是实际上只要 SAN 和 ID 匹配即可,这个“域名”到底是不是我们的并没有关系。(当然只有自签才能签出这种证书)

Subject Alternative Name

要颁发带有 SAN 的证书只需要在 tmpl 文件(参见 Part2)中添加如下内容:

# 域名 SAN
dns_name="san.hosta.com"
# IP 地址 SAN
ip_address = "fd00::1"

需要注意的是,SAN 是区分类型的,比如上文的 DNS 和 IP,而 IKEv2 使用的 ID 也是分类型的(FQDN,IP,etc.)。类型需要匹配才能认证成功。有的教程会使用形如@xxx.xxx.xx.x这样的 IP,可能原因是,某些客户端使用 IP 作为 ID 但是却标记为 FQDN,或者是生成证书的时候将 IP 标记成了域名 SAN。这种情况就需要给 IP 添加前缀@来强制让 strongSwan 将其当作域名 ID。具体的解析规则可见 Identity Parsing 文档。

除了在证书生成上的这些零碎注意点之外,我觉得另一点导致 IKEv2 很难配置的因素是,strongSwan 允许省略很多参数,而不同的平台会给这些被省略的参数提供不同的默认值,导致你以为的设置和程序实际使用的设置出现偏差。更不用说有的平台还有一些稀奇古怪的 BUG。

Android Client w/o DN

这里我们尝试重复 Part6 中的实验,只是不出现 DN,全部用域名代替。同样的,只有有变化的部分才有注释。首先重新生成证书加入 SAN,你只需要重新用 CA 私钥签发服务器证书即可:

1
2
3
4
5
cn = "server common name"
# 域名 SAN,需要和 ID 相匹配
dns_name = "vpn.server.com"
activation_date = "2020-01-01 00:00:00 UTC+0"
expiration_date = "2021-01-01 00:00:00 UTC+0"

然后先看客户端 Profile 的变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
jq -n \
--arg uuid `uuidgen -r` \
--arg name "My server"\
--arg type "ikev2-cert"\
# 注意这里(以及下面的 json 里)去掉了 remote.id。在 Android 客户端上,如果不指定
# remote.id,那么 ID 就默认是 remote.addr 中的值。
#
# 用服务器的域名代替了 IP 地址,目前客户端似乎还是会优先使用解析出来的 IPv4 地址,
# 所以后面 JSON 里的 ipv6-transport 配置也不用了。
--arg addr "vpn.server.com"\
# 这里换成了 CA 证书,注意下方新增了 remote.certreq 配置项。
# 虽然这个配置似乎默认就是 true,不过还是加上以防万一。
--arg servercert "`sed '/-----/d' ca-cert.pem`"\
--arg localbundle "`sed '/-----/d' client-p12bundle.pem`"\
'{
uuid:$uuid,
name:$name,
type:$type,
remote:{
addr:$addr,
cert:$servercert,
certreq: true
},
local: {
p12:$localbundle
}
}' > profile.sswan

主要变化之一是省略了 remote.id 并将 remote.addr 从 IP 改成域名。注意这里不是不设置 ID,而是采用客户端的默认值(remote.addr)。和服务器配置中的省略 remote.id(%any)有巨大不同。之二是使用 CA 证书而不是直接使用服务器证书,至于为什么需要设定 certreq 请参考第一段。

再看服务端配置变化:

server.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
connections {
android-connection {
version = 2
# 由于客户端优先采用 IPv4 解析,这里就需要改成 IPv4 的地址了。
local_addrs = 12.34.56.78
remote_addrs = %any
proposals = aes256gcm128-sha512-x25519
pools = ip4pool
local {
# 和客户端设定的 remote.id 保持一致(
# 恰好是客户端设定的 remote.addr (
# 恰好是服务器的域名
# )
# )
id = vpn.server.com
auth = pubkey
}
remote {
id = %any
auth = pubkey
}
children {
child_sa {
local_ts = 0.0.0.0/0
remote_ts = dynamic
mode = tunnel
esp_proposals = aes256gcm128-sha512-x25519
}
}
}
}

pools {
ip4pool {
addrs = 10.10.10.100-10.10.10.150
}
}

这样我们就配置好使用域名作为 ID 的 strongSwan 服务器了。