keyload is a Go library that reloads TLS certificates, keys and CA bundles while your servers and clients keep running. When cert-manager renews a certificate, the next handshake uses it. When a bad file shows up, the last good one keeps serving.
Reloads the certificate you present and the CAs you trust, on servers and clients. That's the part crypto/tls has no hook for.
Expired certificates, mismatched keys and half-written files are rejected. The last good certificate keeps serving and you get an event and a metric.
Watches Secret volumes correctly, including the ..data symlink swap. CA rotations get an overlap window so peers keep working.
crypto/tls still does the verification. No dependencies; gRPC support is a separate module.
Point keyload at your files and hand its configs to your server and client. That's the whole setup.
r, err := keyload.New(keyload.Files{ Cert: "/tls/tls.crt", Key: "/tls/tls.key", Roots: "/tls/ca.crt", // to verify servers ClientCAs: "/tls/ca.crt", // to verify clients }) if err != nil { log.Fatal(err) // the first load must be valid } defer r.Close()
// Server: certificate and client CAs follow reloads. srv := &http.Server{Addr: ":8443", TLSConfig: r.ServerConfig(&tls.Config{ ClientAuth: tls.RequireAndVerifyClientCert, NextProtos: []string{"h2", "http/1.1"}, })} go srv.ListenAndServeTLS("", "") // cert comes from TLSConfig // Client: each connection uses the current files. tr := &http.Transport{ForceAttemptHTTP2: true} if err := r.ConfigureTransport(tr, nil); err != nil { log.Fatal(err) }
No certificates to try it with? go run github.com/rossedman/keyload/examples/gencerts@latest -dir ./certs writes a throwaway CA and a certificate for localhost; add -rotate to issue a new one. The getting started guide walks through running, rotating, breaking and testing.
Mount a Kubernetes Secret as a whole directory. A subPath mount never sees updates, from keyload or anything else.
Everything lives in one immutable snapshot. Handshakes read it without locks; a reload builds a new one off to the side and swaps it in only if it passes every check.
A file in a watched directory, SIGHUP with OnSignal, a timer with PollEvery, or a call to r.Reload(ctx).
If a read fails or the files look half-written, wait briefly and retry, up to 3 times.
Every check in the table must pass. Unchanged files are a no-op.
The new snapshot goes live atomically with the next generation number. On failure, the old one stays and you get an event.
| Check | Error |
|---|---|
| Key matches the certificate | ErrKeyMismatch |
| Chain inside its validity window | ErrExpiredErrNotYetValid |
At least MinRemaining(d) left | ErrExpiresTooSoon |
Chains to the new roots (RequireChain()) | ErrChainVerification |
| CA bundles aren't empty | ErrEmptyBundle |
| No truncated PEM | ErrTruncatedPEM |
Works with any crypto/tls server. Each handshake gets a config built from the current snapshot, cached per reload, so it adds about 2.5 ns and no allocations. Session resumption keeps working across reloads. For HTTP/2, set NextProtos here.
srv := &http.Server{TLSConfig: r.ServerConfig(&tls.Config{
ClientAuth: tls.RequireAndVerifyClientCert,
NextProtos: []string{"h2", "http/1.1"},
})}
srv.ListenAndServeTLS("", "")Every new connection gets a fresh config with the current roots and client certificate, and crypto/tls verifies the server. If the transport uses an HTTPS proxy, ConfigureTransport returns ErrProxyConfigured, because http.Transport skips the custom dialer for proxied requests. Use Config mode there.
tr := &http.Transport{ForceAttemptHTTP2: true}
if err := r.ConfigureTransport(tr, nil); err != nil {
log.Fatal(err)
}
client := &http.Client{Transport: tr}
// Or for raw dials:
dial := r.DialTLSContext(&tls.Config{ServerName: "db.internal"})Transport credentials for grpc-go, in a separate module so the core stays dependency-free. New connections pick up reloads; existing ones keep the certificates they started with.
import "github.com/rossedman/keyload/grpccreds" srv := grpc.NewServer(grpc.Creds(grpccreds.New(r, grpccreds.WithTLSConfig(&tls.Config{ ClientAuth: tls.RequireAndVerifyClientCert, })))) conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(grpccreds.New(r)))
For libraries that take a *tls.Config and nothing else. keyload sets InsecureSkipVerify and verifies the server in VerifyConnection with exactly the options crypto/tls uses, on every handshake. ServerName is required. CI checks it against crypto/tls on 2,000 generated chains.
cfg, err := r.ClientConfig(&tls.Config{ServerName: "api.internal"}) if err != nil { log.Fatal(err) } tr := &http.Transport{Proxy: http.ProxyFromEnvironment, TLSClientConfig: cfg}
Push files at a reloader and see what it serves. This runs keyload's reload rules in your browser: the same checks, retries, keep-last-good and CA overlap.
Every reload that changes something or fails produces an event naming the file, the reason, and the certificates that changed (never key material). MetricsHandler serves Prometheus metrics with no client library.
| Metric | Meaning |
|---|---|
keyload_generation | Current snapshot generation |
keyload_reload_total{result} | success, invalid, read_error |
keyload_cert_not_after_seconds{role} | Expiry of your certificate and the earliest CA in each bundle |
keyload_last_success_timestamp_seconds | Catches a watcher that silently stopped |
# Prometheus alerting rules - alert: CertificateExpiresSoon expr: keyload_cert_not_after_seconds - time() < 7 * 86400 - alert: CertificateReloadFailing expr: increase(keyload_reload_total{result!="success"}[15m]) > 0
Small runnable programs. The commands to try each one are at the top of its file, and gencerts makes throwaway certificates to try them with.
An mTLS server and a client in one program. Its tests show how to test code that uses keyload.
An HTTPS server whose certificate and client CAs reload, with SIGHUP for on-demand reloads.
An HTTP client whose certificate and trusted CAs reload between requests.
A gRPC server and client with mutual TLS from the same files.
An HTTPS client going through an HTTP proxy, which needs Config mode.
Prometheus metrics, structured reload logs and alerting rules.
A Certificate renewed into a Secret and mounted into a Deployment.
The details that usually bite.
Keep the certificates they started with. Only new handshakes use reloaded files, so long-lived connections rotate when they reconnect.
Add the new CA, move certificates over, then remove the old one. With OverlapWindow, peers that haven't rotated yet keep working until the window ends.
inotify misses changes on NFS and FUSE, so keyload checks every 30 seconds there, and on macOS and Windows. Add OnSignal or PollEvery if you need faster.
Anything that can return PEM works: implement Source, or wrap a function with SourceFunc, for Vault or a secrets API.