feat: Add Certificates page + stop auto-provisioning certs
New dedicated Certificates page in Wild Central UI showing: - All tracked certs (central, wildcard, per-service) - Status (valid with days remaining, or missing) - Per-domain Provision button - Renew All button Cert provisioning is now user-initiated only — reconciliation logs warnings about missing certs but does NOT auto-provision. This prevents unexpected wildcard cert requests on startup. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -146,79 +146,34 @@ func (api *API) reconcileNetworking() {
|
||||
)
|
||||
}
|
||||
|
||||
// ensureTLSCerts checks that TLS certificates exist for HTTP services
|
||||
// that need Central to terminate TLS. Uses wildcard certs where possible.
|
||||
// ensureTLSCerts logs which certificates are missing for registered services.
|
||||
// Does NOT auto-provision — cert provisioning is user-initiated via the
|
||||
// Certificates page. This just logs warnings so the operator knows what's needed.
|
||||
func (api *API) ensureTLSCerts(globalCfg *config.GlobalConfig, svcs []services.Service) {
|
||||
// Determine the gateway domain from central domain (e.g., central.payne.io → payne.io)
|
||||
centralDomain := globalCfg.Cloud.Central.Domain
|
||||
gatewayDomain := ""
|
||||
if parts := strings.SplitN(centralDomain, ".", 2); len(parts) == 2 {
|
||||
gatewayDomain = parts[1] // e.g., "payne.io"
|
||||
if parts := strings.SplitN(globalCfg.Cloud.Central.Domain, ".", 2); len(parts) == 2 {
|
||||
gatewayDomain = parts[1]
|
||||
}
|
||||
|
||||
// Check if we have a Cloudflare token for cert provisioning
|
||||
cfToken := api.getCloudflareToken()
|
||||
if cfToken == "" {
|
||||
slog.Debug("reconcile/tls: no Cloudflare token, skipping cert provisioning")
|
||||
return
|
||||
}
|
||||
|
||||
email := globalCfg.Operator.Email
|
||||
if email == "" {
|
||||
slog.Debug("reconcile/tls: no operator email, skipping cert provisioning")
|
||||
return
|
||||
}
|
||||
|
||||
// Check if wildcard cert exists for the gateway domain
|
||||
wildcardCertPath := "/etc/haproxy/certs/wildcard.pem"
|
||||
if gatewayDomain != "" {
|
||||
wildcardDomain := "*." + gatewayDomain
|
||||
// Check if cert exists at the standard wildcard path or per-domain path
|
||||
certPath := certbot.HAProxyCertPath(gatewayDomain)
|
||||
if _, err := os.Stat(certPath); err == nil {
|
||||
// Per-domain cert exists, use it as wildcard
|
||||
slog.Debug("reconcile/tls: wildcard cert exists", "domain", gatewayDomain, "path", certPath)
|
||||
} else if _, err := os.Stat(wildcardCertPath); err == nil {
|
||||
slog.Debug("reconcile/tls: wildcard cert exists at default path")
|
||||
} else {
|
||||
// No wildcard cert — provision one
|
||||
slog.Info("reconcile/tls: provisioning wildcard cert", "domain", wildcardDomain)
|
||||
if err := api.certbot.EnsureCredentials(cfToken); err != nil {
|
||||
slog.Error("reconcile/tls: failed to write certbot credentials", "error", err)
|
||||
return
|
||||
}
|
||||
if err := api.certbot.Provision(wildcardDomain, email); err != nil {
|
||||
slog.Warn("reconcile/tls: failed to provision wildcard cert", "domain", wildcardDomain, "error", err)
|
||||
// Fall through — try per-domain certs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For services NOT under the gateway domain, provision individual certs
|
||||
for _, svc := range svcs {
|
||||
if svc.TLS != services.TLSTerminate || svc.Domain == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this domain is covered by the gateway wildcard
|
||||
// Check if covered by wildcard
|
||||
if gatewayDomain != "" && strings.HasSuffix(svc.Domain, "."+gatewayDomain) {
|
||||
continue // Covered by wildcard
|
||||
}
|
||||
|
||||
// Check if per-domain cert exists
|
||||
certPath := certbot.HAProxyCertPath(svc.Domain)
|
||||
if _, err := os.Stat(certPath); err == nil {
|
||||
continue // Cert already exists
|
||||
}
|
||||
|
||||
// Provision individual cert
|
||||
slog.Info("reconcile/tls: provisioning cert", "domain", svc.Domain)
|
||||
if err := api.certbot.EnsureCredentials(cfToken); err != nil {
|
||||
slog.Error("reconcile/tls: failed to write certbot credentials", "error", err)
|
||||
wildcardCert := certbot.HAProxyCertPath(gatewayDomain)
|
||||
if _, err := os.Stat(wildcardCert); err != nil {
|
||||
slog.Warn("reconcile/tls: no wildcard cert for gateway domain — provision via Certificates page",
|
||||
"service", svc.Name, "domain", svc.Domain, "needed", "*."+gatewayDomain)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := api.certbot.Provision(svc.Domain, email); err != nil {
|
||||
slog.Warn("reconcile/tls: failed to provision cert", "domain", svc.Domain, "error", err)
|
||||
|
||||
// Check individual cert
|
||||
if _, err := os.Stat(certbot.HAProxyCertPath(svc.Domain)); err != nil {
|
||||
slog.Warn("reconcile/tls: no cert for service — provision via Certificates page",
|
||||
"service", svc.Name, "domain", svc.Domain)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user