Skip to content

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. DefaultConfig does not.
  • The floor is not a default. Go's TLS 1.2 minimum is a default, and GODEBUG=tls10server=1 restores TLS 1.0 on a zero-value server config. An explicit MinVersion ignores 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:

  • ClientCAs set with no ClientAuth means require-verify, not "collect certificates and ignore them".
  • require-verify or verify-if-given with no ClientCAs is an error, because a verifying mode with nothing to verify against cannot be satisfied.
  • An unrecognised ClientAuth is 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:

  • CertPool returns 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.
  • ClientConfig propagates 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.Certificate and ServerConfig surface 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.