Tailscale/Headscale自建异地组网

曾经用过一段时间的Zerotier作为异地组网/内网穿透的工具,用了一段时间感觉还行。

后来听说了Tailscale这个工具,再后来了解到了还有Headscale这样的开源实现,觉得这玩意似乎更加优雅,于是,折腾了半个晚上后总算把网络组起来了。在这里记录一下详细的过程。

术语

这类网络工具总是存在非常多的专有名词,所以得稍微先介绍一下:

Tailscale

Tailscale是一个基于WireGuard协议的组网工具。这一协议的好处是,在建立设备间通信时会尝试打洞,从而实现设备之间的P2P点对点通信,即便这些设备位于一些NAT网关后(不过根据NAT类型不同,打洞成功的可能性也不同,可参见:How NAT traversal works)。

Tailscale的工作流程不准确概括如下,如果希望更深入了解,可以阅读Tailscale: How it works

  1. Tailscale官方搭设了一系列中央服务器,为用户提供注册、通信建立与数据中继等功能。
  2. 用户在需要组网的设备上安装Tailscale客户端,登录账号并加入虚拟子网。
  3. 虚拟子网建立后,所有子网内设备在通信时,将先与Tailscale中央服务器进行通信,判断两个设备间能否直连(打洞)。
  4. 如果能打洞,直连(DIRECT);如果不能打洞,则两个设备间的通信将需要中继服务器转发流量,即中继连接(RELAY)。

Tailscale服务器

值得注意的是,在Tailscale中,中央服务器中继服务器可以是不同的服务器。

  • 中央服务器, Control Server: 控制用户认证、存储组网配置等信息。
  • 中继服务器, DERP Server: 专用于在设备间无法建立P2P直接通信时,转发设备间流量实现互相访问。

值得注意的是,所有设备间流量在网络上传输时都是加密的,且私钥都在设备本地,因此通常认为中继服务器是无法解密传输流量的。

Headscale

Tailscale官方尽管允许自行部署DERP服务器,但Tailscale中央服务器程序是闭源的,官方也不允许我们部署。而人们又常常因为一些原因,想要或者需要搭建自己控制的中央服务器,真正地将所有信息都掌控在可控范围内。

因此,Headscale,也就是今天我们要介绍的主角出现了。Headscale是Tailscale中央服务器程序的开源实现,并采用了BSD协议发行,我们可以任意部署到我们的服务器上。

有意思的是,Headscale的一位主要开发者实际上是Tailscale公司的雇员,Headscale的开发也受到了Tailscale公司的支持,参见: Making heads or tails of open source

除了基本的Headscale程序,我们今天还会稍稍提一嘴Headscale-ui,这是一个管理Headscale服务器的Web UI界面,可以免去敲命令管理Headscale组网的烦恼。一般来说需要为Headscale配置API Key,这点我们后面说。

技术细节

这篇文章采用的小技术细节:

  1. Headscale运行在Docker上,方便管理;
  2. Nginx实现反向代理;
  3. 提供Web UI管理Headscale服务器;
  4. 使用一个子域名如myvlan.example.com作为访问入口;
  5. 在已托管有好几个网站的服务器上搭设,因此如TLS证书、Nginx配置等都与现有其他网站兼容。

配置Headscale服务器

准备工作

显然,在开始配置自己的Headscale中央服务器之前,我们需要:

  • 一台具备公网IP的服务器(至少对你来说,这个IP地址应当确保在你的应用场景下,总是可达的)
  • 一个可用的域名(没有也能组网,但域名能省去非常多麻烦。同时,也假设已经为该域名配置好了HTTPS与证书)
  • Docker(直接在本机系统上设置也行,具体可以参考安装文档)
  • Nginx(如果你的服务器同时还运行着其他网站,需要充当反向代理的角色)

接下来让我们开始吧!

准备配置文件

首先我们需要前往Headscale的Github仓库,复制一份配置文件样板config-example.yaml

按需要修改配置文件,可以参考下面我的示例,一些无关紧要的项目注释已被删除,有需要可以参考上述完整的样板文件。

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
---
# Headscale服务器暴露在公网上的地址
# 该地址可被用于客户端与服务器的通信
server_url: https://myvlan.example.com

