How to Use HTTPS/TLS with a Go HTTP Server

Web
Start a Go HTTPS server by calling http.ListenAndServeTLS with your certificate and key file paths.

The tunnel before the conversation

You deploy your Go service to a virtual machine. The browser shows a red warning triangle. The API client drops the connection with a certificate error. Your logs show successful HTTP responses, but the data never reaches the user. The network is insecure. You need TLS.

Go includes TLS support in the standard library. You do not need external packages. You do not need OpenSSL configuration files. You write Go code to configure the encryption, load the certificates, and start the server. The crypto/tls package handles the handshake. The net/http package plugs into it. TLS sits between the network layer and your application. It negotiates encryption keys and verifies identities before any HTTP data flows. Think of HTTP as the language you speak and TLS as the locked room where you speak it. Go gives you a function that sets up the room and starts the conversation in one call.

The shortcut for simple servers

For local development or simple scripts, Go provides a single function that does everything. http.ListenAndServeTLS binds a port, loads certificate files, creates a TLS listener, and starts serving HTTP requests. It blocks the main goroutine until the server fails or the program exits.

Here is the minimal setup. You need a certificate file and a private key file. The function takes the address, the cert path, the key path, and an optional handler.

package main

import (
	"log"
	"net/http"
)

func main() {
	// Define a simple handler to prove the server works.
	// This function matches the http.HandlerFunc signature.
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		// Write a response to the client.
		// The TLS handshake already succeeded before this code runs.
		w.Write([]byte("Secure connection established"))
	})

	// ListenAndServeTLS loads the cert/key, creates a TLS listener, and starts the server.
	// It blocks the main goroutine.
	// The nil handler means http.DefaultServeMux is used, which contains our HandleFunc.
	// log.Fatal prints the error and exits with code 1 if the server cannot start.
	log.Fatal(http.ListenAndServeTLS(":443", "cert.pem", "key.pem", nil))
}

log.Fatal is the standard convention for server startup in Go. If the server cannot bind the port or load the certificates, the process should die. Printing the error and exiting with code 1 alerts the operating system or container orchestrator that something went wrong.

TLS is not optional. Encrypt the tunnel.

What happens under the hood

When you call ListenAndServeTLS, Go performs several steps behind the scenes. Understanding these steps helps you debug issues and move to advanced configurations.

First, the function reads the certificate and key files from disk. It parses the PEM-encoded data and builds a tls.Config struct. This struct holds all the TLS parameters: the certificate chain, the private key, the minimum TLS version, and the cipher suites.

Second, it creates a net.Listener using tls.Listen. This listener wraps a standard TCP listener. When a client connects, the TCP connection is established. Then the TLS handshake begins. The server sends its certificate to the client. The client verifies the certificate against its trusted root store. If verification succeeds, both sides negotiate a session key. If verification fails, the connection closes immediately. No HTTP data is read or processed.

Third, the listener starts an accept loop. Each accepted connection gets its own goroutine. The goroutine runs the TLS handshake, then passes the connection to the HTTP server. The HTTP server reads the request, routes it to your handler, and writes the response. The response is encrypted by the TLS layer before being sent over the network.

Trust the standard library. Do not reinvent the handshake.

Production configuration with http.Server

Production servers rarely use ListenAndServeTLS directly. You need more control. You need to set the minimum TLS version. You need to configure timeouts. You need to handle graceful shutdown. You need to load certificates from memory or a database instead of files.

For production, you create an http.Server struct and a tls.Config struct explicitly. You pass the config to the server and start it. This approach gives you full access to every TLS setting and server lifecycle hook.

Here is a realistic production setup. It loads certificates from files, enforces TLS 1.2 minimum, sets read and write timeouts, and handles graceful shutdown.

package main

import (
	"context"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"
)

