feat(cert,scripts,docs): Linux 原生安装包与证书申请三处修复(DNS 解析器 / Buypass 下线 / ZeroSSL 报错与密钥脱敏)(v1.2.13)
CI / verify (push) Canceled after 0s
CI / verify (push) Canceled after 0s
- Linux 安装包:scripts/build-linux.ps1 交叉编译出 fonu-<版本>-linux-{amd64,arm64}.tar.gz(二进制 + migrations + systemd 安装脚本 + 运行时资源 + sha256),scripts/fonu-install.sh 负责装机(/opt/fonu、/var/lib/fonu、/etc/fonu/fonu.env 首次生成随机会话密钥)并新增 --reset-admin;装完直接打印初始管理员口令
- 证书 DNS-01:lego 默认读系统 resolv.conf(容器里是 Docker 的 127.0.0.11,不返回权威 NS)导致 could not determine authoritative nameservers;现显式传 AddRecursiveNameservers,默认 223.5.5.5/1.1.1.1/8.8.8.8,设置项 acme_dns_resolvers 可覆盖(system = 回到系统解析器)
- Buypass 已停止签发(2025-10-15)并终止服务(2026-04-15):从可选 CA 移除,历史记录仍可展示,直接提交会被拦下并给出中文原因
- ZeroSSL:422 时带出 error.type/error.code(非 JSON 退回截断正文);同时修掉网络错误把含 access_key 的完整 URL 写进任务日志与 app.log 的密钥泄露
- 测试:解析器列表解析 3 例 + ZeroSSL EAB 4 例(含不泄露密钥断言);gofmt / go build / go vet / go test 全绿
This commit is contained in:
@@ -3,6 +3,7 @@ package acme
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@@ -21,13 +22,16 @@ type zeroSSLEABResponse struct {
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
// zeroSSLEABEndpoint 单独成变量,测试可以替换成本地 httptest 服务。
|
||||
var zeroSSLEABEndpoint = "https://api.zerossl.com/acme/eab-credentials"
|
||||
|
||||
func zerosslEABCredentials(ctx context.Context, apiKey string) (kid, hmac string, err error) {
|
||||
apiKey = strings.TrimSpace(apiKey)
|
||||
if apiKey == "" {
|
||||
return "", "", fmt.Errorf("请在设置中配置 ZeroSSL API Key")
|
||||
}
|
||||
|
||||
endpoint := "https://api.zerossl.com/acme/eab-credentials?access_key=" + url.QueryEscape(apiKey)
|
||||
endpoint := zeroSSLEABEndpoint + "?access_key=" + url.QueryEscape(apiKey)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
@@ -36,7 +40,9 @@ func zerosslEABCredentials(ctx context.Context, apiKey string) (kid, hmac string
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("请求 ZeroSSL API 失败:%w", err)
|
||||
// 不能把 url.Error 整个包进错误:它的字符串里是带 access_key 的完整 URL,
|
||||
// 而这条错误会进任务日志与 <数据目录>/logs/app.log,等于把密钥写进日志。
|
||||
return "", "", fmt.Errorf("请求 ZeroSSL API 失败:%s", redactURLError(err))
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
@@ -44,19 +50,45 @@ func zerosslEABCredentials(ctx context.Context, apiKey string) (kid, hmac string
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", "", fmt.Errorf("ZeroSSL API 返回 %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var payload zeroSSLEABResponse
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
return "", "", fmt.Errorf("解析 ZeroSSL 响应失败:%w", err)
|
||||
parseErr := json.Unmarshal(body, &payload)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
// 422 基本都是 access_key 无效(也可能是账号/权限问题)。ZeroSSL 会在
|
||||
// error.type 与 error.code 里写清原因,所以带上它,而不是只回状态码让用户猜。
|
||||
if parseErr == nil && payload.Error.Type != "" {
|
||||
return "", "", fmt.Errorf("ZeroSSL API 返回 %d:%s(错误码 %d)", resp.StatusCode, payload.Error.Type, payload.Error.Code)
|
||||
}
|
||||
return "", "", fmt.Errorf("ZeroSSL API 返回 %d:%s", resp.StatusCode, snippet(body))
|
||||
}
|
||||
if parseErr != nil {
|
||||
return "", "", fmt.Errorf("解析 ZeroSSL 响应失败:%w", parseErr)
|
||||
}
|
||||
if payload.Success != 1 || payload.EABKid == "" || payload.EABHmacKey == "" {
|
||||
if payload.Error.Type != "" {
|
||||
return "", "", fmt.Errorf("ZeroSSL API 错误:%s", payload.Error.Type)
|
||||
return "", "", fmt.Errorf("ZeroSSL API 错误:%s(错误码 %d)", payload.Error.Type, payload.Error.Code)
|
||||
}
|
||||
return "", "", fmt.Errorf("ZeroSSL API 未返回有效的 EAB 凭据")
|
||||
}
|
||||
return payload.EABKid, payload.EABHmacKey, nil
|
||||
}
|
||||
|
||||
// redactURLError 只保留底层错误原因,避免把带 access_key 的 URL 写进日志。
|
||||
func redactURLError(err error) string {
|
||||
var urlErr *url.Error
|
||||
if errors.As(err, &urlErr) && urlErr.Err != nil {
|
||||
return urlErr.Err.Error()
|
||||
}
|
||||
return err.Error()
|
||||
}
|
||||
|
||||
// snippet 压平并截断响应正文:既不把整页 HTML 灌进日志,也不让换行破坏排版。
|
||||
func snippet(body []byte) string {
|
||||
text := strings.Join(strings.Fields(string(body)), " ")
|
||||
if text == "" {
|
||||
return "(响应体为空)"
|
||||
}
|
||||
if len(text) > 200 {
|
||||
text = text[:200] + "…"
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user