Secure gRPC Communication Setup Guide
gRPC uses HTTP/2 and Protocol Buffers to provide efficient service-to-service communication. Like any RPC framework, leaving it with defaults is insecure: no TLS means plaintext traffic, no authentication means any client can call any method, and no rate limiting means a single bad actor can exhaust your service. This guide adds each security layer with working code.
What gRPC Security Looks Like
A secured gRPC deployment has:
- TLS — all traffic encrypted in transit
- Mutual TLS (mTLS) — server and client both prove identity with certificates
- Token authentication — calls carry a JWT or API key validated by the server
- Authorization interceptors — method-level access control
- Reflection disabled — service schema not exposed to unauthenticated clients
- Rate limiting — per-client call rate caps
Step 1 — Generate Certificates
Create a CA and issue certificates for server and clients:
# CA
openssl genrsa -out ca.key 4096
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \
-subj "/CN=gRPC-CA/O=MyOrg"
# Server cert
openssl genrsa -out server.key 4096
openssl req -new -key server.key -out server.csr \
-subj "/CN=grpc.internal/O=MyOrg"
openssl x509 -req -days 825 \
-in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-out server.crt
# Client cert
openssl genrsa -out client.key 4096
openssl req -new -key client.key -out client.csr \
-subj "/CN=grpc-client-01/O=MyOrg"
openssl x509 -req -days 825 \
-in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-out client.crt
Step 2 — Server with Mutual TLS (Go)
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"log"
"net"
"os"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
pb "myservice/proto"
)
func loadTLSCredentials() (credentials.TransportCredentials, error) {
// Load CA cert to verify clients
caCert, err := os.ReadFile("ca.crt")
if err != nil {
return nil, err
}
certPool := x509.NewCertPool()
certPool.AppendCertsFromPEM(caCert)
// Load server cert and key
serverCert, err := tls.LoadX509KeyPair("server.crt", "server.key")
if err != nil {
return nil, err
}
config := &tls.Config{
Certificates: []tls.Certificate{serverCert},
ClientAuth: tls.RequireAndVerifyClientCert, // enforce mTLS
ClientCAs: certPool,
MinVersion: tls.VersionTLS13, // TLS 1.3 only
}
return credentials.NewTLS(config), nil
}
func main() {
tlsCredentials, err := loadTLSCredentials()
if err != nil {
log.Fatalf("Failed to load TLS credentials: %v", err)
}
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("Failed to listen: %v", err)
}
s := grpc.NewServer(
grpc.Creds(tlsCredentials),
grpc.UnaryInterceptor(authInterceptor),
)
pb.RegisterMyServiceServer(s, &myServiceServer{})
fmt.Println("gRPC server listening on :50051 (mTLS)")
s.Serve(lis)
}
Step 3 — Authentication Interceptor (JWT)
Add JWT validation as a server-side interceptor that runs before every method call:
import (
"context"
"strings"
"github.com/golang-jwt/jwt/v5"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
var jwtSecret = []byte("your-secret-key") // use env var in production
// Methods that don't require authentication
var publicMethods = map[string]bool{
"/myservice.MyService/HealthCheck": true,
}
func authInterceptor(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
// Skip auth for public methods
if publicMethods[info.FullMethod] {
return handler(ctx, req)
}
// Extract metadata
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "missing metadata")
}
// Get authorization header
authHeader := md.Get("authorization")
if len(authHeader) == 0 {
return nil, status.Error(codes.Unauthenticated, "missing authorization header")
}
// Validate Bearer token
tokenStr := strings.TrimPrefix(authHeader[0], "Bearer ")
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return jwtSecret, nil
})
if err != nil || !token.Valid {
return nil, status.Error(codes.Unauthenticated, "invalid token")
}
// Add claims to context for use in handlers
ctx = context.WithValue(ctx, "claims", token.Claims)
return handler(ctx, req)
}
Step 4 — Client with mTLS and Token
func newSecureClient() (pb.MyServiceClient, error) {
caCert, _ := os.ReadFile("ca.crt")
certPool := x509.NewCertPool()
certPool.AppendCertsFromPEM(caCert)
clientCert, _ := tls.LoadX509KeyPair("client.crt", "client.key")
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{clientCert},
RootCAs: certPool,
MinVersion: tls.VersionTLS13,
}
creds := credentials.NewTLS(tlsConfig)
conn, err := grpc.Dial(
"grpc.internal:50051",
grpc.WithTransportCredentials(creds),
grpc.WithUnaryInterceptor(tokenInterceptor("your-jwt-token")),
)
if err != nil {
return nil, err
}
return pb.NewMyServiceClient(conn), nil
}
func tokenInterceptor(token string) grpc.UnaryClientInterceptor {
return func(ctx context.Context, method string, req, reply interface{},
cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
ctx = metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+token)
return invoker(ctx, method, req, reply, cc, opts...)
}
}
Step 5 — Disable Server Reflection in Production
gRPC server reflection lets any client with a grpcurl or grpc_cli tool enumerate all your methods. Disable it in production:
// BAD — reflection enabled (development convenience)
import "google.golang.org/grpc/reflection"
reflection.Register(s) // do NOT include this in production builds
// GOOD — conditional registration
if os.Getenv("ENVIRONMENT") == "development" {
reflection.Register(s)
}
Verify reflection is disabled:
# This should return an error in production
grpcurl -insecure localhost:50051 list
# Expected: Error: failed to list services: rpc error: code = Unimplemented
Step 6 — Rate Limiting
Add per-client rate limiting using a token bucket interceptor:
import "golang.org/x/time/rate"
type rateLimiter struct {
limiters map[string]*rate.Limiter
mu sync.Mutex
r rate.Limit
b int
}
func newRateLimiter(r rate.Limit, b int) *rateLimiter {
return &rateLimiter{
limiters: make(map[string]*rate.Limiter),
r: r,
b: b,
}
}
func (rl *rateLimiter) getLimiter(clientID string) *rate.Limiter {
rl.mu.Lock()
defer rl.mu.Unlock()
if l, exists := rl.limiters[clientID]; exists {
return l
}
l := rate.NewLimiter(rl.r, rl.b)
rl.limiters[clientID] = l
return l
}
// 100 requests/second, burst of 10
var limiter = newRateLimiter(100, 10)
func rateLimitInterceptor(ctx context.Context, req interface{},
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
md, _ := metadata.FromIncomingContext(ctx)
clientID := "anonymous"
if ids := md.Get("client-id"); len(ids) > 0 {
clientID = ids[0]
}
if !limiter.getLimiter(clientID).Allow() {
return nil, status.Error(codes.ResourceExhausted, "rate limit exceeded")
}
return handler(ctx, req)
}
Chain multiple interceptors:
s := grpc.NewServer(
grpc.Creds(tlsCredentials),
grpc.ChainUnaryInterceptor(
rateLimitInterceptor,
authInterceptor,
loggingInterceptor,
),
)
Security Checklist
[ ] TLS 1.3 enforced (no TLS 1.0/1.1/1.2 allowed)
[ ] Mutual TLS — clients must present valid cert
[ ] JWT or API key validated on every call
[ ] Server reflection disabled in production
[ ] Rate limiting per client
[ ] Method-level authorization (not just service-level)
[ ] Certificates rotated regularly
[ ] CA private key stored offline / in HSM
Related Reading
- Secure GraphQL API Hardening Guide
- Secure MQTT Broker Setup for IoT
- How to Set Up Cilium for Network Security
Built by theluckystrike — More at zovo.one