# Headscale服务器监听的端口
# 使用Docker可以直接写0.0.0.0:8080
# 若直接在本机系统上运行且8080端口被占用,可调整为其他空闲端口
listen_addr: 0.0.0.0:8080

# Headscale /metrics Web API
# 可用于获取Headscale运行状态
# 根据自身需求选择是否暴露该API
metrics_listen_addr: 127.0.0.1:9090

# Headscale gRPC 端口,用于CLI工具远程控制服务器
# 该功能需要配合证书使用
# 根据自身需求选择是否暴露该端口
grpc_listen_addr: 127.0.0.1:50443
grpc_allow_insecure: false

noise:
private_key_path: /var/lib/headscale/noise_private.key

# 设置Headscale分配的虚拟内网IP地址段
# 根据设计,Headscale要求内网IP段 **必须** 在以下子网范围内:
# - IPv4: 100.64.0.0/10
# - IPv6: fd7a:115c:a1e0::/48
# 请注意,这一功能与CGNAT冲突,见:https://tailscale.com/kb/1015/100.x-addresses
# 因此不能在如阿里云等启用了CGNAT的云服务器上安装 **Headscale客户端**
# 也就是不能将云服务器作为一台客户机加入VLAN。
prefixes:
v4: 100.98.76.0/24
v6: fd7a:115c:a1e0::/48

# IP分配策略,有`sequential`和`random`两种可选
allocation: sequential

## DERP配置
#
# 配置Headscale向客户机所分发的DERP服务器列表
# 这些DERP服务器可由3种不同方式定义配置
derp:
# 方法1:Headscale内置DERP服务器
server:
# 主开关,指定是否启用Headscale服务器内置的DERP服务器
# 启用后,可以将这台服务器主机作为转发中继流量的节点之一
# 由于DERP服务器强制要求TLS,因此如果启用这一选项,`server_url`必须以`https`开头
enabled: true

# 内置DERP服务器的Region ID
# 可以随便写,但要注意检查与其他DERP的Region ID冲突
# 如果冲突,内置DERP的Region ID会覆盖外部导入DERP
region_id: 999

# 随便写
region_code: "myoffice"
region_name: "DERP in My Office"

# 用于DERP服务器STUN连接的端口,可以自定义
# 需要前往云服务器防火墙设置中放行对应端口的UDP链接
stun_listen_addr: "0.0.0.0:10086"

private_key_path: /var/lib/headscale/derp_server_private.key
automatically_add_embedded_derp_region: true

# 填写云服务器的IP地址
ipv4: 1.2.3.4
ipv6: 2001:db8::1

# 方法2:可订阅的JSON格式响应API,获取一系列DERP服务器列表
# 在示例配置文件中,默认包含了一条从Tailscale服务器上拉取DERPMAP的URL
# 如果想完全禁用Tailscale提供的服务器,请直接将这一段改成:`urls: []`
urls:
- https://controlplane.tailscale.com/derpmap/default

# 方法3:本地DERP YAML文件定义
# 指定所使用的DERP定义YAML文件绝对路径
# 示例:
# paths:
# - /etc/headscale/myderp.yaml
paths: []

auto_update_enabled: true
update_frequency: 24h

disable_check_updates: true
ephemeral_node_inactivity_timeout: 30m

database:
type: sqlite
debug: false
gorm:
prepare_stmt: true
parameterized_queries: true
skip_err_record_not_found: true
slow_threshold: 1000
sqlite:
path: /var/lib/headscale/db.sqlite
write_ahead_log: true
wal_autocheckpoint: 1000

## TLS配置
#
# 因为我的服务器上已配置好HTTPS,且我希望由Nginx反向代理管理HTTPS连接
# 因此本配置文件将这部分内容置空,令Headscale服务器跳过自动配置TLS
# 如果有需要,请参考官方提供的完整示例文件
acme_url: https://acme-v02.api.letsencrypt.org/directory
acme_email: ""
tls_letsencrypt_hostname: ""
tls_letsencrypt_cache_dir: /var/lib/headscale/cache
tls_letsencrypt_challenge_type: HTTP-01
tls_letsencrypt_listen: ":http"
tls_cert_path: ""
tls_key_path: ""

