skip to content

What does tls.Config.MinVersion control, and what applies when you leave it at zero?

level: middleimportance: should knowfreq 50%

answer

  1. a floor, not a preference
  2. zero does not mean no minimum
  3. the package picks when you do not
  4. nothing silently downgrades on mismatch
  5. the constants are tls.VersionTLS12 and tls.VersionTLS13

basics

~20 s

tls.Config.MinVersion is the oldest TLS protocol version Go will negotiate; older peers are refused during the handshake. Zero means the crypto/tls package default, TLS 1.2 in current Go. Set it explicitly, because that default has been raised across releases.

solid answer

~50 s

`MinVersion` is a floor, not a preference: a handshake that cannot reach it fails, and Go never falls back to an older version. You set it with `tls.VersionTLS12` or `tls.VersionTLS13`. Zero — the field's zero value — does not mean "anything"; it means the crypto/tls default, which in current Go is TLS 1.2 on both the serving and the dialing side. The paired `MaxVersion` defaults to TLS 1.3 and is best left alone; capping it is a debugging tool, not hardening. The same field governs both directions: `http.Server.TLSConfig` inbound, `http.Transport.TLSClientConfig` or the config you hand `tls.Dial` outbound. If you have a compliance floor, write it down rather than inherit it, because the default has moved between releases — and note that a TLS 1.3 floor also makes `CipherSuites` inert, since Go does not let you configure TLS 1.3 suites.

code

go · 10 lines
go
var minVer uint16
switch cfg.MinTLS {
case "1.3":
	minVer = tls.VersionTLS13
case "1.2":
	minVer = tls.VersionTLS12
default:
	return fmt.Errorf("unsupported min_tls %q", cfg.MinTLS)
}
srv.TLSConfig = &tls.Config{MinVersion: minVer}

go deeper

for a junior

Know that MinVersion is the oldest protocol version accepted, that the constants are tls.VersionTLS12 and tls.VersionTLS13, and that a peer below the floor simply fails to connect.

for a middle

Explain that the zero value means the package default rather than no limit, that the default has been raised across releases, and why MaxVersion should normally be left alone.

for a senior

Show how you verify the floor in a running service — a probe that pins MaxVersion and expects failure, or logging the negotiated version per request — and remember the outbound clients, not just the listener.

for a principal

Own the decision itself: which floor the platform sets by default, how a service records an exception, and how you learn what the current client mix actually negotiates before you raise it.

## What the field means `tls.Config.MinVersion` is a `uint16` naming the oldest protocol version this side will accept. Its counterpart `MaxVersion` names the newest. During a handshake the two peers agree on the highest version both support; if that number falls below your `MinVersion`, the handshake is aborted and the connection is closed. There is no downgrade, no "insecure but connected" state, and no callback that lets you wave one through. That is deliberate — protocol downgrade is exactly the behaviour TLS hardening exists to prevent. The constants are `tls.VersionTLS10`, `tls.VersionTLS11`, `tls.VersionTLS12` and `tls.VersionTLS13`. They are opaque numbers; compare and assign, never arithmetic. ## The zero value is not "no minimum" The single most common misconception is that leaving `MinVersion` unset means anything goes. It does not. Zero means "the package chooses", and the package's choice in current Go is TLS 1.2 for both servers and clients. So an unconfigured `&tls.Config{}` today is already reasonably safe. The trap is a different one: that default is a moving target. Earlier Go releases would happily negotiate TLS 1.0, and the default minimum has been raised more than once. Code that never states its floor therefore behaves differently depending on which toolchain built it, which is uncomfortable when the floor is the thing an auditor asks about. If you have a stated requirement, state it in the code: ```go srv.TLSConfig = &tls.Config{MinVersion: tls.VersionTLS12} ``` That line costs nothing and turns a toolchain-dependent behaviour into a compiled-in fact. It is also the line a reviewer can find with a grep, which matters more than it sounds. ## Driving it from configuration A service that must differ per environment usually maps a config file field onto the constant at startup, validating as it goes: ```go var minVer uint16 switch cfg.MinTLS { case "1.3": minVer = tls.VersionTLS13 case "1.2": minVer = tls.VersionTLS12 default: return fmt.Errorf("unsupported min_tls %q", cfg.MinTLS) } srv.TLSConfig = &tls.Config{MinVersion: minVer} ``` The `default` branch matters. A config parser that silently leaves the variable at zero on a typo hands you the package default while the config file claims something stricter — a setting that reads as enforced and is not. ## MaxVersion `MaxVersion` defaults to the newest version the package implements, TLS 1.3 in current Go. Setting it is almost always wrong: it freezes you below whatever comes next and, in the short term, it drops you back to the TLS 1.2 handshake. The legitimate uses are narrow — reproducing a bug, or working around a middlebox that mangles a newer handshake — and both should be temporary and commented as such. ## Both directions, not just the listener `MinVersion` is a `tls.Config` field, and `tls.Config` is used identically on both sides. A service that hardens `http.Server.TLSConfig` and leaves its outbound `http.Transport.TLSClientConfig` nil has hardened half of itself. Reviewers who only check the listener miss this constantly; if the policy is a fleet policy, it belongs in whatever constructs your HTTP clients too. ## Verifying it rather than believing it The field is easy to set and easy to set somewhere that never runs. Two checks are worth wiring up. Inside a handler, `r.TLS.Version` reports what the current request actually negotiated, which makes it cheap to log the real distribution before you raise a floor. From outside, dial the running listener with a client config whose `MaxVersion` is pinned to the version you claim to reject, and assert the handshake fails; that is a three-line test and it is the only evidence that the floor is real in the deployed binary rather than in the branch you meant to deploy. ## Interaction with cipher suites Raising `MinVersion` to `tls.VersionTLS13` has a side effect people find surprising: it makes `tls.Config.CipherSuites` completely inert, because that field only ever applied to TLS 1.0 through 1.2 and Go does not expose the TLS 1.3 suites for configuration. That is not a bug to work around — every suite TLS 1.3 defines is already an AEAD suite — but it does mean a hardened suite list and a 1.3 floor are alternatives, not a belt-and-braces pair.

  • What is the default MaxVersion, and when would you ever set it?
    It defaults to the newest version crypto/tls implements — TLS 1.3 in current Go. Setting it is almost always a mistake: capping at TLS 1.2 gives up the newer handshake and freezes you below whatever ships next. The honest uses are temporary — reproducing a bug, or routing around a middlebox that mangles a 1.3 handshake — and should be commented as such.
  • A Go client can only reach TLS 1.0 and your server sets MinVersion to tls.VersionTLS12. What does the client observe?
    A failed handshake and a closed connection, reported as a protocol version error. Go does not negotiate downward to satisfy the peer and offers no override at connection time. That is the intended outcome — a version floor that could be talked out of is not a floor.
  • How would you prove the deployed listener really enforces the minimum you configured?
    Probe it. Dial the running listener with a client `tls.Config` whose `MaxVersion` is pinned to the version you claim to reject and assert the handshake fails; dial again unpinned and print `ConnectionState().Version` to see what a real client gets. Logging `r.TLS.Version` per request gives the same evidence continuously from inside.

saying these in an interview costs you the question

  • Thinks a zero MinVersion means every version is accepted
  • Expects Go to fall back to an older version for old peers
  • Sets MaxVersion to TLS 1.2 believing it is hardening
  • Assumes the package default is fixed across Go releases
  • Hardens the listener and leaves outbound configs untouched