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.
