What this package does not do¶
A short list of things people reasonably expect from something called tls and will
not find here. Each one is deliberate: the package is TLS plumbing, not a TLS
platform.
If you are looking for a feature and it is on this page, the answer is no — build it in your own code, or reach for a different module.
It does not issue, renew or rotate certificates¶
There is no certificate authority, no ACME client, no CSR generation, no key
generation. Pair holds two file paths and the package reads them. How those files
arrive — cert-manager, Let's Encrypt, an internal CA, a secret mounted by your
orchestrator — is entirely yours, as is protecting the key on disk.
For local development certificates, gitlab.com/phpboyscout/go/localca is the
sibling module that mints them.
It does not reload a certificate that changed on disk¶
Pair.Certificate reads the files at the moment you call it, and ServerConfig
calls it once. A renewed certificate written over the same path is not picked up: the
running listener keeps serving the material it loaded at startup until the process
restarts.
Nothing stops you doing it yourself. crypto/tls.Config.GetCertificate is called per
handshake, so a config that wires it to a cached, periodically-reloaded certificate
rotates without a restart — build that config yourself and merge the rest of the pair
in with ApplyTo.
ClientConfig cannot present a client certificate¶
ClientConfig sets RootCAs and nothing else. It has no parameter for a client
certificate, so it cannot on its own be one half of a mutual-TLS connection.
The pieces are all here, just not joined up — load the certificate with
Pair.Certificate and assign it:
cfg, err := tls.ClientConfig("/etc/certs/ca.pem")
cert, err := clientPair.Certificate()
cfg.Certificates = []cryptotls.Certificate{cert}
Worked through end to end in Require a client certificate.
Private-CA trust replaces the system roots¶
CertPool starts from an empty x509.NewCertPool(), never
x509.SystemCertPool(). So ClientConfig("/etc/certs/ca.pem") produces a client
that trusts that one CA and nothing else — every public certificate on the
internet now fails verification for that client.
For a client that only ever talks to your own services, that is the right, tight
answer. For one that also calls a public API, it is a bug that surfaces as
x509: certificate signed by unknown authority against a perfectly valid public
certificate.
There is no option to combine the two. Build the pool yourself when you need both:
pool, err := x509.SystemCertPool()
pem, err := os.ReadFile("/etc/certs/ca.pem")
pool.AppendCertsFromPEM(pem)
cfg := tls.DefaultConfig()
cfg.RootCAs = pool
x509.SystemCertPool returns a copy of the system pool, so adding to it affects
nothing else in the process. It also snapshots: roots added to the machine after the
call are not picked up.
ApplyTo does not harden the config you pass it¶
ApplyTo adds a certificate, ALPN protocols and possibly a client-CA policy. It does
not touch MinVersion, CipherSuites or CurvePreferences. Merge a pair into
&cryptotls.Config{MinVersion: cryptotls.VersionTLS10} and you get a TLS 1.0 listener
with a certificate in it.
The hardening lives in DefaultConfig, and ServerConfig is exactly
DefaultConfig() + ApplyTo. If you are building a config by hand and want the
posture too, start from DefaultConfig() and modify that.
ApplyTo is also not idempotent — calling it twice with the same pair appends the
certificate twice.
Enabled is advisory, and nothing enforces it¶
Pair.Enabled is read by exactly one function, Pair.Valid. ServerConfig,
ApplyTo and Certificate all build and load happily with enabled: false.
If a false in config is meant to mean "serve plaintext", you have to branch on it
before you build the config. A service that calls ServerConfig unconditionally will
serve TLS regardless of what the flag says.
One certificate per config, and no SNI selection¶
ServerConfig loads a single certificate. There is no GetCertificate hook, no
per-hostname certificate map, and no helper for a listener that fronts several
domains with different certificates. ApplyTo appends to cfg.Certificates, so a
multi-certificate config is buildable by hand — crypto/tls picks by SNI — but the
package gives you no support for assembling one.
No revocation checking¶
No OCSP, no OCSP stapling, no CRL fetching, no VerifyPeerCertificate hook wired up
to any of it. A client certificate that chains to a CA in client_cas is accepted
even if that certificate was revoked this morning. This matches the crypto/tls
default, which also does no revocation checking; the package does not improve on it.
Short-lived certificates are the usual answer. Real revocation is your own
VerifyPeerCertificate on a config you build.
The TLS posture is fixed, not configurable¶
There is no key, flag or option for the minimum version, the cipher suites or the
curves. Pair cannot express them and DefaultConfig takes no arguments. Questions
of the form "how do I require TLS 1.3" or "how do I add a cipher suite" have the same
answer: take the returned config and set the field.
Nothing prevents that, and nothing in the package undoes it afterwards. What the package refuses to give you is a configuration key that lowers the posture — see Why there is no insecure switch.
The one place the fixed posture is weaker than Go's own default is key exchange:
setting CurvePreferences opts out of the post-quantum X25519MLKEM768 hybrid. See
Key exchange is weaker than Go's own default.
The package does not load configuration¶
No file parsing, no environment variables, no flags, no PHPBOYSCOUT_-style prefix.
Pair carries mapstructure, yaml and json tags so your loader can fill it in,
and that is the whole integration story. ResolvePair merges already-materialised
values and takes an explicit mask rather than asking a config library which keys were
set.
That is what "framework-free" means here, and why it is worth the inconvenience.
It is not a transport¶
No listener, no dialer, no http.Server, no gRPC credentials, no connection pooling,
no timeouts. The package hands you a *crypto/tls.Config and stops. Wiring it into
http.Server.TLSConfig, http.Transport.TLSClientConfig or
credentials.NewTLS is a line of your code, deliberately.