skip to content

How do you serve HTTPS from a Go net/http server, and where does a tls.Config go?

level: juniorimportance: must knowfreq 58%

answer

  1. two file paths and one method call
  2. the convenience function takes no options
  3. options live in a struct field, not an argument
  4. build the server yourself to reach it
  5. http.Server.TLSConfig plus ListenAndServeTLS

basics

~20 s

Call ListenAndServeTLS with a certificate file and a key file. The package-level http.ListenAndServeTLS takes no tls.Config, so to set TLS options you build an http.Server, fill its TLSConfig field, and call that server's ListenAndServeTLS method.

solid answer

~40 s

The one-liner is `http.ListenAndServeTLS(addr, certFile, keyFile, handler)`: it serves HTTPS using a PEM certificate chain and its private key. It gives you nowhere to put TLS options, so real services build the server explicitly — `srv := &http.Server{Addr: ":8443", Handler: mux, TLSConfig: &tls.Config{MinVersion: tls.VersionTLS12}}` — and then call `srv.ListenAndServeTLS("cert.pem", "key.pem")`. The certificate file holds the server's own leaf certificate first, followed by any intermediate certificates the peer needs to chain to a trusted root; the key file holds the matching private key. If you already loaded the pair yourself with `tls.LoadX509KeyPair` into `TLSConfig.Certificates`, or set a `GetCertificate` callback that picks a certificate per requested server name, you pass two empty strings instead of filenames. `srv.ServeTLS(l, cert, key)` is the same thing over a listener you created.

code

go · 9 lines
go
srv := &http.Server{
	Addr:    ":8443",
	Handler: mux,
	TLSConfig: &tls.Config{
		MinVersion: tls.VersionTLS12,
	},
}
// cert.pem holds the leaf certificate first, then any intermediates
log.Fatal(srv.ListenAndServeTLS("cert.pem", "key.pem"))

go deeper

for a junior

Be ready to write the four-line version from memory: build an http.Server, set Addr, Handler and TLSConfig, call ListenAndServeTLS with the certificate and key paths. Know which argument is which file.

for a middle

Explain why the package-level helper is a dead end for configuration, what the clone at startup means for late edits, and when empty filenames are legal because Certificates or GetCertificate already supplies the certificate.

for a senior

Show that you check the shipped chain, not just that the handshake works locally. Missing intermediates, a certificate loaded from a secret store, and reloading on rotation are the operational parts interviewers listen for.

for a principal

Frame it as what the service template hands every team by default: which fields are pre-filled, which are configurable, and how certificates reach the process at all so that no team has to invent their own answer.

## The two ways to start an HTTPS listener Go's `net/http` gives you a package-level convenience function and a method on `http.Server`, and the difference between them is the whole point of this question. ```go // convenience: no place to configure TLS http.ListenAndServeTLS(":8443", "cert.pem", "key.pem", mux) ``` That call creates an `http.Server` internally, and you never get a reference to it. It is fine for a demo. It is not fine for a service that has to state a minimum protocol version, because the only place TLS options live is the server's `TLSConfig` field. ```go srv := &http.Server{ Addr: ":8443", Handler: mux, TLSConfig: &tls.Config{ MinVersion: tls.VersionTLS12, }, } log.Fatal(srv.ListenAndServeTLS("cert.pem", "key.pem")) ``` `TLSConfig` is a `*tls.Config` from `crypto/tls`. Everything TLS-related on the serving side is a field on that struct — the version floor, the cipher-suite list for older protocol versions, the certificates, the callbacks. `http.Server` itself has no TLS fields beyond that one pointer. ## What the two files are `certFile` is a PEM file containing the server's certificate. If the certificate was issued by an intermediate CA rather than directly by a root, the intermediates must be concatenated into the same file, leaf first, in issuing order. Go's own TLS client does not go and fetch missing intermediates, and neither do many other clients, so a chain that verifies in your browser can still fail from a Go program if you shipped only the leaf. `keyFile` is the PEM private key that matches the leaf certificate. If the key is in the same file as the certificate, you may pass the same path for both arguments. ## Loading the certificate yourself Sometimes the files are not files: the certificate comes from a secret store, or you need one certificate per hostname. Then you fill the config instead: ```go cert, err := tls.LoadX509KeyPair("cert.pem", "key.pem") if err != nil { return err } srv.TLSConfig = &tls.Config{ MinVersion: tls.VersionTLS12, Certificates: []tls.Certificate{cert}, } return srv.ListenAndServeTLS("", "") ``` Empty filenames are legal precisely when the config can already produce a certificate — `Certificates` is non-empty, or `GetCertificate` is set. `GetCertificate` is a callback that receives the client's hello and returns the certificate to present, which is how one listener serves several hostnames. ## Serving over a listener you own `ListenAndServeTLS` creates the TCP listener for you from `srv.Addr`. When you need the listener first — to bind port 0 and discover the port, to serve on a Unix socket, to hand the listener in from a supervisor — use `srv.ServeTLS(l, certFile, keyFile)` with an already-created `net.Listener`. The certificate arguments behave identically. ## Details worth knowing early `net/http` does not use your `*tls.Config` object directly: when the server starts it takes a clone and adds the ALPN protocol names it supports to `NextProtos`, which is how HTTP/2 gets negotiated. Two consequences follow. First, mutating `srv.TLSConfig` after the server has started changes nothing for that listener — set it before you start. Second, if you set `NextProtos` yourself, you are overriding what the server would have advertised, so you can accidentally turn HTTP/2 off. A `*tls.Config` is meant to be shared and read concurrently by every connection. Treat it as immutable once the server is running; if you need a variant, use its `Clone` method rather than editing the live one. Finally, an HTTPS server is an ordinary `http.Server` — the same handlers, the same mux, the same everything. Serving HTTPS is a matter of which start method you call and what you put in one struct field, not of a different server type. Inside a handler, `r.TLS` is a `*tls.ConnectionState` (nil for a plaintext request), which is how a handler learns what was negotiated for the request it is serving.

  • When may you pass empty strings for the certificate and key filenames?
    When the server's `TLSConfig` can already produce a certificate on its own: `Certificates` is non-empty, or `GetCertificate` is set. That is the normal path when the key pair comes from a secret store rather than from disk, or when one listener serves several hostnames and picks the certificate per client hello.
  • What besides the server's own certificate belongs in the certificate PEM file?
    Any intermediate CA certificates needed to chain the leaf to a trusted root, concatenated after the leaf in issuing order. Go's TLS client does not fetch missing intermediates, so a leaf-only file produces verification failures for callers even though the same certificate looks fine in a browser that cached the intermediate.
  • Why does editing srv.TLSConfig after the server is already serving have no effect?
    `net/http` clones the config when the listener starts — partly so it can append its own ALPN protocol names for HTTP/2 — and the running listener uses that clone. Set everything before you start. It is also the safe rule generally: a `*tls.Config` is read concurrently by every connection, so mutating a live one is a data race.

The package-level helper is a pre-locked door: it works, but you never hold the lock. Building the http.Server yourself is what gives you the lock to specify.

saying these in an interview costs you the question

  • Thinks http.ListenAndServeTLS accepts a tls.Config argument
  • Ships a PEM file with the leaf certificate and no intermediates
  • Believes assigning TLSConfig after startup reconfigures the listener
  • Confuses the key file with the certificate chain file
  • Thinks HTTPS needs a different server type than http.Server