Skip to content

🌐 API Gateway: Envoy + gRPC-Gateway with Authentication #28

Description

@VAIBHAVSING

Overview

Implement a production-grade API Gateway using Envoy Proxy and gRPC-Gateway to provide unified REST/gRPC access, authentication, rate limiting, and observability for the cloud IDE platform.

🎯 Architecture Goals

  • Unified API: Single entry point for REST and gRPC clients
  • Authentication: JWT validation and user context injection
  • Rate Limiting: Per-user and global rate limiting
  • Observability: Request tracing, metrics, and logging
  • Security: TLS termination and request validation

🏗️ Gateway Architecture

Internet → Envoy Proxy → gRPC-Gateway → Go gRPC Services
           ↓
       Auth, Rate Limiting,
       Metrics, Logging

📋 Implementation Tasks

Envoy Proxy Configuration

  • Core Envoy Setup (gateway/envoy.yaml)

    • HTTP/HTTPS listeners
    • gRPC-Gateway routing
    • TLS termination
    • Health check endpoints
  • Authentication Filter (gateway/filters/auth.lua)

    • JWT token validation
    • User context extraction
    • Request enrichment with user info
    • Integration with NextAuth.js tokens
  • Rate Limiting Configuration

    • Redis-based rate limiting
    • Per-user limits (100 req/min)
    • Global limits (10k req/min)
    • Burst handling
  • Observability Filters

    • Access logging with structured format
    • Metrics collection (Prometheus)
    • Distributed tracing (OpenTelemetry)
    • Request ID generation

gRPC-Gateway Setup

  • Gateway Server (gateway/grpc-gateway/main.go)

    • gRPC to REST translation
    • Custom marshaling options
    • Error handling middleware
    • CORS configuration
  • OpenAPI Serving (gateway/swagger/)

    • Swagger UI hosting
    • OpenAPI spec serving
    • API documentation portal
    • Interactive testing interface

Configuration Files

Envoy Configuration

# gateway/envoy.yaml
static_resources:
  listeners:
  - name: listener_0
    address:
      socket_address:
        protocol: TCP
        address: 0.0.0.0
        port_value: 8080
    filter_chains:
    - filters:
      - name: envoy.filters.network.http_connection_manager
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
          stat_prefix: ingress_http
          access_log:
          - name: envoy.access_loggers.stdout
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog
              log_format:
                json_format:
                  timestamp: "%START_TIME%"
                  method: "%REQ(:METHOD)%"
                  path: "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%"
                  status: "%RESPONSE_CODE%"
                  duration: "%DURATION%"
                  user_id: "%REQ(X-USER-ID)%"
                  request_id: "%REQ(X-REQUEST-ID)%"
          http_filters:
          # Authentication filter
          - name: envoy.filters.http.lua
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
              inline_code: |
                function envoy_on_request(request_handle)
                  local auth_header = request_handle:headers():get("authorization")
                  if auth_header == nil then
                    request_handle:respond({":status"}, "401", {["content-type"] = "application/json"}, '{"error":"missing authorization header"}')
                    return
                  end
                  
                  -- Extract JWT token and validate
                  local token = string.gsub(auth_header, "Bearer ", "")
                  -- Add JWT validation logic here
                  
                  -- Set user context for downstream services
                  request_handle:headers():add("x-user-id", user_id)
                  request_handle:headers():add("x-user-email", user_email)
                end
          
          # Rate limiting filter
          - name: envoy.filters.http.local_ratelimit
            typed_config:
              "@type": type.googleapis.com/udpa.type.v1.TypedStruct
              type_url: type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit
              value:
                stat_prefix: http_local_rate_limiter
                token_bucket:
                  max_tokens: 100
                  tokens_per_fill: 100
                  fill_interval: 60s
                filter_enabled:
                  runtime_key: test_enabled
                  default_value:
                    numerator: 100
                    denominator: HUNDRED
                filter_enforced:
                  runtime_key: test_enforced
                  default_value:
                    numerator: 100
                    denominator: HUNDRED
          
          # Router filter
          - name: envoy.filters.http.router
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
          
          route_config:
            name: local_route
            virtual_hosts:
            - name: local_service
              domains: ["*"]
              routes:
              # Health check endpoint
              - match:
                  path: "/health"
                direct_response:
                  status: 200
                  body:
                    inline_string: "OK"
              
              # Swagger UI
              - match:
                  prefix: "/docs"
                route:
                  cluster: swagger_ui
              
              # API v1 routes to gRPC-Gateway
              - match:
                  prefix: "/api/v1"
                route:
                  cluster: grpc_gateway
                  timeout: 30s
              
              # gRPC routes (direct)
              - match:
                  prefix: "/"
                  headers:
                  - name: content-type
                    string_match:
                      prefix: "application/grpc"
                route:
                  cluster: grpc_backend
                  timeout: 30s

  clusters:
  - name: grpc_gateway
    connect_timeout: 5s
    type: LOGICAL_DNS
    lb_policy: ROUND_ROBIN
    load_assignment:
      cluster_name: grpc_gateway
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: grpc-gateway
                port_value: 8081
  
  - name: grpc_backend
    connect_timeout: 5s
    type: LOGICAL_DNS
    lb_policy: ROUND_ROBIN
    http2_protocol_options: {}
    load_assignment:
      cluster_name: grpc_backend
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: grpc-server
                port_value: 8080
  
  - name: swagger_ui
    connect_timeout: 5s
    type: LOGICAL_DNS
    lb_policy: ROUND_ROBIN
    load_assignment:
      cluster_name: swagger_ui
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: swagger-ui
                port_value: 8080

