Security threat model & defaults¶
This package exists so that "use TLS" does not become "hand-roll a crypto/tls.Config
and hope." It fixes one reviewed posture — a TLS 1.2 floor, AEAD-only cipher suites,
explicit curves — and applies it to every listener and every client in a service, so
the review happens once rather than at each call site.
Every server and client config this package produces is built from
DefaultConfig, so
the guarantees below hold uniformly across both ends of a connection.
How much of this Go now does for you¶
Go's own defaults have moved, and a good deal of what a hardened config used to buy is
now standard. On Go 1.26 a zero-value tls.Config already refuses TLS 1.0 and 1.1 on
both the client and the server side, and already keeps static-RSA key exchange out of
the suites it will negotiate. Claims that the standard library "negotiates down to
TLS 1.0" describe a Go that no longer ships.
What an explicit config still changes:
- CBC suites are gone. Go's default TLS 1.2 suite list still offers four
ECDHE-CBC-SHA suites.
DefaultConfigdoes not. - The floor is not a default. Go's TLS 1.2 minimum is a default, and
GODEBUG=tls10server=1restores TLS 1.0 on a zero-value server config. An explicitMinVersionignores that setting. - The posture is pinned to this package, not to the toolchain. The stdlib default suite list is free to change between Go releases; this one changes when this package changes, and the change is reviewable in one diff.
It also costs one thing, on curves — see Key exchange is weaker than Go's own default.
What we defend against¶
- Protocol-downgrade attacks. A network attacker who can influence the handshake should not be able to force a connection onto a weak, break-able protocol version.
- Weak cipher negotiation. Cipher suites with known structural weaknesses (CBC padding oracles, non-AEAD constructions, static RSA key exchange with no forward secrecy) should never be on the table, even if a peer offers them.
- Missing forward secrecy. Compromise of a server's long-term private key should not retroactively decrypt previously-captured sessions.
- Fragile trust configuration. A misconfigured certificate path or an empty CA bundle should fail loudly at construction, not silently accept an untrusted peer.
What is outside the threat model¶
The package is not a PKI, a certificate issuer, or a secret store. It consumes certificate paths you provide and applies a hardened policy to them. How those files are provisioned, rotated and protected on disk is the caller's responsibility, and a private key readable by the wrong user defeats everything below.
It also does no revocation checking, does not reload a rotated certificate, and does not protect a listener from an attacker who can edit its configuration. The full list is What this package does not do.
The defaults, and why¶
TLS 1.2 as the floor¶
MinVersion is pinned to VersionTLS12. TLS 1.0 and 1.1 are deprecated
(RFC 8996) and carry downgrade and
cipher-construction weaknesses. Setting an explicit floor removes them from
negotiation entirely, defeating downgrade attempts rather than relying on peers to
prefer better. TLS 1.3 is negotiated automatically when both ends support it; the
floor guarantees the worst case is still 1.2.
AEAD-only cipher suites¶
The suite list is restricted to six ECDHE authenticated-encryption suites:
| Suite | Key exchange | Auth | Cipher |
|---|---|---|---|
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 |
ECDHE | ECDSA | AES-256-GCM |
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 |
ECDHE | RSA | AES-256-GCM |
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 |
ECDHE | ECDSA | AES-128-GCM |
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 |
ECDHE | RSA | AES-128-GCM |
TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305 |
ECDHE | ECDSA | ChaCha20-Poly1305 |
TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305 |
ECDHE | RSA | ChaCha20-Poly1305 |
Three properties are guaranteed by construction:
- Forward secrecy — every suite uses ephemeral ECDHE key exchange, so a stolen long-term key cannot decrypt past captured traffic.
- AEAD only — GCM and ChaCha20-Poly1305 authenticate as they encrypt. There are no CBC-mode suites, so the padding-oracle class of attacks (Lucky 13 and relatives) does not apply.
- Both signature families — ECDSA and RSA variants are present so the list works with either an EC or an RSA server certificate; the negotiated suite follows the certificate's key type.
Both AES-GCM and ChaCha20-Poly1305 are included so hardware without AES acceleration still negotiates a fast, constant-time cipher.
Cipher order and TLS 1.3
Go ignores the CipherSuites list for TLS 1.3 connections — the 1.3 suites are
fixed by the protocol and always safe. This list governs the TLS 1.2 floor. Go
also chooses the mutually-preferred suite itself; the list defines which suites
are permitted, not a rigid server-dictated order.
Modern curve preferences¶
CurvePreferences is set to X25519 then CurveP256. X25519 is a fast, misuse-
resistant Montgomery curve and is preferred; P-256 follows for peers or certificates
that require a NIST curve. Slower and less-scrutinised curves are omitted.
Setting the field at all has a cost that did not exist when the list was written. Go
1.24 added the post-quantum hybrid X25519MLKEM768 and made it the preferred key
exchange for a config that leaves CurvePreferences nil. Naming curves explicitly
opts out of it, so a DefaultConfig connection negotiates plain X25519 where a
zero-value config would negotiate the hybrid. Against an attacker who records traffic
now to decrypt it later, that is a downgrade. Setting cfg.CurvePreferences = nil on
the returned config restores Go's choice and leaves the rest of the posture intact.
Requiring client certificates fails closed¶
The client-certificate policy is expressed as two Pair fields, ClientCAs and
ClientAuth, and every ambiguous combination resolves towards more enforcement or
towards an error:
ClientCAsset with noClientAuthmeansrequire-verify, not "collect certificates and ignore them".require-verifyorverify-if-givenwith noClientCAsis an error, because a verifying mode with nothing to verify against cannot be satisfied.- An unrecognised
ClientAuthis an error rather than a fallback, so a typo cannot quietly become "no client certificates". This holds even when the pair's policy is going to be discarded in favour of a caller's.
One case does not fail closed, and it is worth knowing before you rely on
ApplyTo for mTLS. ApplyTo skips the pair's policy whenever the config it is
merging into already carries one, and it decides that by looking at both
cfg.ClientAuth and cfg.ClientCAs. A config that has a ClientCAs pool but has
left ClientAuth at NoClientCert counts as "already has a policy" — so the pair's
require-verify is dropped, and the resulting listener asks for no client certificate
at all. ServerConfig is not affected, because it starts from a fresh config with
neither field set.
Failing closed on trust¶
The trust helpers are written to error rather than silently degrade:
CertPoolreturns an error if a CA file cannot be read, and — importantly — if a file parses to zero certificates. An empty or malformed bundle is a configuration error, not an empty set of trusted roots to shrug at.ClientConfigpropagates those errors, so a client is never constructed with a broken custom trust store. With no CA files it falls back to the system root store (the correct default for public endpoints); it never disables verification.Pair.CertificateandServerConfigsurface load failures at startup, so a bad certificate path stops the process rather than breaking the first client handshake.
There is deliberately no option to skip verification or lower the floor. If you need a
looser policy for a specific integration, build a crypto/tls.Config yourself for that
one call site — the insecure path should be explicit and local, never the default this
package hands out. The reasoning is in
Why there is no insecure switch.
Relationship to crypto/tls¶
This package is a thin, opinionated layer over the standard library, not a replacement
for it. DefaultConfig returns a plain *crypto/tls.Config you can inspect or further
constrain. The value it adds is a single, reviewed set of decisions applied
consistently — so a security review happens once, here, instead of at every listener
in every service.