skip to content

Your Go server sets tls.Config.CipherSuites, yet clients negotiate a suite you excluded. Why?

level: seniorimportance: should knowfreq 33%

answer

  1. the field has a version range in its documentation
  2. one protocol version never consults it
  3. the newer handshake defines suites differently
  4. there is nothing left to prune there
  5. MinVersion is the lever that actually bites

basics

~10 s

tls.Config.CipherSuites applies only to TLS 1.0 through 1.2. Go does not let you configure TLS 1.3 cipher suites, so a TLS 1.3 handshake ignores the list entirely. Constrain the protocol version instead.

solid answer

~50 s

`CipherSuites` is documented as the list of enabled TLS 1.0–1.2 suites; TLS 1.3 suites are not configurable in Go. So a hardened list satisfies a scanner and a reviewer, then every modern client negotiates TLS 1.3 and the list never applies — a setting written, reviewed and shipped with no effect. That is fine security-wise, since all three suites TLS 1.3 defines in Go are AEAD suites with no CBC or RC4 to remove, but it is not fine as a control you claim to have. The real levers: raise `MinVersion` to `tls.VersionTLS13` to guarantee no legacy suite can appear, or keep TLS 1.2 available and use `CipherSuites` knowing it governs only those connections. `PreferServerCipherSuites` is a further trap — deprecated and ignored, since crypto/tls picks the order itself. Verify by probing the listener, not by reading the config.

code

go · 7 lines
go
conn, err := tls.Dial("tcp", "svc.internal:8443", nil)
if err != nil {
	log.Fatal(err)
}
defer conn.Close()
st := conn.ConnectionState()
fmt.Println(tls.VersionName(st.Version), tls.CipherSuiteName(st.CipherSuite))

go deeper

for a junior

Take away one fact: in Go, TLS 1.3 cipher suites cannot be configured, and the CipherSuites field only covers TLS 1.0 through 1.2.

for a middle

Explain why TLS 1.3 needs no suite list — the suite no longer names a key exchange, and all the remaining suites are AEAD — and which field actually constrains a 1.3 connection.

for a senior

Demonstrate the diagnosis: probe the listener, print the negotiated version and suite, and distinguish a control that applies from one that is silently inapplicable in production but works in a pinned test.

for a principal

Treat it as a review policy question. Decide what evidence a TLS change must carry before it closes an audit item, so no team can ship a setting whose only effect is on the compliance spreadsheet.

## The symptom Someone hardens a service: `CipherSuites` is set to a short list of ECDHE AEAD suites, the change is reviewed, the audit item is closed. Weeks later a handshake probe against the running listener reports `TLS_AES_128_GCM_SHA256` — a suite that is not in the list. Nothing is broken and nothing was overridden. The list simply does not apply to the connection that was made. ## Why `tls.Config.CipherSuites` is, in the package's own words, a list of enabled TLS 1.0–1.2 cipher suites, and TLS 1.3 cipher suites are not configurable. TLS 1.3 redefined what a cipher suite names: in TLS 1.2 a suite bundled the key exchange, the authentication algorithm, the bulk cipher and the MAC, which is why there were hundreds of them and why so many were bad. In TLS 1.3 the key exchange and signature are negotiated separately, so a suite names only an AEAD cipher and a hash. Go implements three, all AEAD. There is nothing dangerous in the set, so the package exposes no way to prune it — and once a connection negotiates TLS 1.3, your list is simply not consulted. So the field is not ignored in general. It is ignored *for the connections your modern clients actually make*, which is the worst of both worlds: it works in the test where you forced TLS 1.2, and it is inert in production. ## The related trap `PreferServerCipherSuites` is deprecated and ignored in current Go. crypto/tls decides the suite ordering itself, taking into account whether the machine has hardware AES support, so a server that would be slow at AES prefers ChaCha20. Code that sets this field is expressing an intent the package no longer honours — and, like the suite list, it reads as a control in a review. ## What to do instead Decide what you actually want and pick the matching lever. *I want to guarantee no CBC-mode or otherwise legacy suite can ever be used.* Set `MinVersion: tls.VersionTLS13`. Then the only suites available are the three AEAD ones and the question disappears. The cost is that every client must speak TLS 1.3. *I must keep TLS 1.2 for some clients but want to constrain those connections.* Keep `MinVersion: tls.VersionTLS12` and set `CipherSuites` to the AEAD ECDHE suites you accept. Write a comment saying, in one line, that the list governs TLS 1.2 only — otherwise the next reader re-derives this whole page. `tls.CipherSuites()` returns the suites the package currently implements and considers safe, and `tls.InsecureCipherSuites()` returns the ones it keeps only for compatibility, which is a better basis for a list than a copy-pasted constant block that rots. *I want a report of what is really happening.* Probe it. ```go conn, err := tls.Dial("tcp", "svc.internal:8443", nil) if err != nil { log.Fatal(err) } defer conn.Close() st := conn.ConnectionState() fmt.Println(tls.VersionName(st.Version), tls.CipherSuiteName(st.CipherSuite)) ``` `ConnectionState` reports what the handshake agreed; `VersionName` and `CipherSuiteName` turn the opaque `uint16` values into the names you can paste into a ticket. Run it from a job against every listener you own and you have the fleet's real posture instead of its configured one. From inside a handler, `r.TLS` carries the same `ConnectionState` for the current request, so logging the negotiated version and suite is a one-liner if you would rather sample continuously. ## The reviewing lesson The generalisable point is not about cipher suites. It is that a security setting is a claim, and a claim needs evidence. A field that is silently inapplicable produces no error, no warning and no log line; the only thing that distinguishes it from a working control is an observation of the running system. When a review adds a TLS field, the reviewable artefact is the probe output, not the diff.

  • Given TLS 1.3 suites cannot be pruned, is the inability to configure them a security problem?
    No. TLS 1.3 removed the key exchange and signature from the suite, so a suite names only an AEAD cipher and a hash, and every one Go implements is a sound AEAD construction. There is no CBC, no RC4 and no static-RSA variant left to disable. The problem is only the false claim that a suite list is enforcing something.
  • What does tls.Config.PreferServerCipherSuites do in current Go?
    Nothing — it is deprecated and ignored. crypto/tls chooses the ordering itself, including a preference for ChaCha20 on machines without hardware AES acceleration. Code that still sets it is stating an intent the package no longer honours, and like an inert suite list it reads as a control during review.
  • You must keep TLS 1.2 for one legacy client. How do you constrain those connections without pretending the list covers everything?
    Keep `MinVersion: tls.VersionTLS12`, set `CipherSuites` to the AEAD ECDHE suites you accept, and comment that the list governs TLS 1.2 connections only. Then prove it: probe the listener from both a modern and a pinned-1.2 client and record the negotiated suite from each, so the scope of the control is documented by observation.
  • How would you catch this class of dead setting before it ships?
    Make the evidence part of the change. A test that dials the configured server and asserts the negotiated version and suite turns a claim into an assertion, and it fails the day a default moves underneath you. Reading the diff can never distinguish a field that applies from a field that silently does not.

It is a lock fitted to a door that newer visitors no longer use. The lock works; it is just not on the path anyone walks.

saying these in an interview costs you the question

  • Believes CipherSuites restricts every TLS version
  • Adds a suite list as the compliance control without checking it applies
  • Sets PreferServerCipherSuites expecting the server order to win
  • Concludes TLS 1.3 is unsafe because its suites cannot be pruned
  • Verifies TLS hardening by reading the config rather than probing