func main() {
	// Load the certificate and key from files.
	// tls.LoadX509KeyPair returns a tls.Certificate struct.
	// This allows you to load certs from memory or other sources too.
	cert, err := tls.LoadX509KeyPair("cert.pem", "key.pem")
	if err != nil {
		// Exit immediately if certs are invalid.
		// The error message explains the problem.
		log.Fatalf("Failed to load certificate: %v", err)
	}

	// Configure TLS parameters.
	// MinVersion enforces TLS 1.2 or higher. TLS 1.0 and 1.1 are deprecated.
	// CipherSuites can be customized for compliance, but Go defaults are secure.
	// NextProtos enables HTTP/2 and HTTP/1.1.
	tlsConfig := &tls.Config{
		MinVersion: tls.VersionTLS12,
		Certificates: []tls.Certificate{cert},
		NextProtos:   []string{"h2", "http/1.1"},
	}

	// Create the HTTP server.
	// ReadTimeout limits the time for reading the entire request.
	// WriteTimeout limits the time for sending the response.
	// IdleTimeout closes connections that are idle.
	server := &http.Server{
		Addr:         ":443",
		Handler:      http.DefaultServeMux,
		ReadTimeout:  10 * time.Second,
		WriteTimeout: 10 * time.Second,
		IdleTimeout:  60 * time.Second,
		TLSConfig:    tlsConfig,
	}

	// Start the server in a goroutine.
	// ListenAndServeTLS blocks, so we run it in the background.
	// This allows the main goroutine to handle shutdown signals.
	go func() {
		// ListenAndServeTLS uses the TLSConfig from the server struct.
		// It returns an error only if the listener fails to start.
		if err := server.ListenAndServeTLS("", ""); err != http.ErrServerClosed {
			// Log the error if it's not a graceful shutdown.
			log.Fatalf("Server failed: %v", err)
		}
	}()

	// Wait for interrupt signal to gracefully shutdown the server.
	// signal.Notify registers the channel to receive OS signals.
	quit := make(chan os.Signal, 1)
	signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
	<-quit

	// Create a context with timeout for shutdown.
	// The server stops accepting new connections and waits for active requests.
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	// Shutdown the server gracefully.
	// It waits for active requests to complete or the context to expire.
	if err := server.Shutdown(ctx); err != nil {
		log.Fatalf("Server forced to shutdown: %v", err)
	}

	log.Println("Server exiting")
}

The tls.Config struct is the heart of TLS configuration. MinVersion prevents clients from connecting with outdated protocols. Certificates holds the certificate chain. NextProtos enables HTTP/2 support. Go's HTTP/2 implementation is automatic when you use http.Server and include h2 in NextProtos.

The http.Server struct controls the HTTP lifecycle. Timeouts prevent slow clients from holding resources. ReadTimeout protects against slowloris attacks. WriteTimeout ensures responses are sent promptly. IdleTimeout closes connections that are not being used.

Graceful shutdown is essential for production. When the server receives a termination signal, it stops accepting new connections. It waits for active requests to finish. If the context expires, it forces the shutdown. This prevents dropped requests and data corruption.

Load certs once. Reload only when necessary.

Common pitfalls and errors

TLS configuration can fail in subtle ways. The compiler catches type mismatches. The runtime catches certificate issues. The operating system catches permission errors. Knowing these errors saves debugging time.

Compiler errors

If you pass the wrong type to a function, the compiler rejects the code. For example, if you pass a function with the wrong signature as a handler, you get a type mismatch error.

The compiler complains with cannot use handler (type func(http.ResponseWriter, *http.Request)) as type http.Handler in argument to http.ListenAndServeTLS if the function signature does not match.

If you forget to import a package, you get an undefined error.

Forget to import crypto/tls and you get undefined: tls from the compiler.

Runtime errors

If the certificate files are missing or unreadable, the server fails to start.

The server logs open cert.pem: no such file or directory if the file path is wrong. The server logs open key.pem: permission denied if the file permissions are too restrictive.

If the certificate is self-signed or expired, clients reject the connection. The server might start, but the handshake fails.

The client logs x509: certificate signed by unknown authority for self-signed certs. The client logs x509: certificate has expired or is not yet valid for time issues.

If the TLS version is too low, modern clients reject the connection.

The client logs tls: handshake failure if the server does not support a compatible protocol version.

Permission errors

The private key file must be readable by the process user. It should not be readable by other users. Use chmod 600 key.pem to restrict access. If the key is readable by others, some systems reject it for security reasons.

The server logs tls: private key does not match public key if the cert and key do not belong together.

Verify your certificates match. Run openssl x509 -noout -modulus -in cert.pem | openssl md5 and openssl rsa -noout -modulus -in key.pem | openssl md5. The hashes must match.

Verify the chain. Trust the files.

Decision matrix

Choose the right approach based on your needs. Go provides options for every scenario.

Use http.ListenAndServeTLS when you are writing a quick script, a local development server, or a simple tool where you do not need custom timeouts or graceful shutdown. It is the fastest way to get HTTPS running.

Use http.Server with a custom tls.Config when you are building a production service. You need to enforce TLS versions, configure cipher suites, set timeouts, and handle graceful shutdown. This gives you full control over the server lifecycle.

Use golang.org/x/crypto/acme/autocert when you want zero-configuration HTTPS with Let's Encrypt. Autocert automatically obtains and renews certificates. It handles the ACME protocol for you. Use this for public-facing servers where you do not want to manage certificate files manually.

Use server.Serve with a custom net.Listener when you need to wrap the listener with custom logic. For example, you might want to log every TCP connection, implement rate limiting at the network level, or use a custom TLS listener that reloads certificates on disk changes.

Use mutual TLS (mTLS) when you need to verify client identities. Configure tls.Config.ClientAuth to require client certificates. This is common in internal microservices where every service must authenticate before communicating.

Pick the simplest tool that meets your requirements. Add complexity only when you need it.

Where to go next