Skip to content

gRPC Security Cheat Sheet

Introduction

gRPC (gRPC Remote Procedure Call) is a high-performance, language-neutral RPC framework that uses HTTP/2 for transport and Protocol Buffers for serialization. While gRPC offers significant performance advantages for microservices and distributed systems, it introduces unique security challenges that differ from traditional REST APIs.

The following sections cover essential security controls for protecting gRPC services from common attack vectors.

Transport Security

Always Use TLS in Production

Production deployments need TLS encryption to protect against eavesdropping and man-in-the-middle attacks.

// Go - Secure server with TLS
creds, err := credentials.NewServerTLSFromFile(certFile, keyFile)
if err != nil {
    log.Fatalf("Failed to load TLS credentials: %v", err)
}
s := grpc.NewServer(grpc.Creds(creds))

Configure TLS 1.2 or higher with strong cipher suites, and disable weak protocols and ciphers.

Implement Mutual TLS (mTLS) for Service-to-Service Communication

mTLS provides mutual authentication where both client and server verify each other's certificates, enabling zero-trust communication.

// Go - mTLS client configuration
cert, err := tls.LoadX509KeyPair(clientCertFile, clientKeyFile)
caCert, err := ioutil.ReadFile(caCertFile)
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)

creds := credentials.NewTLS(&tls.Config{
    Certificates: []tls.Certificate{cert},
    RootCAs:      caCertPool,
})
conn, err := grpc.Dial(address, grpc.WithTransportCredentials(creds))

Use short-lived certificates (90 days or less) with automated rotation to limit the impact of compromised keys.

Authentication and Authorization

Implement Strong Authentication

Implement authentication checks for each protected service method.

Token-Based Authentication

// Go - JWT token validation interceptor
func authInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return nil, status.Errorf(codes.Unauthenticated, "missing metadata")
    }

    tokens := md["authorization"]
    if len(tokens) == 0 {
        return nil, status.Errorf(codes.Unauthenticated, "missing authorization token")
    }

    token := strings.TrimPrefix(tokens[0], "Bearer ")
    if !validateJWT(token) {
        return nil, status.Errorf(codes.Unauthenticated, "invalid token")
    }

    return handler(ctx, req)
}

API Key Authentication

// Go - API key validation
func validateAPIKey(ctx context.Context) error {
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok {
        return status.Error(codes.Unauthenticated, "missing metadata")
    }

    keys := md["x-api-key"]
    if len(keys) == 0 || !isValidAPIKey(keys[0]) {
        return status.Error(codes.Unauthenticated, "invalid API key")
    }
    return nil
}

Implement token expiration and refresh mechanisms with short-lived tokens (15-60 minutes). Avoid embedding credentials in gRPC method parameters - use metadata headers.

Enforce Granular Authorization

Implement method-level authorization checks based on the principle of least privilege.

// Go - Role-based authorization
func authorizeMethod(ctx context.Context, methodName string, userRoles []string) error {
    requiredRole, exists := methodPermissions[methodName]
    if !exists {
        return status.Errorf(codes.PermissionDenied, "method not found")
    }

    for _, role := range userRoles {
        if role == requiredRole {
            return nil
        }
    }

    return status.Errorf(codes.PermissionDenied, "insufficient permissions")
}

Log all authorization failures to detect potential attacks and compliance violations.

Input Validation and Data Security

Validate All Protocol Buffer Messages

Protocol Buffers provide type safety but not business logic validation. Always perform thorough server-side validation.

// Use protoc-gen-validate for automatic validation
syntax = "proto3";
import "validate/validate.proto";

message CreateUserRequest {
  string email = 1 [(validate.rules).string.email = true];
  string name = 2 [(validate.rules).string = {min_len: 1, max_len: 100}];
  int32 age = 3 [(validate.rules).int32 = {gte: 0, lte: 150}];
}

Use allowlist validation for string inputs to prevent unexpected characters and injection attempts.

