// certreload.go — hot-reloading server cert for the agent plane. // // The agent-plane server cert (issued by the fleet CA) is rotated OUT of process // by the `agent-cert-renewer` sidecar running `step ca renew` (docker-compose.yml, // profile cert-auto). For that rotation to take effect WITHOUT restarting the // manager — which would drop every live agent stream — the TLS config serves the // cert through GetCertificate instead of a fixed Certificates slice, and this // reloader re-reads server.crt/server.key from disk when they change. // // Reload is lazy + modtime-gated: each handshake checks server.crt's modtime and // re-reads the pair only when it advances. A failed reload (a half-written pair // mid-renewal, transient bad perms) is logged and the LAST GOOD cert keeps // serving — a renewer hiccup must never take the agent plane down — and because a // failed reload does not advance the cached modtime, the next handshake retries // until the write settles. package main import ( "crypto/tls" "log" "os" "sync" "time" ) type certReloader struct { certPath, keyPath string mu sync.RWMutex cert *tls.Certificate modTime time.Time } // newCertReloader loads the pair once (fail-fast if missing/bad at startup) and // returns a reloader whose GetCertificate hook is wired into the TLS config. func newCertReloader(certPath, keyPath string) (*certReloader, error) { r := &certReloader{certPath: certPath, keyPath: keyPath} if err := r.reload(); err != nil { return nil, err } return r, nil } func (r *certReloader) reload() error { c, err := tls.LoadX509KeyPair(r.certPath, r.keyPath) if err != nil { return err } var mt time.Time if fi, err := os.Stat(r.certPath); err == nil { mt = fi.ModTime() } r.mu.Lock() r.cert, r.modTime = &c, mt r.mu.Unlock() return nil } // GetCertificate is the tls.Config hook. It reloads when server.crt's modtime has // advanced past the cached copy, otherwise returns the cached cert unchanged. func (r *certReloader) GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) { r.mu.RLock() cur, mt := r.cert, r.modTime r.mu.RUnlock() if fi, err := os.Stat(r.certPath); err == nil && fi.ModTime().After(mt) { if err := r.reload(); err != nil { log.Printf("agent plane: cert reload failed, keeping previous: %v", err) } else { r.mu.RLock() cur = r.cert r.mu.RUnlock() log.Printf("agent plane: reloaded rotated server cert %s", r.certPath) } } return cur, nil }