log:
format: text
level: info

policy:
mode: file
path: ""

## DNS配置
#
# Headscale可支持Talescale的DNS配置以及MagicDNS,具体可参考文档:
# - https://tailscale.com/kb/1054/dns/
# - https://tailscale.com/kb/1081/magicdns/
# - https://tailscale.com/blog/2021-09-private-dns-with-magicdns/
# - https://tailscale.com/kb/1235/resolv-conf
# 如果不希望Headscale/Tailscale管理DNS,请将以下所有子项目值置空
dns:
# MagicDNS主开关
# 我个人挺喜欢的一项功能,可以自动为每台加入虚拟局域网的主机分配一个DNS记录
# 如果不需要,或在复杂网络配置下担心扰乱现有网络配置,请关闭这一选项
magic_dns: true

# MagicDNS基础域名(二级虚拟域名),附加在主机名后作为DNS记录
# 这一名称不可以和`server_url`使用的域名相同
# 建议可以使用如`.lan`等目前IANA没有分配的顶级域名,不干扰其他正常域名解析
# 例如,`base_domain`取值为`my.lan`,则可以用`myroom.my.lan`指代网络中名为`myroom`的主机
base_domain: my.lan

# 向客户端暴论的DNS列表,要求客户端使用这些DNS进行解析
# 这一选项在公司等网络中非常有用
nameservers:
global:
- 114.114.114.114
- 223.5.5.5
- 8.8.8.8
- 2400:3200::1
- 2001:4860:4860::8888

# Split DNS (参见:https://tailscale.com/kb/1054/dns/),
split:
{}
# foo.bar.com:
# - 1.1.1.1
# darp.headscale.net:
# - 1.1.1.1
# - 8.8.8.8

search_domains: []

# 设置Headscale服务器上额外的DNS记录
# 目前只支持A和AAAA记录
# 参见:docs/ref/dns.md
extra_records: []
# - name: "git.myroom.my.lan"
# type: "A"
# value: "100.98.76.2"
#
# # 也可以写在一行里
# - { name: "git.myroom.my.lan", type: "A", value: "100.98.76.2" }
#
# 也可以从一个外部JSON文件导入,该文件每次被更改时会被自动解析加载
# extra_records_path: /var/lib/headscale/extra-records.json

# Unix socket可以让CLI不需要认证就能连接到服务器控制程序,直接抄就行
unix_socket: /var/run/headscale/headscale.sock
unix_socket_permission: "0770"

logtail:
enabled: false

randomize_client_port: false

写好以后,保存,文件名为config.yaml

准备Docker

我们新建一个文件夹,用于放置我们的配置文件等等,假设就叫hs-server吧。

在里面新建一个文件夹,并把刚刚我们编写好的config.yaml放进来,假设就叫hs-etc吧,这个文件稍后会被映射到容器的/etc/headscale文件夹。

接下来我们要使用Docker Compose创建容器,我们回到hs-server文件夹,新建docker-compose.yml文件如下。

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
services:
headscale:
image: headscale/headscale
restart: unless-stopped
container_name: headscale
ports:
# Headscale服务器主端口,如果不打算使用反向代理可以去掉前面的`127.0.0.1`
- "127.0.0.1:10080:8080"
# /metrics API端口,根据需要开放(在前面Headscale配置里需要开放外部监听)
# - "127.0.0.1:10089:9090"
# DERP STUN,如果启用了内置DERP服务器,请在这里映射对应端口
# - 10086:10086
# - 10086:10086/udp
volumes:
# 配置文件文件夹
- ./hs-etc/:/etc/headscale
# 用一个卷volume存放Headscale的数据库等
- hs-data:/var/lib/headscale
# 将本机系统时间与时区等映射到容器内
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
command: serve

volumes:
hs-data:

完成后,保存。

现在我们的文件夹hs-server下应该包含以下内容:

1
2
3
4
5
6
hs-server/
├── docker-compose.yml
└── hs-etc
└── config.yaml

