You own the Go service template's TLS baseline: how do you choose the MinVersion every generated service ships with, and handle the partner that cannot meet it?
answer
- two listener classes, not one number
- a default the service can override
- a fork is what a constant buys you
- outbound is the forgotten half
- measure the traffic before you raise the floor
basics
~20 sShip the strictest floor your real clients meet, measured rather than assumed: usually TLS 1.3 between internal services, TLS 1.2 on a public listener. Make it a configurable default, not a constant, and give every exception an owner and an expiry.
solid answer
~50 sTwo decisions, not one. The floor itself should come from evidence — log the negotiated version per request on the public listener for a few weeks before raising anything — and it can legitimately differ between an internal listener where both ends are yours and a public one where unknown clients live. The second decision matters more: the template must express the floor as a default a service can override in its own config, validated at startup, rather than as a constant, because a constant forces the one team with a legacy partner to fork the template and stop receiving its updates. An exception then carries a named owner and a date, and a fleet report of what each listener actually negotiates keeps it visible. Two things I would insist on regardless: the same baseline applies to outbound `http.Transport.TLSClientConfig`, which teams forget, and no suite list ships as a control unless someone proved it applies.
code
go · 11 lines// min_tls defaults to "1.3" in the template's generated config file
var minVer uint16
switch cfg.MinTLS {
case "1.3":
minVer = tls.VersionTLS13
case "1.2": // exception: requires a named owner and an expiry date
minVer = tls.VersionTLS12
default:
return fmt.Errorf("min_tls %q not permitted by the platform baseline", cfg.MinTLS)
}
srv.TLSConfig = &tls.Config{MinVersion: minVer}go deeper
You will not own this call yet, but know the shape: the floor comes from a config value the service sets, and one number is chosen for everyone rather than per feature.
Be able to implement it faithfully — map the config value onto the constant, fail startup on an unrecognised value, and apply the same floor to the outbound client the service builds.
Argue the floor from evidence you gathered: negotiated-version logs, the affected client list, and a probe proving the deployed listener enforces what the config claims.
Own the mechanism as much as the number. Defend why the template ships a default rather than a constant, how an exception stays visible and expires, and why an inert control is worse than none.
## What is actually being decided The template generates every new Go service in the organisation. Whatever it puts in `tls.Config` becomes the default posture of code nobody will revisit for years. Three things are on the table: the floor, the mechanism for exceptions, and how anyone finds out whether the floor is real. ## The floor The honest starting point is that this is not one number. Distinguish the listeners: *Internal, service to service.* Both ends are your Go binaries built from your template. There is no unknown client, so `MinVersion: tls.VersionTLS13` costs nothing and removes an entire category of question. Take it. *Public.* Here the answer depends on who actually connects, and that is measurable rather than arguable. Keep the current floor, log `r.TLS.Version` per request, and look at the distribution. If the TLS 1.2 traffic is a rounding error from crawlers, raise the floor. If it is a real customer segment, you have a migration to plan and a date to negotiate, not a config change to make. Deciding this from a scanner report instead of from your own traffic is how a platform team breaks a partner integration and learns about it from support. ## Default, not constant The mechanism is the part that outlives the number. If the template hardcodes a floor, the first team with a legacy partner has exactly one move: fork the template. That fork then stops receiving every other improvement you make, and you have lost visibility into the very service you most wanted to watch. So the template maps a config field onto the constant at startup — with a `default` branch that fails to start on an unrecognised value, because a config parser that silently leaves the version at zero hands you the package default while the config file claims something stricter. The rules around the override are what keep it from becoming the new default: an exception names an owner and an expiry, it is visible in whatever inventory the platform keeps, and it is reported rather than merely permitted. A permitted exception nobody reports is just a lower baseline with extra steps. ## The half everyone forgets A baseline expressed only on the listener covers half the service. The same template generates the outbound HTTP client, and `http.Transport.TLSClientConfig` is where the floor applies on that side. The workable form is one constructor in the template — clone `http.DefaultTransport`, set the config, return an `*http.Client` — so the policy is one function to review. Then a code search for services constructing their own clients is a meaningful audit. ## Evidence, not configuration The posture you can defend is the one you observed. A small job that dials each listener and prints the negotiated version and suite gives you the fleet's real state; the same probe as a startup self-check or a test gives each service its own. This matters more than usual on this topic because TLS settings fail silently: a suite list that does not apply to the negotiated version, a floor that never got deployed, and a correct configuration are indistinguishable in a diff and trivially distinguishable in a probe. The corollary is a review rule worth stating out loud: the template should not ship a `CipherSuites` list at all unless someone can show it applies to the connections that are actually made. Shipping an inert control to every service in the company is worse than shipping none, because it ends an argument that should have continued. ## Being overruled The security team may want TLS 1.3 everywhere, including the public listener, and they may be right. What you owe them is not resistance but the traffic distribution, the list of affected clients, and a date. What you should refuse is the compromise where the floor stays low and a suite list is added to make the ticket look addressed — that trades a real, measurable decision for an unmeasurable one. And there are things not to spend the budget on. Pinning `MaxVersion`, hand-ordering suites, and expressing preferences the package ignores all generate work and review load without changing what is negotiated. The baseline is one number per listener class, one override path, and one probe. Everything else on this page is somebody's leftover.
- Before raising the public listener's floor to TLS 1.3, how do you learn which clients would break?Instrument first. `r.TLS.Version` inside a handler reports what each request negotiated, so a few weeks of that gives you the distribution by client, path and customer. If you need to see what clients offer rather than what they settled on, a `GetConfigForClient` callback receives the client hello, whose `SupportedVersions` tells you whether a TLS 1.2 connection was a limit or a preference.
- A team needs a TLS 1.2 exception. How do you express it so it does not quietly become the fleet default?As data, not as code: a per-service config value with a named owner and an expiry, validated at startup, visible in the platform inventory, and reported — a periodic list of services below the baseline that someone reads. The failure mode to design against is a copied config file, so the exception should be uncomfortable to duplicate and easy to see.
- Security wants a hardened CipherSuites list in the template. What do you say?That I will ship it only with evidence it applies. `CipherSuites` governs TLS 1.0 through 1.2, and once the floor is TLS 1.3 it is consulted for nothing — so as a fleet-wide control it would be inert while reading as enforced in every review thereafter. If we must keep TLS 1.2 anywhere, the list belongs there, scoped and probed.
- Where does this baseline most often fail to reach in practice?Outbound calls. Teams harden `http.Server.TLSConfig`, leave `http.Transport.TLSClientConfig` nil, and inherit whatever the toolchain defaults to — which is a different question from what the policy says. The fix is one client constructor in the template that every service uses, so that outbound posture is reviewable in one place rather than per call site.
saying these in an interview costs you the question
- Hardcodes one MinVersion in the template with no override path
- Raises the floor from a scanner report rather than traffic data
- Hardens the listener and leaves outbound transports at defaults
- Grants a TLS 1.2 exception with no owner and no expiry
- Adds a cipher-suite list to close the ticket without proving it applies