How to Use HTTPS/TLS in Go

Enable HTTPS in Go by loading certificate and key files into a tls.Config and passing it to your HTTP server.

The red warning in the browser

You built a REST API. It works locally. You push it to a server. The browser blocks the connection with a red warning. Your curl command returns plaintext JSON. The network admin tells you traffic is unencrypted. You need TLS. Go makes this straightforward, but the details matter when you move from "it works" to "it's secure."

How TLS protects your traffic

TLS stands for Transport Layer Security. It wraps your HTTP traffic in an encrypted tunnel. Before any data flows, the client and server perform a handshake. They agree on encryption algorithms and verify identities using certificates. The server proves who it is with a certificate signed by a trusted authority. The client trusts the authority and establishes a secure session. Without TLS, anyone on the network can read your requests and responses.

Think of TLS like a sealed diplomatic pouch. The server presents credentials to prove its identity. The client checks the seal against a list of trusted authorities. If the seal is valid, they exchange keys to lock the pouch. All messages inside become unreadable to anyone who intercepts them. TLS adds overhead, but plaintext traffic is a liability you cannot afford.

The minimal HTTPS server

Here's the simplest way to add HTTPS to a Go server: load a certificate pair and pass it to the HTTP server.

package main

import (
	"crypto/tls"
	"log"
	"net/http"
)

func main() {
	// Load the certificate and private key from files.
	// These files must exist and be readable by the process.
	cert, err := tls.LoadX509KeyPair("server.crt", "server.key")
	if err != nil {
		// Fatal exits the program with a non-zero status.
		// This prevents the server from starting with bad config.
		log.Fatalf("failed to load key pair: %v", err)
	}

	// Configure the TLS settings.
	// At minimum, attach the certificate to the config.
	tlsConfig := &tls.Config{
		Certificates: []tls.Certificate{cert},
	}

	// Create the HTTP server with the TLS config.
	server := &http.Server{
		Addr:      ":8443",
		TLSConfig: tlsConfig,
	}

	// Start listening and serving with TLS.
	// Empty strings for cert/key files because we loaded them manually.
	if err := server.ListenAndServeTLS("", ""); err != http.ErrServerClosed {
		log.Fatalf("server error: %v", err)
	}
}

The if err != nil pattern is verbose by design. The community accepts the boilerplate because it makes the unhappy path visible. You cannot accidentally swallow an error when the check is explicit. Certificates are files. Treat them like secrets.

What happens under the hood

The tls.LoadX509KeyPair function reads PEM-encoded files. PEM is a base64 wrapper around DER binary data. Go parses the text, decodes the base64, and validates the structure. If the files are missing or the key doesn't match the certificate, the function returns an error. The tls.Config struct holds all TLS parameters. You attach the certificate slice here. The http.Server uses this config to wrap the underlying TCP listener.

When a connection arrives, the TLS handshake runs before any HTTP data is processed. The client sends a ClientHello with supported versions and cipher suites. The server responds with a ServerHello, its certificate, and a key exchange message. The client verifies the certificate chain. If verification succeeds, both sides derive session keys. The connection is now encrypted. The handshake runs before the handler. Fail fast, encrypt early.

Production-ready configuration

Production servers need tighter security settings. Here's a realistic setup with version constraints and a proper handler.

// Load certificate and key from disk.
cert, err := tls.LoadX509KeyPair("server.crt", "server.key")
if err != nil {
	log.Fatalf("failed to load key pair: %v", err)
}

// Configure TLS with security constraints.
// MinVersion forces clients to use TLS 1.2 or newer.
// Older versions have known vulnerabilities.
tlsConfig := &tls.Config{
	MinVersion:   tls.VersionTLS12,
	Certificates: []tls.Certificate{cert},
}
// Create the HTTP server.
// Attach the TLS config and the request handler.
server := &http.Server{
	Addr:      ":8443",
	Handler:   http.HandlerFunc(handleHealth),
	TLSConfig: tlsConfig,
}

// Start listening.
// Check for errors other than graceful shutdown.
if err := server.ListenAndServeTLS("", ""); err != http.ErrServerClosed {
	log.Fatalf("server error: %v", err)
}

Setting MinVersion to tls.VersionTLS12 blocks legacy clients that use weak protocols. You can also set tls.VersionTLS13 if you don't need backward compatibility. TLS 1.3 is faster and more secure. The log.Fatalf call exits cleanly and prints the error. This is better than panic in main because it signals failure to the operating system. Security defaults save you from lazy configurations.

Dynamic certificate rotation

Static files require restarts to rotate certificates. Use GetCertificate for dynamic loading.

// GetCertificate returns the certificate dynamically.
// This allows rotation without restarting the server.
tlsConfig.GetCertificate = func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
	// Load or fetch the current certificate.
	// Return nil if no certificate matches the SNI.
	return currentCert, nil
}

The callback receives ClientHelloInfo, which includes the Server Name Indication (SNI). You can inspect the SNI to return the correct certificate for virtual hosts. This pattern is essential for services that manage many domains or rotate keys frequently. Trust gofmt. Argue logic, not formatting.

Pitfalls and runtime errors

If the certificate and key don't match, the runtime returns an error like tls: private key does not match public key. This happens when you swap files or generate a new key without updating the certificate. If you use a self-signed cert in a browser, the client rejects the connection with x509: certificate signed by unknown authority. Self-signed certs are fine for development, but production clients require a chain signed by a public CA.

Forgetting to load the cert results in http: TLS handshake error from ...: no certificate file found. The server starts, but every connection fails immediately. Missing MinVersion allows clients to negotiate weak protocols. Attackers can exploit old versions to decrypt traffic. Always set a minimum version. The worst TLS bug is the one that silently falls back to weak encryption.

When to use TLS in Go

Use server.ListenAndServeTLS when you have static certificate files and want the standard library to handle the listener setup.

Use tls.Listen with a custom config when you need fine-grained control over the listener or want to wrap the TLS listener in middleware before passing it to http.Serve.

Use golang.org/x/crypto/acme/autocert when you want automatic certificate management with Let's Encrypt for production domains.

Use plain http.ListenAndServe when you are behind a reverse proxy like Nginx or a load balancer that terminates TLS for you.

Don't reinvent crypto. Use the standard library or a trusted wrapper.

Where to go next