Add guarded IP certificate flow and renewal history
This commit is contained in:
1 parent
dd94a4cc84
commit
d3699abb18
14 files changed
+343
-56
No files matched your search
@@ -338,9 +338,61 @@ func checkSites() error {
|
||||
}
|
||||
|
||||
func certificateExists(domain string) bool {
|
||||
fullchain, _, _ := tlsservice.CertificateFiles(domain)
|
||||
_, err := os.Stat(fullchain)
|
||||
return err == nil
|
||||
fullchain, privkey, _ := tlsservice.CertificateFiles(domain)
|
||||
pair, err := tls.LoadX509KeyPair(fullchain, privkey)
|
||||
if err != nil || len(pair.Certificate) == 0 {
|
||||
return false
|
||||
}
|
||||
leaf, err := x509.ParseCertificate(pair.Certificate[0])
|
||||
return err == nil && leaf.VerifyHostname(domain) == nil &&
|
||||
!time.Now().Before(leaf.NotBefore) && time.Now().Before(leaf.NotAfter)
|
||||
}
|
||||
|
||||
func ipCertificateArgs(domain string) ([]string, error) {
|
||||
// The scheduled systemd service on supported hosts must use the same Certbot
|
||||
// binary as interactive issuance, or six-day certificates may expire.
|
||||
issuer, err := exec.LookPath("certbot")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("未找到 Certbot: %w", err)
|
||||
}
|
||||
actual, err := filepath.EvalSymlinks(issuer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scheduled, err := filepath.EvalSymlinks("/usr/bin/certbot")
|
||||
if err != nil || actual != scheduled {
|
||||
return nil, errors.New("IP 短期证书要求定时任务与签发使用同一版本的 /usr/bin/certbot;请先配置续签任务")
|
||||
}
|
||||
if err := exec.Command("systemctl", "is-active", "--quiet", "certbot.timer").Run(); err != nil {
|
||||
return nil, errors.New("IP 短期证书要求先启用 certbot.timer 定时续签")
|
||||
}
|
||||
if err := exec.Command("systemctl", "is-enabled", "--quiet", "certbot.timer").Run(); err != nil {
|
||||
return nil, errors.New("certbot.timer 必须设为开机自动运行,才能续签六天的 IP 证书")
|
||||
}
|
||||
service, err := exec.Command("systemctl", "show", "-p", "ExecStart", "--value", "certbot.service").Output()
|
||||
if err != nil || !strings.Contains(string(service), "/usr/bin/certbot ") {
|
||||
return nil, errors.New("certbot.service 未配置为使用 /usr/bin/certbot 续签,请先核对定时任务")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
out, err := exec.CommandContext(ctx, issuer, "--version").CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("检测 Certbot 版本失败: %w", err)
|
||||
}
|
||||
return ipCertificateArgsForVersion(domain, string(out))
|
||||
}
|
||||
|
||||
func ipCertificateArgsForVersion(domain, output string) ([]string, error) {
|
||||
version := regexp.MustCompile(`certbot ([0-9]+)\.([0-9]+)`).FindStringSubmatch(output)
|
||||
if len(version) != 3 {
|
||||
return nil, errors.New("无法识别 Certbot 版本;IP 证书要求 5.4 或更新版本")
|
||||
}
|
||||
major, _ := strconv.Atoi(version[1])
|
||||
minor, _ := strconv.Atoi(version[2])
|
||||
if major < 5 || (major == 5 && minor < 4) {
|
||||
return nil, fmt.Errorf("当前 Certbot %s 不支持 IP 的 webroot 签发;请升级到 5.4 或更新版本", version[0])
|
||||
}
|
||||
return []string{"--preferred-profile", "shortlived", "--ip-address", domain}, nil
|
||||
}
|
||||
|
||||
// Run performs a reversible HTTP bootstrap, certificate issuance, then HTTPS
|
||||
@@ -366,6 +418,11 @@ func RunEntryWithAuth(opts nginxconfig.RenderOptions, routes gin.RoutesInfo, aut
|
||||
if !tlsservice.ValidPublicHost(scheme, domain) {
|
||||
return errors.New("请先保存有效域名;HTTP 也支持 IPv4 地址")
|
||||
}
|
||||
if scheme == "https" && net.ParseIP(domain) != nil && !certificateExists(domain) {
|
||||
if _, err := ipCertificateArgs(domain); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if !tlsservice.ValidScheme(scheme) {
|
||||
return errors.New("协议仅支持 http 或 https")
|
||||
}
|
||||
@@ -449,8 +506,18 @@ func RunEntryWithAuth(opts nginxconfig.RenderOptions, routes gin.RoutesInfo, aut
|
||||
}
|
||||
emit("HTTP-01 验证站点已启用,开始签发证书。\n")
|
||||
if !certificateExists(domain) {
|
||||
if err := command(emit, 4*time.Minute, "certbot", "certonly", "--webroot", "-w", nginxsetup.Webroot,
|
||||
"-d", domain, "--non-interactive", "--agree-tos", "--register-unsafely-without-email"); err != nil {
|
||||
args := []string{"certonly", "--webroot", "-w", nginxsetup.Webroot}
|
||||
if net.ParseIP(domain) != nil {
|
||||
ipArgs, err := ipCertificateArgs(domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
args = append(args, ipArgs...)
|
||||
} else {
|
||||
args = append(args, "-d", domain)
|
||||
}
|
||||
args = append(args, "--non-interactive", "--agree-tos", "--register-unsafely-without-email")
|
||||
if err := command(emit, 4*time.Minute, "certbot", args...); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -473,8 +540,9 @@ func RunEntryWithAuth(opts nginxconfig.RenderOptions, routes gin.RoutesInfo, aut
|
||||
return err
|
||||
}
|
||||
// Certbot runs deploy hooks only when certificates are renewed.
|
||||
const hook = "#!/bin/sh\nset -eu\n/usr/sbin/nginx -t\n/bin/systemctl reload nginx\n"
|
||||
if data, err := os.ReadFile(hookPath); err == nil && string(data) != hook {
|
||||
const oldHook = "#!/bin/sh\nset -eu\n/usr/sbin/nginx -t\n/bin/systemctl reload nginx\n"
|
||||
hook := renewalHook()
|
||||
if data, err := os.ReadFile(hookPath); err == nil && string(data) != hook && string(data) != oldHook {
|
||||
return errors.New("续签钩子已存在且内容不同,请人工检查")
|
||||
} else if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
|
||||
Reference in new issue
Block a user