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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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