Chapter 11: TLS/SSL: Encrypting Your Connections

Updated

October 31, 2026

Chapter 11: TLS/SSL: Encrypting Your Connections

Plaintext TCP connections are vulnerable to eavesdropping, packet tampering, and man-in-the-middle (MITM) attacks. In production, network communications traversing public networks—or zero-trust internal clouds—must be authenticated and encrypted using Transport Layer Security (TLS).

Go’s standard library features an enterprise-grade, memory-safe, pure-Go TLS implementation in package crypto/tls.

In this chapter, you will learn the mechanics of the TLS 1.3 handshake, how to upgrade raw TCP sockets to encrypted tls.Conn streams, how to generate in-memory certificates for development, how to implement mutual TLS (mTLS), and how to configure production-grade cipher suites and security controls.


How TLS Works (Handshake, Certificates, Cipher Suites)

TLS sits cleanly between the transport layer (TCP) and the application layer:

+-----------------------------------------------------------+
| Application Protocol (HTTP, Custom RPC, WebSocket)       |
+-----------------------------------------------------------+
                             │
                             ▼
+-----------------------------------------------------------+
| Transport Layer Security (crypto/tls)                     |
|  - Authenticity: X.509 Certificates signed by CAs         |
|  - Confidentiality: Symmetric Encryption (AES-GCM)        |
|  - Integrity: Message Authentication Code (HMAC / AEAD)   |
+-----------------------------------------------------------+
                             │
                             ▼
+-----------------------------------------------------------+
| Raw TCP Stream (net.Conn)                                 |
+-----------------------------------------------------------+

The TLS 1.3 Handshake (1-RTT)

TLS 1.3 modernized cryptography by reducing handshake latency from two round-trips (2-RTT in TLS 1.2) to a single round-trip (1-RTT):

Client                                                   Server
  │                                                        │
  │ ClientHello (Supported Cipher Suites + Key Share)      │
  ├───────────────────────────────────────────────────────►│
  │                                                        │
  │ ServerHello (Selected Cipher Suite + Key Share)        │
  │ {Server Certificate + Signature + Finished}            │
  │◄───────────────────────────────────────────────────────┤
  │                                                        │
  │ {Finished}                                             │
  ├───────────────────────────────────────────────────────►│
  │                                                        │
  │ [ Authenticated & Encrypted Application Data ]         │
  │◄══════════════════════════════════════════════════════►│
  1. ClientHello: The client proposes supported TLS versions and cryptographic algorithms, generating and transmitting an ephemeral public key share (ECDHE).
  2. ServerHello: The server selects the cipher suite, responds with its own ephemeral public key share, and immediately begins encrypting subsequent handshake messages using the derived keys.
  3. Certificate & Verification: The server provides its X.509 certificate. The client verifies the certificate against its trusted Certificate Authority (CA) root store and checks the hostname against the Subject Alternative Names (SAN).
  4. Symmetric Data Stream: The handshake concludes, and all subsequent application traffic is encrypted with high-speed symmetric AEAD ciphers (e.g., AES-256-GCM or CHACHA20-POLY1305).

Wrapping TCP Connections with tls.Conn

The crypto/tls package provides two primary abstractions: - tls.Listen: Creates a listener that automatically performs the TLS server handshake on inbound connections. - tls.Server / tls.Client: Wraps an already-established net.Conn.

1. The TLS Server

// tls_server.go
package main

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

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

    config := &tls.Config{
        Certificates: []tls.Certificate{cert},
        MinVersion:   tls.VersionTLS13, // Enforce modern TLS 1.3
    }

    ln, err := tls.Listen("tcp", ":8443", config)
    if err != nil {
        log.Fatalf("tls listen error: %v", err)
    }
    defer ln.Close()

    log.Printf("TLS Server listening on %s", ln.Addr())

    for {
        conn, err := ln.Accept()
        if err != nil {
            log.Printf("accept error: %v", err)
            continue
        }

        go func(c net.Conn) {
            defer c.Close()
            // TLS handshake occurs lazily on first Read/Write,
            // or explicitly via c.(*tls.Conn).Handshake()
            buf := make([]byte, 1024)
            n, _ := c.Read(buf)
            _, _ = c.Write(append([]byte("Encrypted Echo: "), buf[:n]...))
        }(conn)
    }
}

2. The TLS Client

// tls_client.go
package main

import (
    "crypto/tls"
    "fmt"
    "log"
)

