Skip to content

Commit 274dbed

Browse files
committed
feat(deploy): 支持 RustFS TLS 证书自动部署与服务重载
- 新增配置项 rustFSPath,用于指定 RustFS TLS 证书目录 - 实现证书下载后自动部署到 RustFS 目录,自动命名证书和密钥文件 - 支持同时向 Nginx、Apache 和 RustFS 部署证书并自动重载相关服务 - 更新配置结构体及验证逻辑,兼容旧配置并支持新目录创建 - 使用统一的 Getter 方法访问各云服务认证字段,优化代码结构 - 在客户端实现 RustFS 证书部署业务处理逻辑与相应测试用例修改 - 更新 README 文档,新增说明支持 Apache 和 RustFS 自动部署和服务重载 - 修改 protobuf 协议,新增 RustFS 证书部署业务类型支持
1 parent f0e423a commit 274dbed

9 files changed

Lines changed: 263 additions & 58 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ go.work.sum
3131
# .idea/
3232
# .vscode/
3333

34+
.claude
3435
bin
3536
anssl
3637
certs

README.md

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
## 特性
66

7-
- 🚀 自动化部署证书并重载 Nginx
7+
- 🚀 自动化部署证书到 Nginx、Apache、RustFS 并自动重载服务
88
- ✅ 内置 HTTP-01 验证服务,自动响应 ACME challenge
99
- ☁️ 支持自动上传证书到云服务(阿里云、七牛云)
1010
- 🔧 守护进程模式,支持后台运行
@@ -36,24 +36,41 @@ server:
3636
port: 19000
3737

3838
ssl:
39-
# 证书存储目录(留空则使用 ./certs)
40-
path: "/etc/nginx/ssl"
39+
# Nginx 证书目录(可选)
40+
nginxPath: "/etc/nginx/ssl"
41+
# Apache 证书目录(可选)
42+
apachePath: "/etc/apache2/ssl"
43+
# RustFS TLS 证书目录(可选)
44+
rustFSPath: "/etc/rustfs/tls"
4145

