gRPC TLS Security

gRPC transmits data as binary over a network. Without encryption, anyone on the network path can read or modify those bytes. TLS (Transport Layer Security) encrypts the connection between client and server so that only the two endpoints can read the data. In production, TLS is not optional — it is mandatory.

What TLS Does

Without TLS (insecure):
  Client ──[binary protobuf bytes — readable by anyone on the network]──► Server

With TLS (encrypted):
  Client ──[encrypted ciphertext — unreadable without the private key]──► Server

TLS provides three guarantees:
  1. Confidentiality  — data cannot be read in transit
  2. Integrity        — data cannot be modified without detection
  3. Authentication   — client verifies it is talking to the real server

How TLS Works — The Handshake

TLS Handshake (happens once before any gRPC data flows):

Client                                          Server
  │                                                │
  │──── ClientHello (TLS version, ciphers) ───────►│
  │                                                │
  │◄─── ServerHello (chosen cipher) ───────────────│
  │◄─── Certificate(server's public key + identity)│
  │◄─── ServerHelloDone ───────────────────────────│
  │                                                │
  │  [Client verifies certificate against CA]      │
  │                                                │
  │──── PreMasterSecret(encrypted with server key)►│
  │──── ChangeCipherSpec──────────────────────────►│
  │──── Finished ─────────────────────────────────►│
  │                                                │
  │◄─── ChangeCipherSpec ──────────────────────────│
  │◄─── Finished ──────────────────────────────────│
  │                                                │
  │  [Encrypted gRPC calls begin here]             │

Types of TLS in gRPC

┌──────────────────────────┬──────────────────────────────────────────────┐
│ Mode                     │ Description                                  │
├──────────────────────────┼──────────────────────────────────────────────┤
│ Insecure (no TLS)        │ Development only. Never use in production.   │
│ Server-side TLS          │ Client verifies server identity only.        │
│ Mutual TLS (mTLS)        │ Both sides verify each other's identity.     │
└──────────────────────────┴──────────────────────────────────────────────┘

Certificates — The Key Concepts

Certificate Authority (CA):
  A trusted organisation (or your own internal CA) that signs certificates.
  Examples: Let's Encrypt, DigiCert, your company's internal CA.

Server Certificate:
  A file (cert.pem) that proves the server's identity.
  Signed by the CA. Contains the server's public key.

Private Key:
  A secret file (key.pem) that lives only on the server.
  Used to decrypt messages encrypted with the public key.

Certificate Chain:
  CA cert ──signs──► Intermediate cert ──signs──► Server cert
  Clients trust the CA, so they trust everything the CA signed.

Files you typically work with:
  ca.crt    — Certificate Authority certificate (shared)
  server.crt — Server certificate (public, shared with clients)
  server.key — Server private key (SECRET, never share)
  client.crt — Client certificate (for mTLS only)
  client.key — Client private key (SECRET, for mTLS only)

Generating Certificates for Development

Using OpenSSL to create a self-signed CA and server certificate:

# Step 1: Generate the CA private key and self-signed certificate
openssl genrsa -out ca.key 4096
openssl req -new -x509 -days 365 -key ca.key -out ca.crt \
  -subj "/CN=MyDevCA"

# Step 2: Generate the server private key and certificate signing request
openssl genrsa -out server.key 4096
openssl req -new -key server.key -out server.csr \
  -subj "/CN=localhost"

# Step 3: Sign the server certificate with the CA
openssl x509 -req -days 365 \
  -in server.csr \
  -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out server.crt

# Result: ca.crt, server.crt, server.key

Server-Side TLS in Go

Server — load certificate and key:
  import (
    "crypto/tls"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
  )

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

    tlsConfig := &tls.Config{
      Certificates: []tls.Certificate{cert},
      MinVersion:   tls.VersionTLS13,   // enforce TLS 1.3 minimum
    }

    creds := credentials.NewTLS(tlsConfig)
    grpcServer := grpc.NewServer(grpc.Creds(creds))

    pb.RegisterUserServiceServer(grpcServer, &userServer{})

    lis, _ := net.Listen("tcp", ":443")
    grpcServer.Serve(lis)
  }