2 directories, 2 files

启动容器

Docker,启动!

1
docker compose up -d

可以curl访问一下刚刚映射的主端口,检查Headscale是否正常启动。默认访问/结点结果是404,所以记得用-D -输出响应头检查确认。

1
2
3
4
user@MyServer:~$ curl -D - http://127.0.0.1:10080
HTTP/1.1 404 Not Found
Date: Sun, 11 Jan 2025 12:24:22 GMT
Content-Length: 0

开放防火墙(可选)

如果配置了DERP服务器,请务必记得在系统防火墙与云服务商控制面板中放行对应端口,例如上面示例配置文件中的10086端口的UDP通信。

配置Nginx反向代理

不过,到目前为止,我们搭建的Headscale中央服务器还没有映射到我们的子域名,如myvlan.example.com上。

而且,还记得我们前面提到,最好为服务器启用TLS加密吗?在这一套实现中,这相关的工作也要交给Nginx完成。

这里放上Nginx的示例配置文件,按需修改其中使用的子域名、SSL证书路径、反向代理Headscale服务器地址等参数后,就可以和其他Nginx配置一起使用了。

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}

server {
listen 443 ssl http2;
listen [::]:443 ssl http2;

server_name myvlan.example.com;

index index.html index.htm;

ssl_certificate /path/to/your/cert/fullchain.pem;
ssl_certificate_key /path/to/your/cert/privkey.pem;

ssl_dhparam /path/to/your/dhparam.pem;
ssl_prefer_server_ciphers on;
ssl_session_timeout 10m;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;

add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";

location / {
#client_max_body_size 512M;
# 这里填写Headscale中央服务器的本地监听端口
proxy_pass http://127.0.0.1:10080;
#proxy_set_header Connection $http_connection;
#proxy_set_header Upgrade $http_upgrade;
#proxy_set_header Host $host;
#proxy_set_header X-Real-IP $remote_addr;
#proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
#proxy_set_header X-Forwarded-Proto $scheme;

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $server_name;
proxy_redirect http:// https://;
proxy_buffering off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
}
}

server {
listen 80;
listen [::]:80;

server_name myvlan.example.com;

if ($host = myvlan.example.com) {
return 301 https://$host$request_uri;
}

return 404;
}

重启Nginx,访问子域名,看是否能响应HTTP请求。

开始组网

添加用户

在Tailscale中,每个组成的虚拟子网,都被称为一个Tailnet。一个Tailnet可以加入多台设备,且在一个Tailnet内,所有设备彼此可见,通过一个局域网IP进行互相访问。而即便是在同一个中央服务器上,隶属于不同Tailnet的设备,彼此之间是不可见的。

而在Headscale中,与 Tailnet 相对应的概念是 User。将不同的设备加入到一个User下,就意味着这些设备隶属于同一个用户,该用户下的所有设备共享一个虚拟子网。

现在,让我们来创建一个用户(子网)吧!在中央服务器上执行命令:

1
docker exec -it headscale headscale users create <username>

这里请把<username>替换成任意喜欢的名称,执行后即可成功创建。

Users还是Namespaces?

许多关于Headscale的文档还使用headscale namespaces命令管理子网,似乎它的功能和users差不多?

其实,在2023年1月(似乎是v0.19.0)之后的版本中,Headscale将所有namespaces相关的命令和概念都换成了users

所以,如果使用的是新版的Headscale,请按照users进行使用。

参见以下Issues

注册设备

常规方法

现在,我们需要将我们想要彼此连接的设备添加到刚刚创建的用户(子网)中。

在设备上安装好Tailscale客户端后,打开终端或命令提示符,执行以下命令。

1
tailscale login --login-server https://myvlan.example.com

命令行会返回类似如下的信息:

1
2
3
To authenticate, visit:

https://myvlan.example.com/register/ab-CDE_fghijkl0123456789

可以继续访问上面的链接,网页会告诉我们,需要前往Headscale中央服务器执行一串命令。实际上,这行命令的作用就是让Headscale中央服务器接受我们在客户端上的入网请求。

