Skip to content

Harden a server & client

Recipes for the common production shapes: advertising ALPN, trusting a private CA on both ends, and letting one certificate serve several transports with targeted overrides.

ServerConfig takes a variadic list of ALPN protocol identifiers. Pass "h2" first to prefer HTTP/2, or "h2" alone for a raw gRPC TLS listener:

// HTTP server: prefer HTTP/2, fall back to HTTP/1.1
httpCfg, err := pair.ServerConfig("h2", "http/1.1")

// gRPC TLS listener: HTTP/2 only
grpcCfg, err := pair.ServerConfig("h2")

With no arguments the returned config leaves NextProtos unset and the standard library negotiates as normal.

Trust a private CA on both ends

When a server presents a certificate signed by an internal or self-signed CA, the client must be told to trust it. CertPool builds an *x509.CertPool from one or more PEM files, and ClientConfig wires that pool into the hardened config for you:

// Client trusts the private CA (and only it, unless combined with system roots).
clientCfg, err := tls.ClientConfig("/etc/certs/ca.pem")
if err != nil {
    log.Fatal(err)
}

ClientConfig returns an error if a CA file is missing or contains no certificates, so a broken trust anchor is caught at construction. Pass the same CA file the servers were issued from to share one trust anchor across your gRPC, HTTP and gateway clients.

To build a pool directly — for example to set RootCAs on a config you own:

pool, err := tls.CertPool("/etc/certs/ca.pem", "/etc/certs/old-ca.pem")

One certificate, several transports

A service often terminates TLS on more than one listener (HTTP, gRPC, a gateway). You usually want a single shared certificate, with the option to override it per transport. ResolvePair performs that merge on already-materialised typed values — no config-lookup interface required:

shared := tls.Pair{Enabled: true, Cert: "/etc/certs/shared.pem", Key: "/etc/certs/shared-key.pem"}

// The gRPC listener overrides only the certificate; everything else
// falls back to the shared pair.
grpcSpecific := tls.Pair{Cert: "/etc/certs/grpc.pem"}

grpcPair := tls.ResolvePair(shared, grpcSpecific, tls.PairOverrides{
    Cert: true, // only Cert was explicitly set for gRPC
})
// grpcPair.Enabled == true (shared), grpcPair.Cert == "/etc/certs/grpc.pem",
// grpcPair.Key == "/etc/certs/shared-key.pem" (shared)

The PairOverrides mask records which fields the per-transport section actually set, so an unset field never clobbers the shared value with a zero. You decide how a field counts as "set" — a config key being present, a non-empty flag, an environment variable — and express it as the boolean mask.

Decide TLS vs. plaintext at boot

Pair.Valid is the single predicate for "is this pair usable for TLS":

switch {
case pair.Valid():
    cfg, err := pair.ServerConfig("h2", "http/1.1")
    // ... serve HTTPS
case pair.Enabled:
    log.Fatal("TLS enabled but cert/key path is missing")
default:
    // ... serve plain HTTP
}