From d5457ae2dc95a52c0c7aec7e2bf7c2d58dde5600 Mon Sep 17 00:00:00 2001 From: Paul Payne Date: Thu, 10 Sep 2026 22:23:05 +0000 Subject: [PATCH] fix: Normalize wildcard domain paths for certbot cert storage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- internal/certbot/manager.go | 13 +++++++++--- internal/certbot/manager_test.go | 34 +++++++++++++++++++++++--------- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/internal/certbot/manager.go b/internal/certbot/manager.go index 8af352c..181424e 100644 --- a/internal/certbot/manager.go +++ b/internal/certbot/manager.go @@ -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. 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. 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. 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 diff --git a/internal/certbot/manager_test.go b/internal/certbot/manager_test.go index 8aae25f..42e09c3 100644 --- a/internal/certbot/manager_test.go +++ b/internal/certbot/manager_test.go @@ -22,7 +22,7 @@ func TestHAProxyCertPath(t *testing.T) { 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"}, {"sub.example.com", "/etc/haproxy/certs/sub.example.com.pem"}, } for _, tt := range tests { @@ -34,15 +34,31 @@ func TestHAProxyCertPath(t *testing.T) { } func TestCertPaths(t *testing.T) { - domain := "example.com" - certPath := CertPath(domain) - keyPath := KeyPath(domain) - - if certPath != "/etc/letsencrypt/live/example.com/fullchain.pem" { - t.Errorf("CertPath = %q", certPath) + tests := []struct { + domain string + wantCert string + wantKey string + }{ + { + "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", + }, } - if keyPath != "/etc/letsencrypt/live/example.com/privkey.pem" { - t.Errorf("KeyPath = %q", keyPath) + 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) + } } }