我们也可以注意到,在刚刚给出的链接中最后一部分,例如上面链接的ab-CDE_fghijkl0123456789,其实就是认证用的密钥。

让我们回到中央服务器,执行命令:

1
docker exec -it headscale headscale nodes register -u <username> -k mkey:<yourkey>

请将<username>替换成用户名(子网名),<yourkey>替换成在上面链接中获得的密钥,例如ab-CDE_fghijkl0123456789

预认证密钥(另一种方法)

当然,还有另一种入网方法。

我们可以先在服务端上为设备预先创建好一个密钥,然后让客户端设备拿着密钥进行认证,这样就不需要从客户端上想办法获得确认口令,再去服务器上批准入网了。

在中央服务器上执行:

1
docker exec headscale headscale preauthkeys create -u <username> -e 2h

这里,-u <username>仍然是用户名,而-e 2h表示这条预认证密钥将在2小时后失效。因此在创建这条密钥之后,需要在2小时内到客户端上使用并完成认证。

没有意外的话,服务器会返回类似以下内容:

1
2
2025-01-11T12:00:00+08:00 TRC expiration has been set expiration=7200000
abcdefghijklmnopqrstuvwxyz0123456789012345678901

第二行就是生成的预认证密钥啦!让我们来到客户端,修改一下刚刚的入网命令:

1
tailscale login --login-server https://myvlan.example.com --auth-key <yourkey>

<yourkey>替换成上面的预认证密钥,执行命令,就可以成功入网,不需要再次回到中央服务器批准请求啦。

开心使用

组网之后,最基本的使用当然是利用分配的虚拟子网IP互相访问。不过如果仅限于此,似乎又差点意思。

MagicDNS

事实上这是我最喜欢的Tailscale/Headscale功能之一。

在使用Zerotier时,在设备间使用域名进行访问相当困难,似乎唯一可行的方法是在子网内搭建私有DNS服务器,手动维护DNS记录。而mDNS这类基于广播的方案在Zerotier上支持也并不好。

但是,在Headscale上,一切就变得简单起来了!

回到先前的Headscale配置文件,如果我们定义了类似于以下内容:

1
2
3
dns:
magic_dns: true
base_domain: my.lan

假设在我们的房间里有一台设备的Hostname为myroom的主机,那么我们就可以用myroom.my.lan这个域名访问这台主机。在这个过程中,我们甚至不太需要和设备的虚拟子网IP地址打交道。

MagicDNS与多级域名

人总是贪心的,当MagicDNS提供了一个域名指向一台机器,就一定会有人希望能设置多个DNS记录指向同一台机器,从而根据不同域名分别访问不同的Web服务,类似于公网上的subdomain.example.com

设想一下,假设myroom主机上同时运行了一个Gitlab实例和一个PhotoPrism实例。我们很自然地希望,在myroom主机的80或443端口上设置一个Nginx服务器,分别监听前往git.myroom.my.lanpic.myroom.my.lan域名的请求,再分别反向代理到对应服务端口上。这样可以免去记忆服务端口号的烦恼。

感谢MagicDNS,和在公网上类似,我们可以在Headscale内网中设置多条AAAAA记录,指向不同的IP。

我们找到Headscale服务器配置中以下部分(在本文前面的配置文件示例中,注释已经写得很详细了):

1
2
dns:
extra_records: []

根据目标地址的域名、IP分别修改一下:

1
2
3
4
5
dns:
extra_records:
- name: "git.myroom.my.lan"
type: "A"
value: "100.98.76.2"

保存配置文件,重启Docker,现在就可以在客户端上通过git.myroom.my.lan域名访问地址为100.98.76.2的主机了。

不过这种方案并不优雅,每次增删改DNS记录后都要重启Headscale实例。还好,我们可以进一步引入一个外置的DNS记录配置JSON文件避免这种尴尬。

创建一个dns-records.json文件,用于和config.yaml一同挂载到Docker容器内。

1
2
3
4
5
6
7
hs-server/
├── docker-compose.yml
└── hs-etc
├── config.yaml
└── dns-records.json

2 directories, 3 files

dns-records.json中填入类似以下内容:

