Files
LambdaMail/services/protocols/internal/presentation/http/admin_tls_api.go
T
Lucas RayanandClaude Opus 5 136f9f339a fix(tls): o painel de seguranca estava vazio em qualquer modo menos um
/admin/security mostrava "Nao foi possivel carregar os dados" porque
/api/v1/admin/tls respondia 503. A rota so era ligada quando o certificado
vinha do watcher do acme.json do Traefik, o unico provider que tinha metodos
de estado proprios.

Nesta instalacao o TRAEFIK_ACME_DIR nao esta definido e o STATIC_CERT_PATH
tambem nao, portanto o servidor cai no certificado autoassinado efemero -- ou
seja, o painel calava-se exactamente no estado em que o operador mais precisa
de ser avisado.

- ProviderStatus deriva o estado de qualquer port.CertProvider, lendo a folha
  do certificado que ele realmente serve
- o painel passa a ser ligado sempre que ha JWT_SECRET
- passa a dizer quem emitiu e se e autoassinado, com um aviso proprio: nas
  portas de correio um autoassinado e recusado por qualquer cliente que
  verifique, o que pesa mais do que uma validade ainda longe

CheckSignatureFrom nao serve para detectar isto: tambem exige que o emissor
seja uma CA, coisa que uma folha descartavel nunca declara, e por isso dava a
fallback degradada como correctamente emitida.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 09:11:49 -03:00

128 lines
3.9 KiB
Go

package httppresentation
import (
"net/http"
"time"
)
// TlsStatusSource reports what the certificate watcher actually sees. The
// admin console used to receive fixed values - "VALID", 72 days - which told
// an operator the certificate was healthy no matter what the process held.
type TlsStatusSource interface {
EarliestExpiry() (host string, expiry time.Time, ok bool)
LastReload() time.Time
LastChange() time.Time
UnknownSNICount() int64
HasCertificateFor(host string) bool
}
// TlsCertificateInspector is optional extra detail: who issued the
// certificate, and whether it is self-signed. A source that cannot say simply
// does not implement it.
type TlsCertificateInspector interface {
CertificateSummary(host string) (issuer string, selfSigned bool, ok bool)
}
type adminTlsAPI struct {
source TlsStatusSource
mailHost string
tlsMode string
sessions *WebSessionVerifier
pollPeriod time.Duration
}
func (a *adminTlsAPI) handleStatus(w http.ResponseWriter, r *http.Request) {
token := bearerOrCookie(r, "lm_admin_session")
if token == "" {
writeError(w, http.StatusUnauthorized, "UNAUTHORIZED", "Admin session required")
return
}
if _, err := a.sessions.RequireSurface(token, "admin"); err != nil {
writeError(w, http.StatusUnauthorized, "UNAUTHORIZED", "Admin session required")
return
}
status := map[string]any{
"tls_mode": a.tlsMode,
"mail_host": a.mailHost,
"has_certificate": a.source.HasCertificateFor(a.mailHost),
"unknown_sni_total": a.source.UnknownSNICount(),
"last_reload": nil,
"last_change": nil,
"watcher_healthy": false,
"certificate_expires": nil,
"expires_in_days": nil,
"state": "UNKNOWN",
}
if last := a.source.LastReload(); !last.IsZero() {
status["last_reload"] = last.UTC().Format(time.RFC3339)
// Two missed polls is the smallest gap that is not a scheduling
// hiccup, and it is the symptom of a watcher gone blind (risk R2).
status["watcher_healthy"] = time.Since(last) < 3*a.pollPeriod
}
if change := a.source.LastChange(); !change.IsZero() {
status["last_change"] = change.UTC().Format(time.RFC3339)
}
if host, expiry, ok := a.source.EarliestExpiry(); ok {
remaining := time.Until(expiry)
status["certificate_expires"] = expiry.UTC().Format(time.RFC3339)
status["expires_in_days"] = int(remaining.Hours() / 24)
status["soonest_expiring_host"] = host
switch {
case remaining <= 0:
status["state"] = "EXPIRED"
case remaining < 24*time.Hour:
status["state"] = "CRITICAL"
case remaining < 7*24*time.Hour:
status["state"] = "WARNING"
default:
status["state"] = "OK"
}
}
// Who signed it, and whether anything would trust it. A self-signed
// certificate on 25 and 993 is refused by every client that verifies, so
// it outranks an expiry that is comfortably far away.
if inspector, ok := a.source.(TlsCertificateInspector); ok {
if issuer, selfSigned, found := inspector.CertificateSummary(a.mailHost); found {
status["issuer"] = issuer
status["self_signed"] = selfSigned
if selfSigned {
status["state"] = "SELF_SIGNED"
}
}
}
if !status["has_certificate"].(bool) {
status["state"] = "NO_CERTIFICATE"
}
writeJSON(w, http.StatusOK, status)
}
// SetAdminTlsAPI wires the TLS panel to the live certificate source.
func (r *Router) SetAdminTlsAPI(source TlsStatusSource, mailHost, tlsMode, sessionSecret string, pollPeriod time.Duration) {
if source == nil || sessionSecret == "" {
return
}
if pollPeriod <= 0 {
pollPeriod = time.Minute
}
r.tls = &adminTlsAPI{
source: source, mailHost: mailHost, tlsMode: tlsMode,
sessions: NewWebSessionVerifier(sessionSecret), pollPeriod: pollPeriod,
}
}
func (r *Router) handleAdminTls(w http.ResponseWriter, req *http.Request) {
if r.tls == nil {
writeError(w, http.StatusServiceUnavailable, "TLS_API_DISABLED",
"The TLS panel needs a certificate watcher and JWT_SECRET")
return
}
r.tls.handleStatus(w, req)
}