feat: Per-service certs with opt-in wildcard provisioning
Remove automatic wildcard cert assumption. Each service gets its own cert by default. Wildcards are user-initiated via the Certificates page. Backend: - HAProxy L7 frontend uses cert directory (/etc/haproxy/certs/) instead of single wildcard file — loads all PEMs, serves by SNI - Cert status API shows every registered service individually with coveredBy field when a wildcard cert covers the domain - Reconciliation filters L7 routes to services that have a cert - No auto-provisioning in reconciliation (just warnings) Frontend: - Certificates page shows per-service cert status - "Provision" button for individual certs - "Add Wildcard" form for opt-in wildcard provisioning - Fixed CloudflareComponent type errors from cert API changes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -21,63 +21,49 @@ func (api *API) CertStatus(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
cfToken := api.getCloudflareToken()
|
||||
|
||||
// Derive gateway domain for wildcard cert
|
||||
gatewayDomain := ""
|
||||
if parts := strings.SplitN(centralDomain, ".", 2); len(parts) == 2 {
|
||||
gatewayDomain = parts[1]
|
||||
}
|
||||
|
||||
// Gather cert statuses
|
||||
// Gather cert statuses for all registered services that need TLS termination
|
||||
certs := []map[string]any{}
|
||||
seen := map[string]bool{}
|
||||
|
||||
// Central domain cert
|
||||
if centralDomain != "" {
|
||||
status := api.certbot.GetStatus(centralDomain)
|
||||
certs = append(certs, map[string]any{
|
||||
"domain": centralDomain,
|
||||
"type": "central",
|
||||
"cert": status,
|
||||
})
|
||||
}
|
||||
|
||||
// Wildcard cert
|
||||
if gatewayDomain != "" {
|
||||
wildcardDomain := "*." + gatewayDomain
|
||||
status := api.certbot.GetStatus(gatewayDomain)
|
||||
certs = append(certs, map[string]any{
|
||||
"domain": wildcardDomain,
|
||||
"type": "wildcard",
|
||||
"cert": status,
|
||||
})
|
||||
}
|
||||
|
||||
// Registered service certs (for services that need TLS termination)
|
||||
svcs, _ := api.services.List()
|
||||
for _, svc := range svcs {
|
||||
if svc.TLS != "terminate" || svc.Domain == "" {
|
||||
continue
|
||||
}
|
||||
// Skip if covered by wildcard
|
||||
if gatewayDomain != "" && strings.HasSuffix(svc.Domain, "."+gatewayDomain) {
|
||||
if seen[svc.Domain] {
|
||||
continue
|
||||
}
|
||||
seen[svc.Domain] = true
|
||||
|
||||
status := api.certbot.GetStatus(svc.Domain)
|
||||
certs = append(certs, map[string]any{
|
||||
|
||||
entry := map[string]any{
|
||||
"domain": svc.Domain,
|
||||
"type": "service",
|
||||
"service": svc.Name,
|
||||
"source": svc.Source,
|
||||
"cert": status,
|
||||
})
|
||||
}
|
||||
|
||||
// Check if covered by a wildcard cert
|
||||
parts := strings.SplitN(svc.Domain, ".", 2)
|
||||
if len(parts) == 2 && !status.Exists {
|
||||
wildcardBase := parts[1]
|
||||
wildcardStatus := api.certbot.GetStatus(wildcardBase)
|
||||
if wildcardStatus.Exists {
|
||||
entry["coveredBy"] = "*." + wildcardBase
|
||||
}
|
||||
}
|
||||
|
||||
certs = append(certs, entry)
|
||||
}
|
||||
|
||||
respondJSON(w, http.StatusOK, map[string]any{
|
||||
"configured": centralDomain != "",
|
||||
"domain": centralDomain,
|
||||
"gatewayDomain": gatewayDomain,
|
||||
"canProvision": cfToken != "" && email != "",
|
||||
"hasToken": cfToken != "",
|
||||
"hasEmail": email != "",
|
||||
"certs": certs,
|
||||
"configured": centralDomain != "",
|
||||
"domain": centralDomain,
|
||||
"canProvision": cfToken != "" && email != "",
|
||||
"hasToken": cfToken != "",
|
||||
"hasEmail": email != "",
|
||||
"certs": certs,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user