fix(ssl): keep panel certificate in sync after auto-renew (#12858)

The auto-renew flow in obtainSSL returned early when OpenResty was not
installed or `nginx -s reload` failed, skipping reloadSystemSSL. The new
certificate was persisted to the DB and written into website Nginx
configs, but the panel's own server.crt / server.key on disk and the
in-memory constant.CertStore were left pointing at the old material.
Because the cert was now fresh, subsequent cron ticks did not retry the
renewal, so the panel kept serving the stale cert until a user manually
re-applied it from 面板设置 → SSL.

Two changes:

1. agent/app/service/website_ssl.go
   reloadSystemSSL is now called unconditionally after a successful
   renewal, regardless of whether OpenResty is present or nginx reload
   succeeded. The function already short-circuits for non-panel SSLs,
   so this is safe.

2. agent/app/service/website_ssl.go + agent/cron/job/ssl.go
   Add SyncSystemSSL, invoked at the start of every renew cron tick.
   It compares the panel's on-disk cert/key with the WebsiteSSL row
   referenced by the SSLID setting and rewrites the files + notifies
   core when they diverge. This recovers existing installs that are
   already in the "DB ahead of disk" state and self-heals any future
   drift introduced by transient failures.
   https://github.com/1Panel-dev/1Panel/issues/12472
This commit is contained in:
崔健壮
2026-05-27 11:58:40 +08:00
committed by GitHub
parent 4f0838c98c
commit 612e67e4cc
2 changed files with 56 additions and 8 deletions

View File

@@ -273,6 +273,53 @@ func reloadSystemSSL(websiteSSL *model.WebsiteSSL, logger *log.Logger) {
}
}
// SyncSystemSSL reconciles the panel certificate on disk with the WebsiteSSL
// row referenced by the SSLID setting. When they differ (typically because a
// previous renewal's reloadSystemSSL was skipped by a transient nginx reload
// failure or because the panel was restarted between the DB save and the file
// rewrite), the on-disk cert is refreshed and core is asked to reload its TLS
// store. Safe to call on every renewal cron tick: it is a no-op when SSL is
// not enabled, when SSLID does not resolve, or when the cert already matches.
func SyncSystemSSL() {
if !global.IsMaster {
return
}
systemSSLEnable, sslID := GetSystemSSL()
if !systemSSLEnable {
return
}
websiteSSL, err := websiteSSLRepo.GetFirst(repo.WithByID(sslID))
if err != nil {
return
}
if strings.TrimSpace(websiteSSL.Pem) == "" || strings.TrimSpace(websiteSSL.PrivateKey) == "" {
return
}
certPath := path.Join(global.Dir.DataDir, "secret/server.crt")
keyPath := path.Join(global.Dir.DataDir, "secret/server.key")
diskCert, _ := os.ReadFile(certPath)
diskKey, _ := os.ReadFile(keyPath)
if strings.TrimSpace(string(diskCert)) == strings.TrimSpace(websiteSSL.Pem) &&
strings.TrimSpace(string(diskKey)) == strings.TrimSpace(websiteSSL.PrivateKey) {
return
}
global.LOG.Infof("panel SSL on disk diverged from DB (SSLID=%d, domain=%s), syncing", websiteSSL.ID, websiteSSL.PrimaryDomain)
fileOp := files.NewFileOp()
if err := fileOp.WriteFile(certPath, strings.NewReader(websiteSSL.Pem), 0600); err != nil {
global.LOG.Errorf("sync panel SSL: write cert failed: %s", err.Error())
return
}
if err := fileOp.WriteFile(keyPath, strings.NewReader(websiteSSL.PrivateKey), 0600); err != nil {
global.LOG.Errorf("sync panel SSL: write key failed: %s", err.Error())
return
}
if err := req_helper.PostLocalCore("/core/settings/ssl/reload"); err != nil {
global.LOG.Errorf("sync panel SSL: notify core failed: %s", err.Error())
return
}
global.LOG.Info("panel SSL synced from DB to disk")
}
func (w WebsiteSSLService) ObtainSSL(apply request.WebsiteSSLApply) error {
return w.obtainSSL(apply.ID, false)
}
@@ -430,15 +477,13 @@ func (w WebsiteSSLService) obtainSSL(id uint, autoRenew bool) error {
printSSLLog(logger, "ErrUpdateWebsiteSSL", map[string]interface{}{"name": website.PrimaryDomain, "err": err.Error()})
}
}
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return
if nginxInstall, err := getAppInstallByKey(constant.AppOpenresty); err == nil {
if err := opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil {
printSSLLog(logger, "ErrSSLApply", nil)
} else {
printSSLLog(logger, "ApplyWebSiteSSLSuccess", nil)
}
}
if err := opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil {
printSSLLog(logger, "ErrSSLApply", nil)
return
}
printSSLLog(logger, "ApplyWebSiteSSLSuccess", nil)
}
reloadSystemSSL(websiteSSL, logger)
if websiteSSL.PushNode {

View File

@@ -24,6 +24,9 @@ func (ssl *ssl) Run() {
sslList, _ := sslRepo.List()
nyc, _ := time.LoadLocation(common.LoadTimeZoneByCmd())
global.LOG.Info("The scheduled certificate update task is currently in progress ...")
// Recover from any prior renewal whose reloadSystemSSL was skipped
// (e.g. nginx reload failure left DB ahead of the on-disk panel cert).
service.SyncSystemSSL()
now := time.Now().Add(10 * time.Second)
for _, s := range sslList {
if !s.AutoRenew || s.Provider == "manual" || s.Provider == "dnsManual" || s.Status == "applying" {