1
2
3
4
5
6
7
[
{
"name": "git.myroom.my.lan",
"type": "A",
"value": "100.98.76.2"
}
]

再修改一下config.yaml

1
2
dns:
extra_records_path: /etc/headscale/dns-records.json

重启Headscale。在此之后,Headscale会在每次检测到dns-records.json发生改动时,自动读取该文件并更新DNS记录。

DNS记录JSON文件格式问题

headscale/docs/ref/dns.md中有这样一段话:

Be sure to "sort keys" and produce a stable output in case you generate the JSON file with a script. Headscale uses a checksum to detect changes to the file and a stable output avoids unnecessary processing.

似乎是说,在修改dns-records.json文件时,要按一定的方式进行排序。暂时不是很清楚这一建议的含义。

(附赠)自签名HTTPS证书

搭建起Headscale后,一种常见用途就是访问内网其他设备的Web服务。在Tailscale/Headscale内网中,所有数据包都是加密后发送的,理论上讲其实并不会因为使用HTTP直接访问造成安全问题。

但是,一部分Web应用要求必须使用安全连接,因此通常又有使用HTTPS的需求。

然而,显然,没有任何第三方CA会给我们的内网域名颁发TLS证书。考虑到内网中的设备其实都是我们自己使用的,此时,自签名证书就成了最方便灵活的选择。

一般的自签名证书只对应于一组域名,在有多个设备多个域名的场景下,相比起重复颁发并在客户端上信任多个证书,或是顶着浏览器的不安全警告继续访问,或许我们其实可以采用类似于第三方CA的做法。

具体来说,我们的思路是:

  1. 先签发一个根证书(Root CA Certificate),此时我们就扮演了Root CA的角色。
  2. 使用根证书,为需要信任的域名(比方说myroom.my.lan)颁发一个新的证书(本文称为子证书)。
  3. 子证书 安装到提供该Web服务的服务器上,并配置使用如Nginx等提供HTTPS服务。
  4. 在客户端(也就是访问Web服务的终端)上安装根证书,此时客户端将信任所有基于该根证书签发的子证书
  5. 由此,可以在客户端上愉快地内网HTTPS服务。
  6. 在未来需要为新的域名颁发证书(例如another.my.lan)时,只需要执行2~3步骤,不再需要在客户端上安装新证书,因为根证书已经被信任了。

接下来是具体步骤,这些步骤需要使用OpenSSL执行。除特别说明外,这些证书与密钥的存储位置与文件名都是比较随意的,只需要考虑合适的文件权限与维护的便利即可。

签发Root CA根证书

我们首先需要做好扮演Root CA的任务,有了根证书以后才能基于该证书签发新的证书。

第一步,生成Root CA的私钥rootca.key该文件需要妥善保管,不能外泄。(其实泄了也没啥,反正是跑在虚拟内网上的。)

下面命令中末尾的4096是RSA密钥长度。都2025年了,别再用2048位的RSA了。

1
openssl genrsa -out rootca.key 4096
证书加密签名算法的问题

一定有小伙伴想,RSA都是多久以前的加密算法了,要不我们换一点新玩意,比方说ECDSA,EdDSA之类的(其实这些算法地位上似乎并不能并列)。

事实上,ECDSA已经开始用于一些网站中。但是EdDSA几乎没有得到支持。而且,EdDSA的证书在Windows 10中无法被正确识别

考虑到ECDSA是NSA弄出来的玩意,要不,我们还是用更长的RSA吧。

第二步,准备一个Root CA证书生成配置文件rootca.conf。该文件主要是提供证书中包含的信息,如别名等。

在这个配置文件中,[req_distinguished_name]段的内容可以随意更改。自签名证书嘛,随便填,好玩就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[req]
distinguished_name = req_distinguished_name
x509_extensions = v3_ca
prompt = no

[req_distinguished_name]
# Country/Region
C = CN
# State/Province
ST = Somewhere
# Location/City
L = OnTheCloud
# Organization
O = Togawa Group
# Common Name
CN = Togawa Group ROOT CA v1