func main() {
    config := &tls.Config{
        ServerName: "localhost",      // Must match certificate Common Name or SAN
        MinVersion: tls.VersionTLS13,
    }

    conn, err := tls.Dial("tcp", "127.0.0.1:8443", config)
    if err != nil {
        log.Fatalf("tls dial failed: %v", err)
    }
    defer conn.Close()

    // Verify negotiated TLS state
    state := conn.ConnectionState()
    fmt.Printf("Connected using TLS version: 0x%04x, CipherSuite: 0x%04x\n",
        state.Version, state.CipherSuite)

    _, _ = conn.Write([]byte("Hello secure world!"))
    buf := make([]byte, 1024)
    n, _ := conn.Read(buf)
    fmt.Printf("Received: %s\n", string(buf[:n]))
}

Creating Self-Signed Certificates for Development

For automated integration testing and local development, you can generate valid self-signed X.509 certificates directly in Go memory without depending on external tools like openssl.

// certgen.go
package main

import (
    "crypto/ecdsa"
    "crypto/elliptic"
    "crypto/rand"
    "crypto/x509"
    "crypto/x509/pkix"
    "encoding/pem"
    "math/big"
    "net"
    "os"
    "time"
)

func GenerateDevCerts(certFile, keyFile string) error {
    priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
    if err != nil {
        return err
    }

    template := x509.Certificate{
        SerialNumber: big.NewInt(1),
        Subject: pkix.Name{
            Organization: []string{"Dev Lab Sockets"},
            CommonName:   "localhost",
        },
        NotBefore:             time.Now(),
        NotAfter:              time.Now().Add(365 * 24 * time.Hour),
        KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
        ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
        BasicConstraintsValid: true,
        IPAddresses:           []net.IP{net.ParseIP("127.0.0.1")},
        DNSNames:              []string{"localhost"},
    }

    derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
    if err != nil {
        return err
    }

    // Save Certificate PEM
    certOut, _ := os.Create(certFile)
    defer certOut.Close()
    _ = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})

    // Save Private Key PEM
    keyBytes, _ := x509.MarshalECPrivateKey(priv)
    keyOut, _ := os.Create(keyFile)
    defer keyOut.Close()
    _ = pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes})

    return nil
}

Mutual TLS (mTLS) for Client Authentication

In zero-trust architectures and inter-service communication, both sides must authenticate each other. With mTLS, the server not only proves its identity to the client, but also demands and validates an X.509 certificate presented by the client.

// Configuring mTLS on the server:
caCert, _ := os.ReadFile("ca.crt")
caPool := x509.NewCertPool()
caPool.AppendCertsFromPEM(caCert)

serverTLSConfig := &tls.Config{
    Certificates: []tls.Certificate{serverCert},
    ClientCAs:    caPool,
    // Enforce valid client certificate signed by trusted CA
    ClientAuth:   tls.RequireAndVerifyClientCert,
    MinVersion:   tls.VersionTLS13,
}

On the client side, attach the client’s certificate:

clientCert, _ := tls.LoadX509KeyPair("client.crt", "client.key")
clientTLSConfig := &tls.Config{
    Certificates: []tls.Certificate{clientCert},
    RootCAs:      caPool,
    ServerName:   "api.internal.service",
}

Configuring TLS for Production (Cipher Suites, Versions, OCSP)

When deploying Go network services to production, apply these security hardening practices:

  1. Enforce MinVersion: tls.VersionTLS13: TLS 1.3 eliminates vulnerable legacy ciphers (such as CBC mode and RSA key exchange) and guarantees forward secrecy.

  2. Explicit Cipher Suites (if supporting TLS 1.2): If legacy clients require TLS 1.2, restrict allowed cipher suites strictly to AEAD constructions:

    CipherSuites: []uint16{
        tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
        tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
        tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
        tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
    }
  3. Session Resumption & Tickets: Go supports stateless TLS session tickets out of the box, reducing handshake latency for reconnecting clients.


Chapter Summary

  • TLS ensures confidentiality, integrity, and mutual authentication above the transport layer.
  • TLS 1.3 simplifies the handshake to 1-RTT and eliminates insecure legacy cryptographic primitives.
  • tls.Listen and tls.Dial wrap standard TCP connections seamlessly into encrypted streams satisfying net.Conn.
  • Go can generate in-memory self-signed test certificates using crypto/x509 without external dependencies.
  • mTLS (RequireAndVerifyClientCert) provides cryptographically validated identity verification for both client and server.
  • In the next chapter, we will build bidirectional real-time communication systems using WebSockets.