admin:
  address:
    socket_address:
      protocol: TCP
      address: 0.0.0.0
      port_value: 9901

gRPC-Gateway Implementation

// gateway/grpc-gateway/main.go
package main

import (
    "context"
    "flag"
    "fmt"
    "net/http"
    
    "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
    
    environmentv1 "github.com/VAIBHAVSING/Dev8.dev/api/grpc/go/environment/v1"
    azurev1 "github.com/VAIBHAVSING/Dev8.dev/api/grpc/go/azure/v1"
)

func main() {
    var (
        grpcServerEndpoint = flag.String("grpc-server-endpoint", "localhost:8080", "gRPC server endpoint")
        httpPort          = flag.String("http-port", "8081", "HTTP port for REST API")
    )
    flag.Parse()

    ctx := context.Background()
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    // Register gRPC server endpoint
    mux := runtime.NewServeMux(
        runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.JSONPb{
            MarshalOptions: protojson.MarshalOptions{
                UseProtoNames:   true,
                EmitUnpopulated: true,
            },
            UnmarshalOptions: protojson.UnmarshalOptions{
                DiscardUnknown: true,
            },
        }),
        runtime.WithErrorHandler(customErrorHandler),
        runtime.WithMetadata(annotator),
    )

    opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
    
    // Register Environment Service
    err := environmentv1.RegisterEnvironmentServiceHandlerFromEndpoint(ctx, mux, *grpcServerEndpoint, opts)
    if err != nil {
        panic(err)
    }
    
    // Register Azure Service
    err = azurev1.RegisterAzureProvisionerServiceHandlerFromEndpoint(ctx, mux, *grpcServerEndpoint, opts)
    if err != nil {
        panic(err)
    }

    // CORS middleware
    corsHandler := func(h http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            w.Header().Set("Access-Control-Allow-Origin", "*")
            w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
            w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
            
            if r.Method == "OPTIONS" {
                w.WriteHeader(http.StatusOK)
                return
            }
            
            h.ServeHTTP(w, r)
        })
    }

    // Start HTTP server
    fmt.Printf("Starting gRPC-Gateway on port %s\n", *httpPort)
    if err := http.ListenAndServe(":"+*httpPort, corsHandler(mux)); err != nil {
        panic(err)
    }
}

func customErrorHandler(ctx context.Context, mux *runtime.ServeMux, marshaler runtime.Marshaler, w http.ResponseWriter, r *http.Request, err error) {
    // Custom error response formatting
    w.Header().Set("Content-Type", "application/json")
    
    status := runtime.HTTPStatusFromCode(grpc.Code(err))
    w.WriteHeader(status)
    
    errorResponse := map[string]interface{}{
        "error": err.Error(),
        "code":  grpc.Code(err).String(),
    }
    
    json.NewEncoder(w).Encode(errorResponse)
}