Client — trust the CA, verify server certificate:
  import (
    "crypto/tls"
    "crypto/x509"
    "os"
    "google.golang.org/grpc/credentials"
  )

  func main() {
    // Load the CA certificate to verify server identity
    caCert, err := os.ReadFile("ca.crt")
    if err != nil {
      log.Fatalf("failed to read CA cert: %v", err)
    }

    certPool := x509.NewCertPool()
    certPool.AppendCertsFromPEM(caCert)

    tlsConfig := &tls.Config{
      RootCAs:    certPool,
      ServerName: "myservice.example.com",  // must match cert CN or SAN
      MinVersion: tls.VersionTLS13,
    }

    creds := credentials.NewTLS(tlsConfig)
    conn, _ := grpc.NewClient("myservice.example.com:443",
      grpc.WithTransportCredentials(creds))
    defer conn.Close()
  }

Mutual TLS (mTLS)

In standard TLS the client verifies the server. In mTLS both the server and the client present certificates. The server rejects any client that does not have a valid certificate signed by the trusted CA. mTLS is the standard for securing service-to-service calls inside a cluster.

mTLS Handshake — both sides authenticate:

Client                                          Server
  │──── ClientHello  ──────────────────────────────►│
  │◄─── ServerHello + Server Certificate  ──────────│
  │◄─── CertificateRequest  ────────────────────────│  ← server asks for client cert
  │──── Client Certificate ────────────────────────►│
  │──── CertificateVerify (signed with client key) ►│
  │◄─── [Server verifies client cert against CA] ───│
  │  [Both sides now trust each other]              │
Server — require client certificate:
  tlsConfig := &tls.Config{
    Certificates: []tls.Certificate{serverCert},
    ClientCAs:    certPool,                    // CA to verify client certs
    ClientAuth:   tls.RequireAndVerifyClientCert,  // reject uncertified clients
    MinVersion:   tls.VersionTLS13,
  }

Client — present client certificate:
  clientCert, _ := tls.LoadX509KeyPair("client.crt", "client.key")

  tlsConfig := &tls.Config{
    Certificates: []tls.Certificate{clientCert},   // present to server
    RootCAs:      certPool,                        // verify server cert
    ServerName:   "myservice.example.com",
    MinVersion:   tls.VersionTLS13,
  }

TLS in Kubernetes with cert-manager

Manual certificate management does not scale. In Kubernetes, cert-manager automates certificate issuance and renewal from Let's Encrypt or an internal CA.

Flow with cert-manager:
  1. You create a Certificate resource in Kubernetes
  2. cert-manager contacts Let's Encrypt (or internal CA)
  3. cert-manager stores the cert and key in a Kubernetes Secret
  4. Your gRPC server reads the Secret, serves TLS
  5. cert-manager auto-renews before expiry

Certificate resource example:
  apiVersion: cert-manager.io/v1
  kind: Certificate
  metadata:
    name: grpc-server-cert
  spec:
    secretName: grpc-server-tls      ← Kubernetes Secret name
    issuerRef:
      name: letsencrypt-prod
      kind: ClusterIssuer
    dnsNames:
      - payment.internal
      - payment.mycompany.com

Common TLS Errors and Fixes

┌──────────────────────────────────────────┬──────────────────────────────────┐
│ Error                                    │ Fix                              │
├──────────────────────────────────────────┼──────────────────────────────────┤
│ x509: certificate signed by unknown CA   │ Add CA cert to client's trust    │
│ x509: certificate is valid for X, not Y  │ ServerName must match cert SAN   │
│ certificate has expired                  │ Renew and reload the certificate │
│ tls: no certificates configured          │ Server loaded wrong file paths   │
│ connection refused on port 443           │ Listener is on wrong port        │
└──────────────────────────────────────────┴──────────────────────────────────┘

Summary

TLS encrypts gRPC connections and verifies server identity. Generate a CA, then sign server and client certificates with that CA. For service-to-service calls inside a cluster, use mTLS so both sides authenticate. In Kubernetes, use cert-manager to automate certificate lifecycle. Always enforce TLS 1.3 as the minimum version and never use insecure channels outside of local development.

Leave a Comment

Your email address will not be published. Required fields are marked *