skip to content

When should a Go TLS client set GetClientCertificate instead of tls.Config.Certificates?

level: middleimportance: nice to knowfreq 22%

answer

  1. static list versus a callback
  2. runs once per handshake
  3. the server describes what it will accept
  4. setting it makes the other field irrelevant
  5. an empty certificate means 'I have none'

basics

~20 s

Set GetClientCertificate when the certificate can change or depends on who is asking. It is a callback run at every handshake where the server requests a certificate, it receives the server's request details, and it takes precedence: Certificates is ignored once it is set.

solid answer

~40 s

`tls.Config.Certificates` is a static list, fixed when the config is built, which is fine for a process whose identity never changes while it runs. `GetClientCertificate` is `func(*tls.CertificateRequestInfo) (*tls.Certificate, error)`, called on each handshake in which the server asks for a client certificate, and when it is set the `Certificates` field is ignored entirely. Use it for rotation — the callback reads whatever the credential store currently holds, so a renewed certificate is picked up without rebuilding the config or the `http.Transport` — and for choosing among identities, since `CertificateRequestInfo` carries `AcceptableCAs` and `SignatureSchemes` and has a `SupportsCertificate` method that tells you whether a candidate would be accepted. Returning an error aborts the handshake; returning a non-nil but empty `tls.Certificate` sends none and lets the server decide.

code

go · 11 lines
go
cfg := &tls.Config{
	GetClientCertificate: func(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) {
		cert := store.Current() // *tls.Certificate, replaced on rotation
		if err := cri.SupportsCertificate(cert); err != nil {
			return &tls.Certificate{}, nil // send none; the server decides
		}
		return cert, nil
	},
}

client := &http.Client{Transport: &http.Transport{TLSClientConfig: cfg}}

go deeper

for a junior

Know that a Go client presents its certificate through the Certificates field of its tls.Config, and that a callback form exists for cases where the certificate is not fixed for the life of the process.

for a middle

Be ready to give the callback's signature and firing point, say that it overrides Certificates entirely, and name rotation and multiple identities as the two reasons to use it.

for a senior

Show the operational edge: the callback runs per handshake and concurrently, so pooled connections keep the old identity after rotation and the callback must not block on I/O.

for a principal

Own the credential model — how short certificate lifetimes, connection lifetimes and the failure behaviour when the credential store is down combine into an availability decision, not just a security one.

## Two ways a Go client supplies its certificate On the dialing side, a client certificate reaches the handshake through one of two `tls.Config` fields. **`Certificates []tls.Certificate`** is the simple one: load a keypair with `tls.LoadX509KeyPair` at start-up, put it in the slice, done. The config is immutable in practice once handshakes are in flight, so this suits a process that holds one identity for its whole lifetime. **`GetClientCertificate func(*tls.CertificateRequestInfo) (*tls.Certificate, error)`** is a callback the runtime invokes on every handshake where the peer sends a CertificateRequest. When it is set, `Certificates` is not consulted at all — a detail worth stating out loud, because leaving both populated and expecting a fallback is a common misreading. ## Why the callback exists **Rotation.** Internal certificates are short-lived by design; a process that ran for a week under a static `Certificates` slice would be presenting an expired credential long before it restarted. With the callback, the current certificate is fetched at handshake time from whatever holds it — a value swapped under a mutex, an atomic pointer, a file reloaded on a timer — and the config, the `http.Transport` and the connection pool all stay untouched. **Selection.** A process that holds more than one identity has to decide which to present. The `*tls.CertificateRequestInfo` argument describes the server's request: `AcceptableCAs` is the list of issuer names the server said it would accept (it may be empty, meaning the server did not restrict), `SignatureSchemes` names the algorithms it can verify, and `Version` is the negotiated protocol version. Rather than interpreting those by hand, call `cri.SupportsCertificate(cert)`, which returns nil when that candidate would satisfy the request. **Deliberately sending nothing.** Returning a non-nil but empty `*tls.Certificate` means 'I have none'; the server then applies its own `ClientAuth` policy and either continues or aborts. Returning an error, by contrast, aborts the handshake on the client side and that error surfaces from the `Dial` or `RoundTrip` call. The distinction matters when the credential store is temporarily unavailable: erroring fails fast and loudly, sending nothing produces a server-side rejection that looks like a policy problem. ## Rotation and connection reuse The callback runs per handshake, not per request. An `http.Transport` keeps idle keep-alive connections, and those connections keep the identity they were established with — so after a rotation, requests continue flowing over old connections presenting the old certificate until those connections are closed. Usually that is harmless, since the previous certificate is still valid for a while; when it is not, the levers are `http.Transport.IdleConnTimeout` to bound how long a stale connection can linger and `CloseIdleConnections` to drop them at the moment of rotation. This is also why 'we rotated but the peer still sees the old identity' is a connection-lifetime question rather than a TLS one. ## Where the callback must be careful It runs on the handshake path, so it is on the latency critical path of every new connection and must not block on network I/O or hold a heavily contended lock. The usual shape is a store that keeps the parsed `*tls.Certificate` ready and refreshes it in the background, with the callback doing nothing but an atomic read and a `SupportsCertificate` check. Parsing PEM inside the callback turns every new connection into a parse. It is also called concurrently — several goroutines can be dialing at once — so whatever it reads from must be safe for concurrent access. ## Choosing between them Static `Certificates` is the right default: fewer moving parts, nothing to get wrong under concurrency, and completely adequate when the credential outlives the process. Reach for `GetClientCertificate` when the identity's lifetime is shorter than the process's, when more than one identity is in play, or when you want a single place to observe how often the client is actually being asked for a certificate — the callback firing is direct evidence that the server requests one, which is a useful signal while migrating a service off shared secrets.

  • A process reloads a rotated certificate but the peer keeps seeing the old identity. Why?
    `GetClientCertificate` runs per handshake, and pooled keep-alive connections already handshaked. Requests reusing them keep presenting the previous certificate until the connections close. Bound that window with `http.Transport.IdleConnTimeout`, or call `CloseIdleConnections` on the transport at the moment of rotation so the next request forces a fresh handshake.
  • What does the CertificateRequestInfo tell you about which certificate to send?
    `AcceptableCAs` lists the issuer names the server said it will accept, `SignatureSchemes` the algorithms it can verify, and `Version` the negotiated protocol version. `AcceptableCAs` may be empty, meaning the server restricted nothing, so do not treat an empty list as 'accepts none'. `SupportsCertificate` applies all of it to a candidate for you.
  • What happens if GetClientCertificate returns an error?
    The handshake is aborted and that error propagates out of whatever initiated the connection — the `Dial` call, or the `RoundTrip` behind an `http.Client` request. It is a client-side failure with no server involvement, which is the right behaviour when the credential store is unavailable and you would rather fail loudly than connect anonymously.

saying these in an interview costs you the question

  • Thinks Certificates is used as a fallback when the callback is set
  • Returns nil rather than an empty certificate to send none
  • Parses PEM from disk inside the handshake callback
  • Expects rotation to affect already-open pooled connections
  • Assumes the callback runs once per request