[v3_ca]
basicConstraints = critical,CA:TRUE
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always,issuer:always
keyUsage = critical,digitalSignature,keyCertSign,cRLSign

第三步,使用上面的私钥和配置文件,接下来我们就可以生成Root CA的根证书文件rootca.crt了。

这个根证书文件rootca.crt,就是接下来可安装到各客户端上的证书。

1
openssl req -x509 -new -key rootca.key -days 3650 -sha512 -config rootca.conf -out rootca.crt

顺带一提,如果不希望使用配置文件rootca.conf控制证书信息,也可以去掉-config rootca.conf这一参数。OpenSSL会启动一个问答,根据提示填写即可。

签发子证书

假设我们希望给myroom.my.lan这台服务器签发一个通配符证书。该证书既信任myroom.my.lan域名,也同时信任*.myroom.my.lan这一泛域名,日后我们使用pic.myroom.my.lan这样的域名访问该服务器时,就不再需要签发新证书了。

首先,我们需要为新的子证书生成一个私钥myroom.key。原则上讲,该私钥不需要透露给Root CA。不过现在只有我们一个人,这些都无所谓啦。

1
openssl genrsa -out myroom.key 4096

第二步,编写子证书的证书请求配置文件myroom.conf。和前面一样,[req_distinguished_name]块的内容可以随意配置,CN也就是Common Name部分需要填写为证书对应的域名。

同时,如果希望该证书同时信任其他域名,只需要在[alt_names]块部分按格式继续添加即可。注意,myserver.lan*.myserver.lan是不同的域名,需要分别列出!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[req]
distinguished_name = req_distinguished_name
prompt = no

[req_distinguished_name]
C = CN
ST = Tokyo
L = Tokyo
O = Anon Tokyo
CN = myroom.my.lan

[v3_req]
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
authorityKeyIdentifier = keyid:always,issuer:always
extendedKeyUsage = serverAuth
subjectAltName = @alt_names

[alt_names]
DNS.1 = myroom.my.lan
DNS.2 = *.myroom.my.lan

第三步,接下来需要生成子证书的证书请求(Certificate Request)文件myroom.csr。在Root CA和子证书请求分离的场景下,证书请求是可以发送给Root CA以申请证书的。

1
openssl req -new -key myroom.key -config myroom.conf -out myroom.csr

第四步,Root CA收到了证书请求myroom.csr后,利用Root CA自己的私钥,以及根证书和配置文件,最终生成子证书myroom.crt

1
2
openssl x509 -req -in myroom.csr -CA rootca.crt -CAkey rootca.key
-CAcreateserial -days 730 -sha512 -extfile myroom.conf -extensions v3_req -out myroom.crt

到此为止,所有需要的证书、密钥文件都已经生成。只需要按需部署即可。

Nginx绑定证书

使用Nginx提供HTTPS时,需要使用上面子证书签发中获得的myroom.crt证书文件与myroom.key密钥文件。Nginx配置的重点在于以下两句,后缀名是不重要的。

1
2
ssl_certificate /path/to/self-signed/myroom.crt;
ssl_certificate_key /path/to/self-signed/myroom.key;

将根证书安装到Windows

  1. 找到刚生成的Root CA根证书rootca.crt
  2. 双击打开后点击安装证书(在资源管理器右键“安装证书”也行)
  3. 存储位置选择当前用户
  4. 接下来选将所有的证书都放入下列存储,点击右侧浏览,选择受信任的根证书颁发机构
  5. 之后一路确定即可。

将根证书安装到Linux

  1. 找到刚生成的Root CA根证书rootca.crt
  2. 将其复制到/usr/local/share/ca-certificates/目录下。需要注意,文件名随意但后缀名必须为.crt。以上命令生成的.crt证书格式就是可以被正常识读的,不需要再做转换。
  3. 执行sudo update-ca-certificates更新系统证书缓存。如果报错发现刚刚添加的证书被跳过了,可以试着加--fresh参数执行。

顺带一提,Linux的Firefox浏览器不依赖系统的证书库,而是使用自建的证书库。因此在Linux上使用Firefox时,需要手动前往设置,在隐私与安全部分,将根证书导入到Authorities列表中。