{} The Go Reference

Http · Web · Intermediate

TLS & HTTPS

How TLS secures HTTP — the handshake, certificates and the CA chain, serving with ListenAndServeTLS, verifying clients, mTLS, and autocert.

Http Intermediate ⏱ 7 min read Complete

🛂 Analogy

TLS is showing your passport at a border. The server hands over a certificate (passport) stamped by a certificate authority (the issuing government the client already trusts). The client checks the stamp and that the name matches — then both sides agree on a secret only they share, and everything after is sealed in an envelope nobody else can read.

How TLS works

A TLS handshake does three jobs before any HTTP flows: authenticate the server (and optionally the client), agree on keys, and encrypt the rest of the connection. The server presents a certificate signed by a CA; the client verifies that signature chain up to a trusted root and confirms the hostname matches.

sequenceDiagram
participant C as Client
participant S as Server
C->>S: ClientHello (versions, cipher suites)
S->>C: ServerHello + Certificate (signed by CA)
C->>C: verify cert chain + hostname
C->>S: key exchange
S->>C: Finished (handshake done)
C->>S: encrypted HTTP request
S->>C: encrypted HTTP response

Once the handshake completes, HTTPS is just HTTP inside that encrypted channel.

Serving HTTPS in Go

http.ListenAndServeTLS takes a certificate file and a private-key file and serves your mux over TLS. For real timeouts, set them on an http.Server and call its ListenAndServeTLS:

mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "hello over TLS")
})

srv := &http.Server{
	Addr:         ":443",
	Handler:      mux,
	ReadTimeout:  5 * time.Second,
	WriteTimeout: 10 * time.Second,
}
// cert.pem must include the full chain (leaf + intermediates)
log.Fatal(srv.ListenAndServeTLS("cert.pem", "key.pem"))

For local testing you can generate a self-signed pair with Go’s bundled tool:

# writes cert.pem and key.pem for localhost, valid 24h
go run $(go env GOROOT)/src/crypto/tls/generate_cert.go --host localhost

On the client side, Go verifies certificates by default — no configuration needed:

client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get("https://example.com") // chain + hostname checked for you
if err != nil { log.Fatal(err) }
defer resp.Body.Close()

To trust a self-signed cert in a test, add it to a cert pool instead of disabling verification:

pool := x509.NewCertPool()
pool.AppendCertsFromPEM(pemBytes) // your test CA / self-signed cert
client := &http.Client{
	Transport: &http.Transport{
		TLSClientConfig: &tls.Config{RootCAs: pool}, // still verifies — just against your pool
	},
}

Mutual TLS and automatic certs

Two extensions you’ll meet in production:

  • mTLS — the server also demands a client certificate. Set tls.Config.ClientAuth = tls.RequireAndVerifyClientCert and give it a ClientCAs pool. Now both sides authenticate each other, common for service-to-service traffic.
  • Let’s Encrypt / autocert — instead of managing cert files by hand, golang.org/x/crypto/acme/autocert fetches and renews free certificates automatically:
m := &autocert.Manager{
	Prompt:     autocert.AcceptTOS,
	HostPolicy: autocert.HostWhitelist("example.com"),
	Cache:      autocert.DirCache("certs"),
}
srv := &http.Server{
	Addr:      ":443",
	Handler:   mux,
	TLSConfig: m.TLSConfig(),
}
log.Fatal(srv.ListenAndServeTLS("", "")) // certs come from the manager

Build a self-signed cert in memory

You can generate a key and a self-signed certificate entirely in memory — no files, no network — and inspect it. This is the same DER the tls package parses on the wire:

cert.go — editable & runnable
package main

import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"math/big"
"time"
)

func main() {
// Generate a private key fully in memory — no files, no network.
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
	fmt.Println("key error:", err)
	return
}

// Describe the certificate: who it's for and how long it's valid.
template := x509.Certificate{
	SerialNumber: big.NewInt(1),
	Subject: pkix.Name{
		Organization: []string{"Go Learn Demo"},
		CommonName:   "localhost",
	},
	DNSNames:  []string{"localhost"},
	NotBefore: time.Unix(0, 0),
	NotAfter:  time.Unix(0, 0).Add(365 * 24 * time.Hour),
	KeyUsage:  x509.KeyUsageDigitalSignature,
}

