fix: Normalize wildcard domain paths for certbot cert storage

Certbot stores wildcard certs under the base domain (e.g.,
*.example.com → /etc/letsencrypt/live/example.com/), but the path
helpers were using the raw wildcard domain. This caused deploy hooks
to reference nonexistent paths, silently failing and leaving certs
unrenewable.
This commit is contained in:
2026-09-10 22:23:05 +00:00
parent a90af10932
commit d5457ae2dc
2 changed files with 35 additions and 12 deletions

View File

@@ -164,19 +164,26 @@ func parseCertOutput(output string, status *CertStatus) {
} }
} }
// certName returns the name certbot uses to store a certificate.
// Certbot strips the "*." prefix from wildcard domains, so
// *.example.com is stored under "example.com".
func certName(domain string) string {
return strings.TrimPrefix(domain, "*.")
}
// CertPath returns the fullchain.pem path for a domain. // CertPath returns the fullchain.pem path for a domain.
func CertPath(domain string) string { func CertPath(domain string) string {
return fmt.Sprintf("/etc/letsencrypt/live/%s/fullchain.pem", domain) return fmt.Sprintf("/etc/letsencrypt/live/%s/fullchain.pem", certName(domain))
} }
// KeyPath returns the privkey.pem path for a domain. // KeyPath returns the privkey.pem path for a domain.
func KeyPath(domain string) string { func KeyPath(domain string) string {
return fmt.Sprintf("/etc/letsencrypt/live/%s/privkey.pem", domain) return fmt.Sprintf("/etc/letsencrypt/live/%s/privkey.pem", certName(domain))
} }
// HAProxyCertPath returns the combined PEM path for HAProxy TLS termination. // HAProxyCertPath returns the combined PEM path for HAProxy TLS termination.
func HAProxyCertPath(domain string) string { func HAProxyCertPath(domain string) string {
return fmt.Sprintf("/etc/haproxy/certs/%s.pem", domain) return fmt.Sprintf("/etc/haproxy/certs/%s.pem", certName(domain))
} }
// BuildHAProxyCert concatenates fullchain.pem + privkey.pem into a single PEM // BuildHAProxyCert concatenates fullchain.pem + privkey.pem into a single PEM

View File

@@ -22,7 +22,7 @@ func TestHAProxyCertPath(t *testing.T) {
want string want string
}{ }{
{"example.com", "/etc/haproxy/certs/example.com.pem"}, {"example.com", "/etc/haproxy/certs/example.com.pem"},
{"*.example.com", "/etc/haproxy/certs/*.example.com.pem"}, {"*.example.com", "/etc/haproxy/certs/example.com.pem"},
{"sub.example.com", "/etc/haproxy/certs/sub.example.com.pem"}, {"sub.example.com", "/etc/haproxy/certs/sub.example.com.pem"},
} }
for _, tt := range tests { for _, tt := range tests {
@@ -34,15 +34,31 @@ func TestHAProxyCertPath(t *testing.T) {
} }
func TestCertPaths(t *testing.T) { func TestCertPaths(t *testing.T) {
domain := "example.com" tests := []struct {
certPath := CertPath(domain) domain string
keyPath := KeyPath(domain) wantCert string
wantKey string
if certPath != "/etc/letsencrypt/live/example.com/fullchain.pem" { }{
t.Errorf("CertPath = %q", certPath) {
"example.com",
"/etc/letsencrypt/live/example.com/fullchain.pem",
"/etc/letsencrypt/live/example.com/privkey.pem",
},
{
"*.example.com",
"/etc/letsencrypt/live/example.com/fullchain.pem",
"/etc/letsencrypt/live/example.com/privkey.pem",
},
}
for _, tt := range tests {
certPath := CertPath(tt.domain)
keyPath := KeyPath(tt.domain)
if certPath != tt.wantCert {
t.Errorf("CertPath(%q) = %q, want %q", tt.domain, certPath, tt.wantCert)
}
if keyPath != tt.wantKey {
t.Errorf("KeyPath(%q) = %q, want %q", tt.domain, keyPath, tt.wantKey)
} }
if keyPath != "/etc/letsencrypt/live/example.com/privkey.pem" {
t.Errorf("KeyPath = %q", keyPath)
} }
} }