func annotator(ctx context.Context, req *http.Request) metadata.MD {
    // Extract headers and add to gRPC metadata
    md := metadata.New(nil)
    
    if userID := req.Header.Get("X-User-ID"); userID != "" {
        md.Append("user-id", userID)
    }
    
    if requestID := req.Header.Get("X-Request-ID"); requestID != "" {
        md.Append("request-id", requestID)
    }
    
    return md
}

Docker Compose Setup

# gateway/docker-compose.yaml
version: '3.8'

services:
  envoy:
    image: envoyproxy/envoy:v1.28.0
    ports:
      - "8080:8080"
      - "9901:9901"
    volumes:
      - ./envoy.yaml:/etc/envoy/envoy.yaml:ro
    depends_on:
      - grpc-gateway
      - grpc-server

  grpc-gateway:
    build: ./grpc-gateway
    ports:
      - "8081:8081"
    environment:
      - GRPC_SERVER_ENDPOINT=grpc-server:8080
    depends_on:
      - grpc-server

  grpc-server:
    build: ../../apps/agent
    ports:
      - "8082:8080"
    environment:
      - DATABASE_URL=postgres://user:pass@postgres:5432/dev8
      - AZURE_SUBSCRIPTION_ID=
      - AZURE_CLIENT_ID=
      - AZURE_CLIENT_SECRET=
      - AZURE_TENANT_ID=

  swagger-ui:
    image: swaggerapi/swagger-ui:latest
    ports:
      - "8083:8080"
    environment:
      - SWAGGER_JSON=/openapi/environments.yaml
    volumes:
      - ../openapi:/openapi:ro

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    command: redis-server --appendonly yes

Authentication Integration

// gateway/auth/jwt.go
package auth

import (
    "crypto/rsa"
    "fmt"
    "github.com/golang-jwt/jwt/v5"
)

type JWTValidator struct {
    publicKey *rsa.PublicKey
}

func NewJWTValidator(publicKeyPath string) (*JWTValidator, error) {
    // Load NextAuth.js public key for JWT validation
    keyData, err := os.ReadFile(publicKeyPath)
    if err != nil {
        return nil, err
    }
    
    publicKey, err := jwt.ParseRSAPublicKeyFromPEM(keyData)
    if err != nil {
        return nil, err
    }
    
    return &JWTValidator{publicKey: publicKey}, nil
}

func (v *JWTValidator) ValidateToken(tokenString string) (*jwt.Token, error) {
    return jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
            return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
        }
        return v.publicKey, nil
    })
}

🔧 Monitoring & Observability

Prometheus Metrics

  • Request rate and latency metrics
  • Error rate tracking
  • Authentication success/failure rates
  • Rate limiting hit rates
  • Upstream service health

Distributed Tracing

  • OpenTelemetry integration
  • Request correlation across services
  • Performance bottleneck identification
  • Error tracking and debugging

Logging

  • Structured access logs
  • Error logging with context
  • Security event logging
  • Performance monitoring logs

🔗 Integration Points

⏱️ Time Estimate

6-8 hours - Gateway setup with authentication and observability

🎯 Success Criteria

  • Envoy Proxy routes requests correctly
  • gRPC-Gateway provides REST API compatibility
  • JWT authentication validates NextAuth.js tokens
  • Rate limiting prevents abuse
  • Swagger UI serves API documentation
  • Metrics and logging work correctly
  • CORS configuration allows frontend access
  • Health checks report service status

🚀 Priority

HIGH - Essential for API access and security

📝 Notes

  • Use Envoy's built-in filters for maximum performance
  • Implement proper error handling and user feedback
  • Configure rate limiting to prevent abuse
  • Add comprehensive monitoring for production readiness
  • Design for horizontal scaling with load balancing

Metadata

Metadata

Assignees

No one assigned

    Labels

    apiAPI endpoint developmentbackendBackend/API related taskscoreCore functionality and infrastructureenhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions