Why does a Go TLS server use tls.Config.GetCertificate rather than Certificates to pick up a renewed key pair?
answer
- the file changed but the process did not
- startup once versus every handshake
- a callback taking *tls.ClientHelloInfo
- leave Certificates empty so it always runs
- old connections keep what they negotiated
basics
~20 stls.Config.GetCertificate is a callback the server consults during each incoming handshake, so a renewed key pair takes effect on the next connection. The Certificates field is read when the server starts, so replacing the files on disk changes nothing until a restart.
solid answer
~50 s`tls.Config.Certificates` is a plain slice the TLS stack reads out of the config; whatever was loaded at startup is what every handshake uses until the process dies. `tls.Config.GetCertificate` is instead a function field of type `func(*tls.ClientHelloInfo) (*tls.Certificate, error)` that the server calls while a handshake is in progress, so it can hand back whichever key pair is current at that instant. That is the whole basis of downtime-free rotation: some other part of the program loads the renewed pair with `tls.LoadX509KeyPair` and publishes it, and the very next handshake picks it up while connections already established carry on with the material they negotiated. Leave `Certificates` empty so the callback is always the source of truth, and keep the parsed certificate in memory — the callback runs on the handshake path and must not do disk I/O.
go deeper
Be ready to say plainly that Certificates is loaded once at startup while GetCertificate is a callback the server runs during each handshake, and that this is why a renewed file on disk does nothing until the process either restarts or reloads.
Explain the callback's signature and when the TLS stack actually consults it, why the parsed certificate must be cached in memory rather than reread per handshake, and what a returned error does to that client's handshake.
Show the operational consequence: the swap only affects new handshakes, so a failed reload must leave the previous certificate serving, and the retiring chain has to stay acceptable until existing connections drain.
Own whether services in your fleet build this path at all, and who maintains it — a per-handshake callback is a small surface, but forty teams each writing their own version of it is not.
## The problem rotation has to solve A TLS server proves its identity with a **key pair**: a private key plus a certificate chain that binds a public key to a name. Certificates expire, and automated issuance means they are replaced often — every few weeks in many environments. The renewal itself writes new PEM files somewhere on disk. The question this leaf is about is what happens *next*: how the running Go process starts using the new material without dropping traffic. ## Why the static field cannot do it The usual first program looks like this: load the pair once and put it in the config. ```go cert, err := tls.LoadX509KeyPair("cert.pem", "key.pem") cfg := &tls.Config{Certificates: []tls.Certificate{cert}} ``` `tls.LoadX509KeyPair` reads and parses both files and returns a `tls.Certificate` value — a struct holding the DER-encoded chain (`Certificate [][]byte`) and the parsed private key (`PrivateKey crypto.PrivateKey`). Once that value is inside `Certificates`, the TLS stack simply reads the slice on every handshake. Nothing re-reads the files. Overwrite `cert.pem` with a freshly issued certificate and the running server keeps presenting the old one until it is restarted — and when the old one finally expires, every client starts failing verification even though a perfectly good replacement has been sitting on disk for weeks. That is the outage this callback exists to prevent. ## What the callback changes `tls.Config.GetCertificate` has the type: ```go GetCertificate func(*tls.ClientHelloInfo) (*tls.Certificate, error) ``` The server invokes it *during* a handshake, after it has read the client's ClientHello. The argument, `*tls.ClientHelloInfo`, describes that specific client: `ServerName` carries the SNI hostname the client asked for, along with the cipher suites and signature schemes it supports. The callback returns the certificate to present. Because it is consulted per handshake, whatever it returns at 10:00 can differ from what it returns at 10:01, with no restart in between. One wiring detail matters: the callback is consulted when the client supplies SNI or when `Certificates` is empty. If you populate both `Certificates` and `GetCertificate`, a client that sends no SNI can end up served from the static slice — the stale pair you were trying to escape. The reliable shape is to leave `Certificates` nil and let the callback be the only source of certificates. ## What the callback must not do The callback runs on the connection-establishment path, once per handshake, on the goroutine handling that connection. Reading and parsing PEM files there would put two file opens and an RSA or ECDSA key parse in front of every new connection, and would make the server's latency depend on the filesystem. The correct shape is to keep an already-parsed `tls.Certificate` in memory and have the callback do nothing but hand back a pointer to it. Reloading is a separate, rare activity — triggered by a timer, a signal, or a filesystem watch — that parses the new files and publishes the result. Because two different goroutines are now touching that shared value, publishing it needs real synchronisation rather than a plain assignment; that is a topic in its own right. Errors returned from the callback abort the handshake for that client, so a reload that fails should leave the previously published certificate in place rather than publishing a broken one. A renewal job that writes a truncated file should cost you nothing: the parse fails, you log and alert, and the server keeps serving with material that is still valid. ## The connection lifecycle, which is what makes it seamless A TLS connection negotiates its certificate exactly once, at handshake time. Swapping the published certificate therefore has no effect on connections that already exist — they continue with the chain and keys they agreed on, and only new handshakes see the replacement. This is why in-place rotation causes no visible interruption at all, and also why the *retiring* material still has to be acceptable for as long as old connections and cached client state live. Rotation is a gradual cutover, not an instant one. ## Related hooks The same idea appears elsewhere in `crypto/tls`. `GetClientCertificate` lets a client choose the certificate it presents per connection, which is what a service doing mutual TLS with a short-lived client certificate needs. `GetConfigForClient` goes further and returns a whole `*tls.Config` per ClientHello. All three exist for the same reason: decisions that must stay live cannot be frozen into a struct at startup.
- Should the GetCertificate callback read the certificate and key files from disk each time it is called?No. It runs once per incoming handshake, so file opens and a private-key parse would sit in front of every new connection and tie handshake latency to the filesystem. Parse once in a separate reload step, keep the resulting `tls.Certificate` in memory, and let the callback do nothing but return a pointer to the current one.
- What happens to connections that are already established when the certificate is swapped?Nothing. A TLS connection negotiates its certificate during its handshake and keeps it for the life of the connection, so live connections carry on with the retiring material and only new handshakes see the replacement. That is why the swap is invisible, and also why the old chain must stay acceptable until those connections drain.
- If tls.Config.Certificates is non-empty, is GetCertificate guaranteed to be called?No. The callback is consulted when the client supplies SNI or when `Certificates` is empty, so a client that sends no server name can be served from the static slice instead — exactly the stale pair you were trying to avoid. Leave `Certificates` nil so the callback is unambiguously the source of truth.
Certificates is a printed sign bolted to the wall at opening time; GetCertificate is a receptionist who checks the current sign each time somebody walks in.
saying these in an interview costs you the question
- Says the TLS server rereads the PEM files on every request
- Claims overwriting cert.pem takes effect in a running process
- Thinks rotation requires dropping all live connections
- Puts a blocking file read and key parse inside the handshake callback
- Assumes GetCertificate is called once at server startup