🛂 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.RequireAndVerifyClientCertand give it aClientCAspool. 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/autocertfetches 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:
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
| Task | API |
|---|---|
| Serve HTTPS | srv.ListenAndServeTLS("cert.pem", "key.pem") |
| Cert file contents | full chain: leaf + intermediates |
| Generate a test cert | crypto/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 protocol | tls.Config{MinVersion: tls.VersionTLS12} |
| Never for production | InsecureSkipVerify: 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.Serveryou 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.
Related topics
Serving HTTP in net/http — handlers and HandlerFunc, the ResponseWriter and Request, the Go 1.22 method+pattern ServeMux with PathValue, decoding request bodies, and a production-shaped http.Server with timeouts.
httpRouting & MiddlewareDispatch and cross-cutting concerns — the Go 1.22 ServeMux (method+path patterns, wildcards, precedence), path values, middleware as Handler-wrapping-Handler, chaining order, and recovery/logging middleware.
httpHTTP ClientCalling services with net/http — http.Get vs a configured http.Client with timeouts, building requests and posting JSON, checking status, closing and draining the body, connection reuse via Transport, and context cancellation.
Check your understanding
Score: 0 / 51. 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.