// Self-sign: the template is both the certificate AND the issuer (its own CA).
der, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
if err != nil {
	fmt.Println("cert error:", err)
	return
}

// Parse the DER bytes back into a structured certificate and inspect it.
cert, err := x509.ParseCertificate(der)
if err != nil {
	fmt.Println("parse error:", err)
	return
}

fmt.Println("subject:    ", cert.Subject.CommonName)
fmt.Println("org:        ", cert.Subject.Organization[0])
fmt.Println("dns names:  ", cert.DNSNames)
fmt.Println("self-signed?", cert.Subject.CommonName == cert.Issuer.CommonName)
fmt.Println("algorithm:  ", cert.SignatureAlgorithm)
}

Secure defaults

Beyond a valid certificate, harden the TLS config — modern Go already picks safe defaults, but be explicit for anything sensitive:

srv.TLSConfig = &tls.Config{
	MinVersion: tls.VersionTLS12, // refuse SSLv3/TLS1.0/1.1 downgrades (prefer TLS13)
	// Go 1.17+ orders cipher suites for you; only override if you know why.
}

Then add HTTP-layer protections (HSTS, security headers, body limits) on top — that’s the subject of the web security page.

Reference

TaskAPI
Serve HTTPSsrv.ListenAndServeTLS("cert.pem", "key.pem")
Cert file contentsfull chain: leaf + intermediates
Generate a test certcrypto/tls/generate_cert.go --host localhost
Trust a test CA (client)add to x509.CertPool, set RootCAs
Require a client cert (mTLS)ClientAuth: tls.RequireAndVerifyClientCert + ClientCAs
Auto certs (Let’s Encrypt)golang.org/x/crypto/acme/autocert
Minimum protocoltls.Config{MinVersion: tls.VersionTLS12}
Never for productionInsecureSkipVerify: true

🔒 Never set InsecureSkipVerify in production

The playground builds a cert in memory only — no listener, no network — so it’s safe and deterministic. The cardinal sin of TLS in Go is reaching for InsecureSkipVerify: true to “make the error go away.” It disables chain and hostname checks and opens you to man-in-the-middle attacks. For a self-signed cert in tests, add it to a RootCAs/ClientCAs pool instead so verification still happens. In production, serve a CA-signed cert (autocert makes this painless) and keep the client’s default verification on.

See also

  • web security — security headers, HSTS, body limits, and injection defense.
  • HTTP server — the http.Server you attach TLS to.
  • HTTP client — certificate verification on the calling side.
  • TCP sockets — the connection TLS wraps.

Next: the broader security surface — headers, injection, and safe defaults — web security.

Check your understanding

Score: 0 / 5

1. What does a certificate authority (CA) chain let a client verify during the TLS handshake?

The server presents a certificate signed (directly or via intermediates) by a CA the client trusts. The client walks that chain up to a root in its trust store and checks the hostname matches, proving identity and enabling an encrypted session. No trusted signer, no trust.

2. What does setting InsecureSkipVerify: true on an http.Client's TLS config do?

InsecureSkipVerify turns off the chain and hostname checks, defeating the entire point of TLS and exposing you to man-in-the-middle attacks. Go's client verifies certificates by default — leave it that way. Use it only for throwaway local tests with self-signed certs.

3. What additionally does mutual TLS (mTLS) require compared to ordinary HTTPS?

In normal HTTPS only the server proves its identity. In mTLS both sides present certificates: the server requires and verifies a client certificate too (via tls.Config.ClientAuth = RequireAndVerifyClientCert), giving strong two-way authentication common in service-to-service setups.

4. What does setting tls.Config.MinVersion = tls.VersionTLS12 (or TLS13) protect against?

Without a floor, a server might negotiate a long-broken version. Setting MinVersion to TLS 1.2 (or 1.3) refuses those, closing downgrade attacks. Modern Go already defaults the minimum to TLS 1.2, but set it explicitly for clarity and to require 1.3 where you can.

5. Your cert.pem works in a browser but Go clients fail with 'unknown authority'. Likely cause?

Go verifies strictly and won't fetch missing intermediates the way some browsers do. Concatenate the leaf certificate and any intermediates into cert.pem (leaf first). The fix is never InsecureSkipVerify — that disables verification entirely.

Comments

Sign in with GitHub to join the discussion.