tls.Config GetClientCertificate for Proactive Credential Rotation
0 reputation · 18 Apr 2021, 17:41 UTC
0 reputation · 18 Apr 2021, 17:41 UTC
Implementing mutual TLS (mTLS) in Go typically involves configuring the tls.Config struct. To maintain least-privilege access and avoid service interruption, credentials must be rotated before they expire. While the GetClientCertificate callback allows for dynamic certificate loading without restarting the application, the crypto/tls package evaluates certificate validity during the handshake process.
There is a design gap regarding the timing of these updates. The standard library provides no built-in event or notification system to trigger a refresh based on a specific time-to-expiry threshold. This leaves the application to either rely on an external timer or wait for a handshake failure to realize a certificate has expired.
What is the recommended pattern for integrating a proactive expiration check within the GetClientCertificate callback to ensure rotation occurs before the certificate becomes invalid? Is there a standard way to signal the net/http.Transport to refresh its cached certificate state?
34775 reputation · 19 Apr 2021, 05:32 UTC
Use a thread‑safe wrapper around GetClientCertificate that checks the certificate’s NotAfter date on every handshake. If the remaining lifetime is below a configurable threshold, reload the key pair from disk and replace the cached *tls.Certificate. The http.Transport will automatically use the updated tls.Config for new connections; no extra signal is required.
tls.Config (or the certificate stored in it) is safe for concurrent use – the struct’s fields are read atomically during handshake.sync.RWMutex to allow concurrent reads and safe writes.GetClientCertificate, acquire a read lock, inspect cert.NotAfter, and if the time left is below threshold, upgrade to a write lock, reload the key pair, and replace the cached value.threshold before expiry to avoid any race with the first handshake after the old cert expires.type CertReloader struct {
mu sync.RWMutex
certPath string
keyPath string
cert *tls.Certificate
threshold time.Duration
}
func NewCertReloader(certPath, keyPath string, threshold time.Duration) (*CertReloader, error) {
r := &CertReloader{certPath: certPath, keyPath: keyPath, threshold: threshold}
if err := r.reload(); err != nil {
return nil, err
}
return r, nil
}
func (r *CertReloader) reload() error {
cr, err := tls.LoadX509KeyPair(r.certPath, r.keyPath)
if err != nil {
return err
}
r.mu.Lock()
defer r.mu.Unlock()
r.cert = &cr
return nil
}
func (r *CertReloader) GetClientCertificate(_ *tls.CertificateRequestInfo) (*tls.Certificate, error) {
r.mu.RLock()
cert := r.cert
r.mu.RUnlock()
// Quick check – if the cert is about to expire, reload.
if time.Until(cert.Leaf.NotAfter) < r.threshold {
if err := r.reload(); err != nil {
// Log and fall back to the old cert.
log.Printf("cert reload failed: %v", err)
} else {
// After reload, fetch the new cert.
r.mu.RLock()
cert = r.cert
r.mu.RUnlock()
}
}
return cert, nil
}
// Usage
reloader, _ := NewCertReloader("/path/to/client.crt", "/path/to/client.key", 24*time.Hour)
transport := &http.Transport{TLSClientConfig: &tls.Config{GetClientCertificate: reloader.GetClientCertificate}}
client := &http.Client{Transport: transport}
The http.Transport does not cache the client certificate; it reads the tls.Config on each handshake. Therefore, simply updating the GetClientCertificate function (or the tls.Config it points to) is sufficient. If you replace the entire Transport instance, make sure to swap it in the http.Client atomically to avoid races.
cert.NotBefore and cert.NotAfter to confirm the callback sees the current cert.What pre‑expiry window do you want to use for automatic reload (e.g., 24 h, 12 h, 1 h)? Adjusting threshold in the reloader changes the reload timing.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.