mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2026-06-19 07:36:59 +00:00
feat(cert): add self-signed certificate renewal worker
Add auto-renewal worker for self-signed certificates that mirrors the ACME renewal logic, using a dedicated shouldRenewSelfSignedCert threshold function verified with TDD. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
package cert
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/0xJacky/Nginx-UI/internal/nginx"
|
||||
"github.com/0xJacky/Nginx-UI/internal/notification"
|
||||
"github.com/0xJacky/Nginx-UI/model"
|
||||
"github.com/0xJacky/Nginx-UI/settings"
|
||||
pkgerrors "github.com/pkg/errors"
|
||||
"github.com/uozi-tech/cosy/logger"
|
||||
)
|
||||
|
||||
// RenewSelfSignedCerts renews every self-signed certificate that is close to
|
||||
// expiry. It is invoked by a dedicated cron job.
|
||||
func RenewSelfSignedCerts() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
buf := make([]byte, 1024)
|
||||
runtime.Stack(buf, false)
|
||||
logger.Errorf("%s\n%s", err, buf)
|
||||
}
|
||||
}()
|
||||
logger.Info("RenewSelfSignedCerts Worker Started")
|
||||
|
||||
db := model.UseDB()
|
||||
if db == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var certs []*model.Cert
|
||||
db.Where("auto_cert = ?", model.AutoCertSelfSigned).Find(&certs)
|
||||
|
||||
now := time.Now()
|
||||
renewalInterval := settings.CertSettings.GetCertRenewalInterval()
|
||||
for _, certModel := range certs {
|
||||
renewSelfSignedCert(certModel, now, renewalInterval)
|
||||
}
|
||||
logger.Info("RenewSelfSignedCerts Worker End")
|
||||
}
|
||||
|
||||
// renewSelfSignedCert renews a single self-signed certificate when it is due.
|
||||
func renewSelfSignedCert(certModel *model.Cert, now time.Time, renewalInterval int) {
|
||||
log := NewLogger()
|
||||
log.SetCertModel(certModel)
|
||||
defer log.Close()
|
||||
|
||||
targetName := getAutoRenewTargetName(certModel)
|
||||
|
||||
if shouldSkipAutoRenew(certModel, now) {
|
||||
logger.Infof("Skip auto renew for %s until %s after previous failure", targetName,
|
||||
certModel.LastAutoRenewAt.Add(autoRenewFailureRetryCooldown).Format(time.DateTime))
|
||||
return
|
||||
}
|
||||
|
||||
if certModel.SSLCertificatePath == "" {
|
||||
handleAutoRenewFailure(certModel, log, targetName,
|
||||
pkgerrors.New("ssl certificate path is empty for self-signed certificate"))
|
||||
return
|
||||
}
|
||||
|
||||
info, err := GetCertInfo(certModel.SSLCertificatePath)
|
||||
if err != nil {
|
||||
handleAutoRenewFailure(certModel, log, targetName,
|
||||
pkgerrors.Wrap(err, "get self-signed certificate info error"))
|
||||
return
|
||||
}
|
||||
|
||||
if !shouldRenewSelfSignedCert(info, now, renewalInterval) {
|
||||
return
|
||||
}
|
||||
|
||||
certPEM, keyPEM, err := RegenerateSelfSigned(certModel)
|
||||
if err != nil {
|
||||
handleAutoRenewFailure(certModel, log, targetName, err)
|
||||
return
|
||||
}
|
||||
|
||||
content := &Content{
|
||||
SSLCertificatePath: certModel.SSLCertificatePath,
|
||||
SSLCertificateKeyPath: certModel.SSLCertificateKeyPath,
|
||||
SSLCertificate: string(certPEM),
|
||||
SSLCertificateKey: string(keyPEM),
|
||||
}
|
||||
if err = content.WriteFile(); err != nil {
|
||||
handleAutoRenewFailure(certModel, log, targetName, err)
|
||||
return
|
||||
}
|
||||
|
||||
nginx.Reload()
|
||||
|
||||
updateAutoRenewStatus(certModel, now, "")
|
||||
notification.Success("Renew Certificate Success",
|
||||
"Certificate %{name} renewed successfully", map[string]any{"name": targetName})
|
||||
|
||||
if err = SyncToRemoteServer(certModel); err != nil {
|
||||
notification.Error("Sync Certificate Error", err.Error(), nil)
|
||||
}
|
||||
}
|
||||
|
||||
// shouldRenewSelfSignedCert reports whether a self-signed certificate with the
|
||||
// given info should be renewed now. It mirrors the renewal-threshold logic of
|
||||
// the ACME auto-renewal job.
|
||||
func shouldRenewSelfSignedCert(info *Info, now time.Time, renewalInterval int) bool {
|
||||
certAge := int(now.Sub(info.NotBefore).Hours() / 24)
|
||||
daysUntilExpiration := int(info.NotAfter.Sub(now).Hours() / 24)
|
||||
totalValidityDays := int(info.NotAfter.Sub(info.NotBefore).Hours() / 24)
|
||||
|
||||
if totalValidityDays < renewalInterval {
|
||||
// short-lived certificate: renew once 2/3 of the lifetime has elapsed
|
||||
earlyRenewalThreshold := 2 * totalValidityDays / 3
|
||||
return daysUntilExpiration <= earlyRenewalThreshold
|
||||
}
|
||||
// normal certificate: renew once the age reaches the renewal interval
|
||||
return certAge >= renewalInterval
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package cert
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestShouldRenewSelfSignedCert(t *testing.T) {
|
||||
now := time.Date(2026, time.May, 18, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
notBefore time.Time
|
||||
notAfter time.Time
|
||||
renewalInterval int
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "normal cert young enough is not renewed",
|
||||
notBefore: now.AddDate(0, 0, -3),
|
||||
notAfter: now.AddDate(0, 0, 362),
|
||||
renewalInterval: 7,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "normal cert past renewal interval is renewed",
|
||||
notBefore: now.AddDate(0, 0, -10),
|
||||
notAfter: now.AddDate(0, 0, 355),
|
||||
renewalInterval: 7,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "short-lived cert with plenty of life left is not renewed",
|
||||
notBefore: now.AddDate(0, 0, -1),
|
||||
notAfter: now.AddDate(0, 0, 4),
|
||||
renewalInterval: 7,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "short-lived cert near expiry is renewed",
|
||||
notBefore: now.AddDate(0, 0, -4),
|
||||
notAfter: now.AddDate(0, 0, 1),
|
||||
renewalInterval: 7,
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
info := &Info{NotBefore: tt.notBefore, NotAfter: tt.notAfter}
|
||||
if got := shouldRenewSelfSignedCert(info, now, tt.renewalInterval); got != tt.expected {
|
||||
t.Fatalf("shouldRenewSelfSignedCert() = %v, want %v", got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user