Prevent Injection Attacks

Validate user input carefully when used in database queries or system operations.

// Go - Safe database query with parameterization
func getUserByEmail(email string) (*User, error) {
    if !isValidEmail(email) {
        return nil, errors.New("invalid email format")
    }

    query := "SELECT id, name, email FROM users WHERE email = ?"
    row := db.QueryRow(query, email)

    var user User
    err := row.Scan(&user.ID, &user.Name, &user.Email)
    return &user, err
}

Always use prepared statements for database operations to prevent SQL injection.

Implement Message Size Limits

gRPC's streaming capabilities allow clients to send arbitrarily large messages, potentially exhausting server memory and triggering denial-of-service conditions. Set clear limits on message sizes.

// Go - Set message size limits
s := grpc.NewServer(
    grpc.MaxRecvMsgSize(4*1024*1024), // 4MB max receive
    grpc.MaxSendMsgSize(4*1024*1024), // 4MB max send
)

Rate Limiting and Resource Protection

Implement Request Rate Limiting

Protect services from request flooding and resource exhaustion.

// Go - Rate limiting with memory management
import (
    "golang.org/x/time/rate"
    "sync"
    "time"
)

type RateLimiterStore struct {
    limiters map[string]*rateLimiterEntry
    mu       sync.RWMutex
}

type rateLimiterEntry struct {
    limiter  *rate.Limiter
    lastSeen time.Time
}

var store = &RateLimiterStore{
    limiters: make(map[string]*rateLimiterEntry),
}

func rateLimitInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
    clientIP := getClientIP(ctx)

    store.mu.Lock()
    entry, exists := store.limiters[clientIP]
    if !exists {
        entry = &rateLimiterEntry{
            limiter:  rate.NewLimiter(rate.Limit(10), 20), // 10 req/sec, burst 20
            lastSeen: time.Now(),
        }
        store.limiters[clientIP] = entry
    }
    entry.lastSeen = time.Now()
    store.mu.Unlock()

    if !entry.limiter.Allow() {
        return nil, status.Errorf(codes.ResourceExhausted, "rate limit exceeded")
    }

    return handler(ctx, req)
}

// Cleanup old limiters periodically
func cleanupOldLimiters() {
    store.mu.Lock()
    defer store.mu.Unlock()

    cutoff := time.Now().Add(-time.Hour)
    for ip, entry := range store.limiters {
        if entry.lastSeen.Before(cutoff) {
            delete(store.limiters, ip)
        }
    }
}

For production environments, use external rate limiting solutions like Redis or dedicated services.

Set Appropriate Timeouts

Configure timeouts to prevent resource exhaustion from long-running requests.

// Go - Server-side timeout for resource protection
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
    // Check if client already set a deadline
    if deadline, ok := ctx.Deadline(); ok && time.Until(deadline) < 5*time.Second {
        return processGetUser(ctx, req)
    }

    // Set defensive timeout to prevent resource exhaustion
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    return processGetUser(ctx, req)
}

Configure both client-side and server-side timeouts appropriately for your use case.

Error Handling and Information Disclosure

Secure Error Responses

Detailed error messages can reveal system internals to attackers. Return generic error messages while logging detailed information server-side.

// Go - Secure error handling
func (s *server) ProcessPayment(ctx context.Context, req *pb.PaymentRequest) (*pb.PaymentResponse, error) {
    if err := validatePayment(req); err != nil {
        // Log detailed error server-side
        log.Printf("Payment validation failed for user %s: %v", getUserID(ctx), err)
        // Return generic error to client
        return nil, status.Error(codes.InvalidArgument, "invalid payment request")
    }

    // Continue processing...
}

Use appropriate gRPC status codes: UNAUTHENTICATED for auth failures, PERMISSION_DENIED for authorization failures, INVALID_ARGUMENT for validation errors.

Implement Structured Logging

Log security events to help detect attacks and investigate incidents. Include authentication attempts, authorization failures, and suspicious activities.

