feat(cert,scripts,docs): Linux 原生安装包与证书申请三处修复(DNS 解析器 / Buypass 下线 / ZeroSSL 报错与密钥脱敏)(v1.2.13)
- 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:
@@ -20,6 +20,7 @@ import (
|
||||
|
||||
"github.com/go-acme/lego/v4/certcrypto"
|
||||
legocert "github.com/go-acme/lego/v4/certificate"
|
||||
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
"github.com/go-acme/lego/v4/registration"
|
||||
|
||||
@@ -47,12 +48,12 @@ type Service struct {
|
||||
|
||||
func NewService(cfg config.Config, store *certstore.Store, ddnsSvc *ddns.Service, settings *settings.Store, proxySvc *service.ProxyService, logger *slog.Logger, notifySvc *notify.Service) *Service {
|
||||
return &Service{
|
||||
cfg: cfg,
|
||||
store: store,
|
||||
ddnsSvc: ddnsSvc,
|
||||
settings: settings,
|
||||
proxySvc: proxySvc,
|
||||
logger: logger.With("module", "ACME"),
|
||||
cfg: cfg,
|
||||
store: store,
|
||||
ddnsSvc: ddnsSvc,
|
||||
settings: settings,
|
||||
proxySvc: proxySvc,
|
||||
logger: logger.With("module", "ACME"),
|
||||
notify: notifySvc,
|
||||
jobs: NewJobManager(),
|
||||
expiryAlerted: map[string]string{},
|
||||
@@ -388,6 +389,9 @@ func (s *Service) resolveCA(ctx context.Context, ca string) (string, error) {
|
||||
if err := ValidateCA(ca); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if reason := CAUnavailableReason(ca); reason != "" {
|
||||
return "", fmt.Errorf("%s", reason)
|
||||
}
|
||||
return ca, nil
|
||||
}
|
||||
|
||||
@@ -446,7 +450,19 @@ func (s *Service) obtain(ctx context.Context, job *Job, ca, email, provider stri
|
||||
return certstore.Record{}, fmt.Errorf("初始化 DNS Provider 失败:%w", err)
|
||||
}
|
||||
dnsProvider = wrapDNSProvider(dnsProvider, job)
|
||||
if err := client.Challenge.SetDNS01Provider(dnsProvider); err != nil {
|
||||
// DNS-01 的传播检查由 lego 自己做,它默认读系统 /etc/resolv.conf;
|
||||
// 容器里那通常是 Docker 的 127.0.0.11,它不返回权威 NS,于是报
|
||||
// 「[zone=example.com.] could not determine authoritative nameservers」。
|
||||
// 所以显式指定可靠递归解析器(设置项可改;设为 system 则显式回到系统解析器)。
|
||||
resolvers, err := s.acmeDNSResolvers(ctx)
|
||||
if err != nil {
|
||||
return certstore.Record{}, err
|
||||
}
|
||||
var challengeOpts []dns01.ChallengeOption
|
||||
if len(resolvers) > 0 {
|
||||
challengeOpts = append(challengeOpts, dns01.AddRecursiveNameservers(resolvers))
|
||||
}
|
||||
if err := client.Challenge.SetDNS01Provider(dnsProvider, challengeOpts...); err != nil {
|
||||
return certstore.Record{}, err
|
||||
}
|
||||
|
||||
@@ -505,6 +521,49 @@ func (s *Service) resolveACMEEmail(ctx context.Context, email string) (string, e
|
||||
return email, nil
|
||||
}
|
||||
|
||||
// defaultACMEDNSResolvers 默认递归解析器(阿里/Cloudflare/Google)。
|
||||
// 容器里的系统解析器常只指向 127.0.0.11,无法用于 DNS-01 的权威 NS 查询;
|
||||
// 设置项写 system 则回到系统解析器,写其它值则按逗号/空格分隔逐条使用。
|
||||
const defaultACMEDNSResolvers = "223.5.5.5:53,1.1.1.1:53,8.8.8.8:53"
|
||||
|
||||
// acmeDNSResolvers 返回本次申请要用的解析器;空 slice 表示交给 lego 读系统解析器。
|
||||
func (s *Service) acmeDNSResolvers(ctx context.Context) ([]string, error) {
|
||||
raw, err := s.settings.Get(ctx, settings.KeyACMEDNSResolvers)
|
||||
if err != nil || strings.TrimSpace(raw) == "" {
|
||||
raw = defaultACMEDNSResolvers
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(raw), "system") {
|
||||
return nil, nil
|
||||
}
|
||||
resolvers := parseResolverList(raw)
|
||||
if len(resolvers) == 0 {
|
||||
return nil, fmt.Errorf("DNS 解析器配置无效(%q):请填 host 或 host:port,多个用逗号分隔;留空用默认值,填 system 用系统解析器", raw)
|
||||
}
|
||||
return resolvers, nil
|
||||
}
|
||||
|
||||
// parseResolverList 解析解析器列表:逗号/空格/换行分隔,未写端口时补 53,最多取前 8 个。
|
||||
func parseResolverList(raw string) []string {
|
||||
fields := strings.FieldsFunc(raw, func(r rune) bool {
|
||||
return r == ',' || r == ' ' || r == '\t' || r == '\n' || r == ';'
|
||||
})
|
||||
out := make([]string, 0, len(fields))
|
||||
for _, item := range fields {
|
||||
item = strings.TrimSpace(item)
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(item, ":") {
|
||||
item += ":53"
|
||||
}
|
||||
out = append(out, item)
|
||||
if len(out) >= 8 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func validateACMEEmail(email string) error {
|
||||
addr, err := mail.ParseAddress(email)
|
||||
if err != nil || addr.Address != email {
|
||||
|
||||
Reference in New Issue
Block a user