4246
# 云服务配置(可选)
4347
provider:
4448
- name: "aliyun"
45-
accessKeyId: "your_key"
46-
accessKeySecret: "your_secret"
49+
remark: "阿里云"
50+
auth:
51+
accessKeyId: "your-aliyun-access-key-id"
52+
accessKeySecret: "your-aliyun-access-key-secret"
53+
54+
- name: "qiniu"
55+
remark: "七牛云"
56+
auth:
57+
accessKey: "your-qiniu-access-key"
58+
accessSecret: "your-qiniu-access-secret"
59+
60+
- name: "cloudTencent"
61+
remark: "腾讯云"
62+
auth:
63+
secretId: "your-tencent-secret-id"
64+
secretKey: "your-tencent-secret-key"
4765
```
48-
> #### 已支持的CDN服务商
49-
> 关于`provider:`的`- name:`的值请看下表:
50-
51-
| 配置项参数 | 英文 | 说明 |
52-
|:--------:|:------:|:------:|
53-
| `name` | `aliyun` | 阿里云 |
54-
| `name` | `qiniu` | 七牛云 |
55-
| `name` | 更多 | 敬请期待 |
5666
67+
> #### 已支持的云服务商
68+
>
69+
> | 服务商 | name 值 | 认证字段 |
70+
> | :----: | :------------: | :--------------------------: |
71+
> | 阿里云 | `aliyun` | accessKeyId, accessKeySecret |
72+
> | 七牛云 | `qiniu` | accessKey, accessSecret |
73+
> | 腾讯云 | `cloudTencent` | secretId, secretKey |
5774

5875
### 3. 配置 Nginx
5976

@@ -92,8 +109,8 @@ sudo ./anssl daemon -c config.yaml
92109
2. 后端推送 ACME challenge token 到 CLI
93110
3. CLI 自动缓存并响应 Let's Encrypt 验证请求
94111
4. 验证成功,证书签发
95-
5. 自动下载并部署证书
96-
6. 自动重载 Nginx
112+
5. 自动下载并部署证书到配置的服务(Nginx/Apache/RustFS)
113+
6. 自动重载 Nginx 和 Apache 服务
97114

98115
**全程自动化,无需手动操作。**
99116

@@ -117,12 +134,14 @@ sudo ./anssl daemon -c config.yaml
117134

118135
## 配置说明
119136

120-
| 配置项 | 必填 | 说明 |
121-
|--------|------|------|
122-
| `server.accessKey` | ✅ | 从 anssl.cn 获取的访问密钥 |
123-
| `server.port` | ❌ | HTTP-01 验证端口,默认 19000 |
124-
| `ssl.path` | ❌ | 证书存储目录,默认 `./certs` |
125-
| `provider` | ❌ | 云服务配置(阿里云/七牛云) |
137+
| 配置项 | 必填 | 说明 |
138+
| ------------------ | ---- | -------------------------------------------- |
139+
| `server.accessKey` | ✅ | 从 anssl.cn 获取的访问密钥 |
140+
| `server.port` | ❌ | HTTP-01 验证端口,默认 19000 |
141+
| `ssl.nginxPath` | ❌ | Nginx 证书目录,配置后自动部署并重载 Nginx |
142+
| `ssl.apachePath` | ❌ | Apache 证书目录,配置后自动部署并重载 Apache |
143+
| `ssl.rustFSPath` | ❌ | RustFS TLS 证书目录,配置后自动部署证书 |
144+
| `provider` | ❌ | 云服务配置(阿里云/七牛云) |
126145

127146
## 故障排除
128147

@@ -184,10 +203,13 @@ sudo systemctl start anssl
184203
A: 登录 [anssl.cn](https://anssl.cn) → 设置 → 个人资料
185204

186205
**Q: 支持哪些 Web 服务器?**
187-
A: 目前仅支持 Nginx 自动重载,其他服务器可使用本工具下载证书后手动配置
206+
A: 支持 Nginx、Apache 和 RustFS TLS 自动部署。只需在 `config.yaml` 中配置对应的证书目录,即可实现自动部署和服务重载(Nginx 和 Apache)
207+
208+
**Q: 可以同时部署到多个服务吗?**
209+
A: 可以。在 `config.yaml` 中同时配置 `nginxPath`、`apachePath` 和 `rustFSPath`,证书更新时会自动部署到所有配置的服务
188210

189211
**Q: 证书会同时部署到本地和云服务吗?**
190-
A: 不会。如果配置了云服务,会优先上传到云服务;否则部署到本地
212+
A: 在 [anssl.cn](https://anssl.cn) 控制台配置部署目标时,可以选择部署到本地 CLI(Nginx/Apache/RustFS)或云服务(阿里云/七牛云)。每个证书可以配置多个部署目标,实现同时部署
191213

192214
**Q: HTTP-01 验证需要手动操作吗?**
193215
A: 不需要。配置好 Nginx 反向代理后,验证全程自动完成

internal/client/cert_deploy.go

Lines changed: 97 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,9 @@ func (cd *CertDeployer) DeployCertificate(domain, url string) error {
7272
sslConfig := config.GetConfig().SSL
7373
nginxPath := sslConfig.NginxPath
7474
apachePath := sslConfig.ApachePath
75+
rustFSPath := sslConfig.RustFSPath
7576

76-
if nginxPath == "" && apachePath == "" {
77+
if nginxPath == "" && apachePath == "" && rustFSPath == "" {
7778
logger.Info("未配置SSL目录,证书已下载", "file", zipFile)
7879
return nil
7980
}
@@ -106,7 +107,14 @@ func (cd *CertDeployer) DeployCertificate(domain, url string) error {
106107
}
107108
}
108109

109-
// 4. 检查nginx是否存在,如果存在则测试配置和重新加载
110+
// 4. 部署到 RustFS 目录
111+
if rustFSPath != "" {
112+
if err := cd.deployToRustFS(extractDir, rustFSPath, safeDomain); err != nil {
113+
return fmt.Errorf("部署到RustFS失败: %w", err)
114+
}
115+
}
116+
117+
// 5. 检查nginx是否存在,如果存在则测试配置和重新加载
110118
if nginxPath != "" && cd.isNginxAvailable() {
111119
// 测试nginx配置
112120
if err := cd.testNginxConfig(); err != nil {
@@ -121,7 +129,7 @@ func (cd *CertDeployer) DeployCertificate(domain, url string) error {
121129
logger.Info("nginx未安装或不在PATH中,跳过nginx相关操作")
122130
}
123131

124-
// 5. 检查apache是否存在,如果存在则测试配置和重新加载
132+
// 6. 检查apache是否存在,如果存在则测试配置和重新加载
125133
if apachePath != "" && cd.isApacheAvailable() {
126134
// 测试apache配置
127135
if err := cd.testApacheConfig(); err != nil {
@@ -266,6 +274,55 @@ func (cd *CertDeployer) DeployCertificateToApache(domain, url string) error {
266274
return nil
267275
}
268276

277+
// DeployCertificateToRustFS 仅部署证书到 RustFS
278+
func (cd *CertDeployer) DeployCertificateToRustFS(domain, url string) error {
279+
sslConfig := config.GetConfig().SSL
280+
rustFSPath := sslConfig.RustFSPath
281+
282+
if rustFSPath == "" {
283+
return fmt.Errorf("未配置 RustFS TLS 目录 (ssl.rustFSPath)")
284+
}
285+
286+
// 创建certs目录
287+
if err := os.MkdirAll(certsDir, 0755); err != nil {
288+
return fmt.Errorf("创建证书目录失败: %w", err)
289+
}
290+
291+
safeDomain := sanitizeDomain(domain)
292+
fileName := fmt.Sprintf("%s_certificates.zip", safeDomain)
293+
zipFile := filepath.Join(certsDir, fileName)
294+
295+
// 下载zip文件
296+
if err := cd.client.downloadFile(url, zipFile); err != nil {
297+
return fmt.Errorf("下载证书失败: %w", err)
298+
}
299+
300+
logger.Info("证书下载完成", "file", zipFile)
301+
302+
defer func() {
303+
if _, err := os.Stat(zipFile); err == nil {
304+
os.Remove(zipFile)
305+
}
306+
}()
307+
308+
folderName := safeDomain + "_certificates"
309+
extractDir := filepath.Join(certsDir, folderName)
310+
311+
if err := cd.extractZip(zipFile, extractDir); err != nil {
312+
os.RemoveAll(extractDir)
313+
return fmt.Errorf("解压证书失败: %w", err)
314+
}
315+
defer os.RemoveAll(extractDir)
316+
317+
// 部署到 RustFS 目录
318+
if err := cd.deployToRustFS(extractDir, rustFSPath, safeDomain); err != nil {
319+
return fmt.Errorf("部署到RustFS失败: %w", err)
320+
}
321+
322+
logger.Info("RustFS证书部署完成", "domain", domain)
323+
return nil
324+
}
325+
269326
// deployToNginx 部署证书到 Nginx 目录并生成配置文件
270327
func (cd *CertDeployer) deployToNginx(sourceDir, nginxPath, folderName, safeDomain string) error {
271328
// 移动证书文件
@@ -308,6 +365,43 @@ func (cd *CertDeployer) deployToApache(sourceDir, apachePath, folderName, safeDo
308365
return nil
309366
}
310367

368+
// deployToRustFS 部署证书到 RustFS 目录
369+
func (cd *CertDeployer) deployToRustFS(sourceDir, rustFSPath, safeDomain string) error {
370+
// RustFS 目标目录(使用域名作为子目录)
371+
targetDir := filepath.Join(rustFSPath, safeDomain)
372+
373+
// 如果目标目录已存在,先删除
374+
if _, err := os.Stat(targetDir); err == nil {
375+
if err := os.RemoveAll(targetDir); err != nil {
376+
return fmt.Errorf("删除现有RustFS证书目录失败: %w", err)
377+
}
378+
}
379+
380+
// 创建目标目录
381+
if err := os.MkdirAll(targetDir, 0755); err != nil {
382+
return fmt.Errorf("创建RustFS证书目录失败: %w", err)
383+
}
384+
385+
// 复制并重命名证书文件
386+
// cert.pem -> rustfs_cert.pem
387+
srcCert := filepath.Join(sourceDir, "cert.pem")
388+
dstCert := filepath.Join(targetDir, "rustfs_cert.pem")
389+
if err := copyFileWithMode(srcCert, dstCert, 0644); err != nil {
390+
return fmt.Errorf("复制证书文件失败: %w", err)
391+
}
392+
393+
// 复制并重命名私钥文件
394+
// privateKey.key -> rustfs_key.pem
395+
srcKey := filepath.Join(sourceDir, "privateKey.key")
396+
dstKey := filepath.Join(targetDir, "rustfs_key.pem")
397+
if err := copyFileWithMode(srcKey, dstKey, 0600); err != nil {
398+
return fmt.Errorf("复制私钥文件失败: %w", err)
399+
}
400+
401+
logger.Info("证书已部署到RustFS目录", "path", targetDir, "cert", "rustfs_cert.pem", "key", "rustfs_key.pem")
402+
return nil
403+
}
404+
311405
// generateNginxSSLConfig 生成 Nginx SSL 配置文件
312406
func (cd *CertDeployer) generateNginxSSLConfig(nginxPath, folderName, safeDomain string) error {
313407
certDir := filepath.Join(nginxPath, folderName)

internal/client/connect.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func (c *Client) handleConnect(stream *connect.BidiStreamForClientSimple[deployP
3030
break
3131
}
3232

33-
provider, err := aliyun.New(providerConfig.AccessKeyId, providerConfig.AccessKeySecret)
33+
provider, err := aliyun.New(providerConfig.GetAccessKeyId(), providerConfig.GetAccessKeySecret())
3434
if err != nil {
3535
return err
3636
}
@@ -49,7 +49,7 @@ func (c *Client) handleConnect(stream *connect.BidiStreamForClientSimple[deployP
4949
break
5050
}
5151

52-
provider := qiniu.New(providerConfig.AccessKey, providerConfig.AccessSecret)
52+
provider := qiniu.New(providerConfig.GetAccessKey(), providerConfig.GetAccessSecret())
5353

5454
success, err = provider.TestConnection()
5555
if err != nil {

internal/client/execute_busines.go

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ func (c *Client) executeBusines(stream *connect.BidiStreamForClientSimple[deploy
4949
case deployPB.ExecuteBusinesType_EXECUTE_BUSINES_ANSSL_CLI_APACHE_CERT:
5050
// 部署证书到本地 apache
5151
result = c.handleApacheCertificateDeploy(domain, downloadURL)
52+
case deployPB.ExecuteBusinesType_EXECUTE_BUSINES_ANSSL_CLI_RUSTFS_CERT:
53+
// 部署证书到本地 RustFS
54+
result = c.handleRustFSCertificateDeploy(domain, downloadURL)
5255
default:
5356
result = deployPB.ExecuteBusinesRequest_REQUEST_RESULT_NOT_SUPPORTED
5457
logger.Warn("不支持的业务类型", "executeBusinesType", executeBusinesType)
@@ -100,6 +103,23 @@ func (c *Client) handleApacheCertificateDeploy(domain, downloadURL string) deplo
100103
return deployPB.ExecuteBusinesRequest_REQUEST_RESULT_SUCCESS
101104
}
102105

106+
// handleRustFSCertificateDeploy 处理证书部署到本地 RustFS
107+
func (c *Client) handleRustFSCertificateDeploy(domain, downloadURL string) deployPB.ExecuteBusinesRequest_RequestResult {
108+
if domain == "" {
109+
logger.Error("域名不能为空")
110+
return deployPB.ExecuteBusinesRequest_REQUEST_RESULT_FAILED
111+
}
112+
113+
deployer := NewCertDeployer(c)
114+
if err := deployer.DeployCertificateToRustFS(domain, downloadURL); err != nil {
115+
logger.Error("RustFS证书部署失败", "error", err, "domain", domain)
116+
return deployPB.ExecuteBusinesRequest_REQUEST_RESULT_FAILED
117+
}
118+
119+
logger.Info("RustFS 证书部署成功", "domain", domain)
120+
return deployPB.ExecuteBusinesRequest_REQUEST_RESULT_SUCCESS
121+
}
122+
103123
// handleCertificateProvider 处理证书提供商的上传操作
104124
func (c *Client) handleCertificateProvider(providerName string, executeBusinesType deployPB.ExecuteBusinesType, remark, cert, key string) deployPB.ExecuteBusinesRequest_RequestResult {
105125
// 只支持上传证书操作
@@ -133,16 +153,20 @@ func (c *Client) getProviderHandler(providerName string) (providers.ProviderHand
133153

134154
switch providerName {
135155
case "aliyun":
136-
if providerConfig.AccessKeyId == "" || providerConfig.AccessKeySecret == "" {
156+
accessKeyId := providerConfig.GetAccessKeyId()
157+
accessKeySecret := providerConfig.GetAccessKeySecret()
158+
if accessKeyId == "" || accessKeySecret == "" {
137159
return nil, fmt.Errorf("阿里云配置不完整: accessKeyId 或 accessKeySecret 为空")
138160
}
139-
return aliyun.New(providerConfig.AccessKeyId, providerConfig.AccessKeySecret)
161+
return aliyun.New(accessKeyId, accessKeySecret)
140162

141163
case "qiniu":
142-
if providerConfig.AccessKey == "" || providerConfig.AccessSecret == "" {
164+
accessKey := providerConfig.GetAccessKey()
165+
accessSecret := providerConfig.GetAccessSecret()
166+
if accessKey == "" || accessSecret == "" {
143167
return nil, fmt.Errorf("七牛云配置不完整: accessKey 或 accessSecret 为空")
144168
}
145-
return qiniu.New(providerConfig.AccessKey, providerConfig.AccessSecret), nil
169+
return qiniu.New(accessKey, accessSecret), nil
146170

147171
default:
148172
return nil, fmt.Errorf("不支持的提供商: %s", providerName)

internal/client/providers/aliyun/aliyun_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ func TestMain(m *testing.M) {
2121

2222
for _, p := range cfg.Provider {
2323
if p.Name == "aliyun" {
24-
logger.Info("测试提供商上传证书", "provider", p.Name, "accessKeyId", p.AccessKeyId, "accessKeySecret", p.AccessKeySecret)
24+
accessKeyId := p.GetAccessKeyId()
25+
accessKeySecret := p.GetAccessKeySecret()
26+
logger.Info("测试提供商上传证书", "provider", p.Name, "accessKeyId", accessKeyId, "accessKeySecret", accessKeySecret)
2527
// 创建实例
26-
provider, err = aliyun.New(p.AccessKeyId, p.AccessKeySecret)
28+
provider, err = aliyun.New(accessKeyId, accessKeySecret)
2729
if err != nil {
2830
logger.Error("创建提供商实例失败", "error", err)
2931
return

internal/client/providers/qiniu/qiniu_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ func TestMain(m *testing.M) {
1818

1919
for _, p := range cfg.Provider {
2020
if p.Name == "qiniu" {
21-
logger.Info("测试提供商上传证书", "provider", p.Name, "accessKey", p.AccessKey, "accessSecret", p.AccessSecret)
21+
accessKey := p.GetAccessKey()
22+
accessSecret := p.GetAccessSecret()
23+
logger.Info("测试提供商上传证书", "provider", p.Name, "accessKey", accessKey, "accessSecret", accessSecret)
2224
// 创建实例
23-
provider = qiniu.New(p.AccessKey, p.AccessSecret)
25+
provider = qiniu.New(accessKey, accessSecret)
2426
}
2527
}
2628

0 commit comments

Comments
 (0)