// Go - Security event logging
func logSecurityEvent(event string, userID string, clientIP string, success bool) {
    log.Printf("SECURITY_EVENT: %s | User: %s | IP: %s | Success: %t | Time: %s",
        event, userID, clientIP, success, time.Now().UTC().Format(time.RFC3339))
}

Include correlation IDs to track requests across distributed services and ensure logs don't contain sensitive data like passwords or tokens.

Service Discovery and Reflection

Disable gRPC Reflection in Production

gRPC reflection allows clients to discover service methods and message schemas at runtime, which is invaluable for development and debugging. However, this same capability gives attackers detailed information about your service's API surface, making it easier to craft targeted attacks.

// Go - Conditional reflection (development only)
if os.Getenv("ENVIRONMENT") != "production" {
    reflection.Register(s)
}

Secure Service Discovery

Service discovery mechanisms require protection to prevent attackers from injecting malicious service endpoints or intercepting service information.

Consul with mTLS:

consulConfig := &api.Config{
    Address:    "consul.example.com:8500",
    Scheme:     "https",
    TLSConfig: &api.TLSConfig{
        CertFile: "/path/to/client.crt",
        KeyFile:  "/path/to/client.key",
        CAFile:   "/path/to/ca.crt",
    },
}

Kubernetes RBAC:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: grpc-service-discovery
rules:
- apiGroups: [""]
  resources: ["services", "endpoints"]
  verbs: ["get", "list", "watch"]

Use service mesh solutions like Istio or Linkerd for automatic mTLS and centralized security policies.

Monitoring and Incident Response

Implement Security Monitoring

Monitor gRPC services for security events and potential attacks.

Key metrics to monitor:

  • Request rates per method and client
  • Authentication and authorization failure rates
  • Error rates and types
  • Unusual traffic patterns

Set up alerts for:

  • High authentication failure rates
  • Attempts to access non-existent methods
  • Resource exhaustion patterns

Enable Distributed Tracing

Track requests across microservices for security analysis.

// Go - OpenTelemetry tracing with security context
tracer := otel.Tracer("grpc-service")
ctx, span := tracer.Start(ctx, "grpc.method.call")
defer span.End()

span.SetAttributes(
    attribute.String("grpc.method", info.FullMethod),
    attribute.String("client.ip", getClientIP(ctx)),
)

Testing and Validation

Perform gRPC Security Testing

Include gRPC-specific security tests in your development pipeline.

Test categories:

  • Authentication bypass attempts
  • Authorization boundary testing
  • Input validation and injection testing
  • Rate limiting effectiveness
  • Message size limit enforcement

Use tools like grpcurl and custom test clients to verify security controls.

# Test authentication requirement
grpcurl -plaintext localhost:50051 list
grpcurl -plaintext localhost:50051 myservice.MyService/GetUser

# Test with invalid tokens
grpcurl -plaintext -H "authorization: Bearer invalid_token" \
  localhost:50051 myservice.MyService/GetUser

Security Assessment Guidelines

  • Test all gRPC methods for proper authentication and authorization
  • Verify input validation on all message fields
  • Test rate limiting and resource exhaustion protections
  • Validate TLS configuration and certificate handling
  • Check for information disclosure in error messages

Language-Specific Considerations

Go

  • Use interceptors for cross-cutting security concerns
  • Leverage the context package for request-scoped security information
  • Explicitly configure TLS - Go's gRPC requires manual TLS setup

Java

  • Use Java's rich security ecosystem (Spring Security, etc.)
  • Configure Netty properly for TLS settings
  • Ensure ALPN support for HTTP/2

Python

  • Validate all inputs as Python's dynamic typing can hide type issues
  • Use secure credential management for certificate storage
  • Be aware of GIL limitations for high-concurrency scenarios

C# (.NET)

  • Leverage ASP.NET Core's built-in security features
  • Use the [Authorize] attribute on service methods
  • Configure HTTPS properly in production environments

References