Set the GetCertificate callback on tls.Config and leave the Certificates list empty. Go then calls your function during handshakes, so your function can return a freshly loaded certificate after renewal. Cache it, reload when the files change, and guard it with a lock. No restart or dropped connections are needed.
Why a static config goes stale
The common pattern is to load a certificate pair once at startup, pass it in the Certificates field, and call ListenAndServeTLS. That certificate lives in memory until the process exits. When a certificate manager renews the files on disk, the server keeps presenting the old one. The outage shows up on the expiry date, as covered in the expiry outage checklist.
What GetCertificate does
The crypto/tls documentation describes GetCertificate as a function that returns a Certificate based on the given ClientHelloInfo. Two details matter:
- It is only called if the client supplies SNI information, or if the Certificates field is empty.
- If it is nil or returns nil, Go falls back to NameToCertificate and then to the best element of Certificates.
So to guarantee your callback runs for every handshake, leave Certificates empty. A client that sends no SNI will otherwise skip the callback and get the static entry.
The documentation also says that once a Certificate is returned it should not be modified. Replace the pointer rather than editing the struct.
A safe reload pattern
type reloader struct {
mu sync.RWMutex
cert *tls.Certificate
crt, key string
}
func (r *reloader) load() error {
c, err := tls.LoadX509KeyPair(r.crt, r.key)
if err != nil { return err }
r.mu.Lock(); r.cert = &c; r.mu.Unlock()
return nil
}
func (r *reloader) GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) {
r.mu.RLock(); defer r.mu.RUnlock()
return r.cert, nil
}
Call load once at startup and fail fast if it errors. Then trigger load again from a file watcher, a timer, or a signal handler.
Choosing the trigger
| Trigger | Strength | Weakness |
|---|---|---|
| Timer every few minutes | Simple, no extra dependency | Delay after renewal |
| File watcher | Fast | Symlink swaps in Kubernetes volumes need care |
| Signal such as SIGHUP | Explicit, works with deploy hooks | Needs a hook to send it |
In Kubernetes, a mounted Secret updates through a symlink swap, and only when you do not use subPath. See why subPath mounts miss renewals.
Keep the old certificate on failure
If a reload fails because a file is half written or the pair does not match, keep serving the previous certificate and log the error. LoadX509KeyPair returns an error when the pair is inconsistent, so you can detect it before swapping.
How to test it
- Start the server with a short-lived test certificate.
- Connect with a TLS client and note the serial number.
- Replace the files with a new pair and trigger the reload.
- Connect again without restarting the server. The serial should change.
- Break the files on purpose and confirm the server keeps the old certificate and logs the failure.
Client side
The same idea applies to mTLS clients: GetClientCertificate is called when a server requests a certificate from the client, and when set, the Certificates field is ignored. Use it to present rotated client certificates. See mTLS decisions.
Where Safeguard fits
Safeguard does not manage certificates. It provides SBOM and dependency scanning, reachability analysis and policy gates for Go modules and the rest of your build. Try the free scan at gold.safeguard.sh/scan.
Quick check
Do I need to restart when the certificate changes?
Not if you use GetCertificate and reload the pair yourself. New handshakes then use the new certificate.
Why leave Certificates empty?
The callback is only called when the client sends SNI or when Certificates is empty, so an empty list makes it run every time.
Do existing connections switch certificates?
No. A certificate is presented during the handshake, so open connections keep their session. Only new handshakes see the change.
Sources
Last reviewed: 19 October 2026.