feat(autocert): generate unique ACME key paths per CA directory URL

Previously, ACME keys were stored at a single default path regardless of
which CA directory URL was configured. This caused key conflicts when
using multiple different ACME CAs.

Now, the key path is derived from a SHA256 hash of the CA directory URL,
allowing each CA to have its own key file:
- Default CA (Let's Encrypt): certs/acme.key
- Custom CA: certs/acme_<url_hash_16chars>.key

This enables running certificates against multiple ACME providers without
key collision issues.
This commit is contained in:
yusing
2026-01-31 16:48:18 +08:00
parent fb6b692f55
commit c54741aab6
2 changed files with 21 additions and 6 deletions

View File

@@ -4,10 +4,13 @@ import (
"crypto/ecdsa" "crypto/ecdsa"
"crypto/elliptic" "crypto/elliptic"
"crypto/rand" "crypto/rand"
"crypto/sha256"
"crypto/x509" "crypto/x509"
"encoding/hex"
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
"path/filepath"
"regexp" "regexp"
"github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/certcrypto"
@@ -27,7 +30,7 @@ type Config struct {
CertPath string `json:"cert_path,omitempty"` CertPath string `json:"cert_path,omitempty"`
KeyPath string `json:"key_path,omitempty"` KeyPath string `json:"key_path,omitempty"`
Extra []ConfigExtra `json:"extra,omitempty"` Extra []ConfigExtra `json:"extra,omitempty"`
ACMEKeyPath string `json:"acme_key_path,omitempty"` // shared by all extra providers ACMEKeyPath string `json:"acme_key_path,omitempty"` // shared by all extra providers with the same CA directory URL
Provider string `json:"provider,omitempty"` Provider string `json:"provider,omitempty"`
Options map[string]strutils.Redacted `json:"options,omitempty"` Options map[string]strutils.Redacted `json:"options,omitempty"`
@@ -88,7 +91,7 @@ func (cfg *Config) validate(seenPaths map[string]int) gperr.Error {
cfg.KeyPath = KeyFileDefault cfg.KeyPath = KeyFileDefault
} }
if cfg.ACMEKeyPath == "" { if cfg.ACMEKeyPath == "" {
cfg.ACMEKeyPath = ACMEKeyFileDefault cfg.ACMEKeyPath = acmeKeyPath(cfg.CADirURL)
} }
b := gperr.NewBuilder("certificate error") b := gperr.NewBuilder("certificate error")
@@ -272,3 +275,16 @@ func (cfg *Config) SaveACMEKey(key *ecdsa.PrivateKey) error {
} }
return os.WriteFile(cfg.ACMEKeyPath, data, 0o600) return os.WriteFile(cfg.ACMEKeyPath, data, 0o600)
} }
// acmeKeyPath returns the path to the ACME key file based on the CA directory URL.
// Different CA directory URLs will use different key files to avoid key conflicts.
func acmeKeyPath(caDirURL string) string {
// Use a hash of the CA directory URL to create a unique key filename
// Default to "acme" if no custom CA is configured (Let's Encrypt default)
filename := "acme"
if caDirURL != "" {
hash := sha256.Sum256([]byte(caDirURL))
filename = "acme_" + hex.EncodeToString(hash[:])[:16]
}
return filepath.Join(certBasePath, filename+".key")
}

View File

@@ -1,8 +1,7 @@
package autocert package autocert
const ( const (
certBasePath = "certs/" certBasePath = "certs/"
CertFileDefault = certBasePath + "cert.crt" CertFileDefault = certBasePath + "cert.crt"
KeyFileDefault = certBasePath + "priv.key" KeyFileDefault = certBasePath + "priv.key"
ACMEKeyFileDefault = certBasePath + "acme.key"
) )