package server import ( "context" "log" "time" ) // renewalWindow re-issues certificates this long before they expire. const renewalWindow = 30 * 24 * time.Hour // StartRenewal keeps certificates fresh: it re-issues those close to expiry, // retries failures, recovers issuances interrupted by a restart, and migrates // certificates from a previous issuer (say the local CA) once a real issuer // is configured. It runs at startup and every 6 hours. func (a *API) StartRenewal(ctx context.Context) { go func() { a.renewDue(true) ticker := time.NewTicker(6 * time.Hour) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: a.renewDue(false) } } }() } func (a *API) renewDue(firstRun bool) { current := a.issuerName() for _, tunnel := range a.Store.Tunnels() { cert := tunnel.Certificate if cert == nil || cert.CSR == "" { continue } switch { case cert.State == "issuing": // An in-flight issuance cannot survive a restart; retry on the // first pass after startup. if firstRun { a.runIssue(tunnel.ID, tunnel.Hostname, cert.CSR, cert.ID) } case certReady(cert): needs := normalizedIssuer(cert.Issuer) != current if !needs && cert.Expiry != "" { if expiry, err := time.Parse(time.RFC3339, cert.Expiry); err == nil { needs = time.Until(expiry) < renewalWindow } } if needs { log.Printf("renewing certificate for %s (issuer %q, expires %s)", tunnel.Hostname, cert.Issuer, cert.Expiry) a.runIssue(tunnel.ID, tunnel.Hostname, cert.CSR, cert.ID) } case cert.State == "failed": a.runIssue(tunnel.ID, tunnel.Hostname, cert.CSR, cert.ID) } } } // normalizedIssuer treats certificates from before the issuer was recorded // as local-CA certificates. func normalizedIssuer(issuer string) string { if issuer == "" { return "local" } return issuer }