A cloud database-as-a-service needs to expose each customer’s database instance through a unified API endpoint while enforcing authentication, authorization, and tenant isolation at the network boundary. The reverse proxy pattern solves this by sitting between the public internet and the internal database instances, handling API key validation, tenant identity injection, header sanitization, and request routing—all before the request touches the database engine.
The proxy routes requests matching /cloud/v1/{instance_id}/* to the correct backend instance. But it does far more than simple routing: it validates the API key against the control plane database, constructs a Principal object containing the tenant’s organization, permissions, and rate limits, strips any client-supplied internal headers (preventing header injection attacks), and injects the authenticated Principal as trusted headers that the backend instance can rely on without re-authenticating.
In a typical cloud database-as-a-service, this proxy is implemented as the single entry point for all cloud API traffic. It handles thousands of requests per second while adding sub-millisecond latency for the auth lookup (backed by an in-memory cache with TTL-based invalidation) and provides the tenant boundary enforcement that the entire security model depends on.
A Principal represents the authenticated identity making a request. It contains the organization ID, project scope, permission set, rate limit tier, and the API key fingerprint (for audit logging without storing the raw key). The proxy constructs the Principal from the API key lookup and serializes it into a trusted header (X-DB-Principal) that backend services consume. Backend services trust this header unconditionally because the proxy strips any client-supplied version of it before forwarding.
Clients can send arbitrary HTTP headers, including headers that the proxy uses internally for tenant context. If the proxy doesn’t strip these before forwarding, a malicious client can impersonate another tenant by setting X-DB-Principal: {"org_id": "victim-org"}. The proxy must remove all internal headers from the incoming request before adding its own authenticated versions. This is a critical security control—the header namespace starting with X-DB- is reserved for proxy-injected headers only.
The proxy extracts the instance_id from the URL path and resolves it to a backend address (IP:port or Kubernetes service DNS name). The routing table is cached in memory and refreshed from the control plane database on a timer and via change notifications. When an instance is provisioned, updated, or deprovisioned, the control plane emits a routing table invalidation event that the proxy consumes to update its cache within seconds.
Even if a client knows the instance ID of another tenant’s database, the proxy rejects the request because the API key’s organization doesn’t match the instance’s owning organization. This is the outermost layer of tenant isolation—the proxy enforces it before the request reaches the database. The check is simple: instance.org_id == principal.org_id. If it fails, the proxy returns 404 (not 403) to avoid leaking the existence of other tenants’ instances.
The proxy enforces per-tenant rate limits before forwarding requests. Rate limit configuration comes from the Principal’s tier (free: 10 QPS, pro: 1000 QPS, enterprise: 50000 QPS). The rate limiter uses a token bucket algorithm backed by a shared Redis instance so that rate limits are consistent across multiple proxy replicas. Exceeding the rate limit returns 429 Too Many Requests with a Retry-After header.
The proxy is built on Go’s net/http/httputil.ReverseProxy with custom Director and ModifyResponse functions. The request flow is: authenticate → strip headers → resolve instance → enforce tenant boundary → rate limit → inject principal → forward.
package proxy
import (
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"strings"
"time"
)
type Principal struct {
OrgID string `json:"org_id"`
ProjectID string `json:"project_id,omitempty"`
Permissions []string `json:"permissions"`
Tier string `json:"tier"`
KeyFingerprint string `json:"key_fingerprint"`
}
func (p *Principal) Encode() string {
b, _ := json.Marshal(p)
return string(b)
}
type APIKeyStore interface {
Lookup(ctx context.Context, key string) (*Principal, error)
}
type CachedKeyStore struct {
backing APIKeyStore
cache map[string]*cacheEntry
ttl time.Duration
}
type cacheEntry struct {
principal *Principal
expiresAt time.Time
}
func (s *CachedKeyStore) Lookup(ctx context.Context, key string) (*Principal, error) {
fingerprint := keyFingerprint(key)
if entry, ok := s.cache[fingerprint]; ok {
if time.Now().Before(entry.expiresAt) {
return entry.principal, nil
}
delete(s.cache, fingerprint)
}
principal, err := s.backing.Lookup(ctx, key)
if err != nil {
return nil, err
}
s.cache[fingerprint] = &cacheEntry{
principal: principal,
expiresAt: time.Now().Add(s.ttl),
}
return principal, nil
}
func keyFingerprint(key string) string {
h := sha256.Sum256([]byte(key))
return fmt.Sprintf("%x", h[:8])
}
func ExtractAPIKey(authHeader string) (string, error) {
if !strings.HasPrefix(authHeader, "Bearer ") {
return "", fmt.Errorf("missing Bearer prefix")
}
key := strings.TrimPrefix(authHeader, "Bearer ")
if len(key) < 32 {
return "", fmt.Errorf("invalid API key format")
}
return key, nil
}
package proxy
import (
"fmt"
"log"
"net/http"
"net/http/httputil"
"net/url"
"strings"
)
const internalHeaderPrefix = "X-DB-"
type CloudProxy struct {
KeyStore APIKeyStore
InstanceRouter InstanceRouter
RateLimiter RateLimiter
}
type InstanceRouter interface {
Resolve(instanceID string) (*InstanceBackend, error)
}
type InstanceBackend struct {
Address string
OrgID string
State string
}
type RateLimiter interface {
Allow(orgID string, tier string) bool
}
func (cp *CloudProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 1. Authenticate
apiKey, err := ExtractAPIKey(r.Header.Get("Authorization"))
if err != nil {
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
return
}
principal, err := cp.KeyStore.Lookup(r.Context(), apiKey)
if err != nil {
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
return
}
// 2. Extract instance ID from path: /cloud/v1/{instance_id}/...
instanceID, subPath, err := parseCloudPath(r.URL.Path)
if err != nil {
http.Error(w, `{"error":"not_found"}`, http.StatusNotFound)
return
}
// 3. Resolve instance backend
backend, err := cp.InstanceRouter.Resolve(instanceID)
if err != nil || backend.State != "ready" {
http.Error(w, `{"error":"not_found"}`, http.StatusNotFound)
return
}
// 4. Enforce tenant boundary (return 404, not 403)
if backend.OrgID != principal.OrgID {
http.Error(w, `{"error":"not_found"}`, http.StatusNotFound)
return
}
// 5. Rate limit
if !cp.RateLimiter.Allow(principal.OrgID, principal.Tier) {
w.Header().Set("Retry-After", "1")
http.Error(w, `{"error":"rate_limited"}`, http.StatusTooManyRequests)
return
}
// 6. Strip all internal headers from client request
for key := range r.Header {
if strings.HasPrefix(key, internalHeaderPrefix) {
r.Header.Del(key)
}
}
// 7. Inject authenticated principal
r.Header.Set("X-DB-Principal", principal.Encode())
r.Header.Set("X-DB-Instance-ID", instanceID)
// 8. Forward to backend
target, _ := url.Parse(fmt.Sprintf("http://%s", backend.Address))
proxy := &httputil.ReverseProxy{
Director: func(req *http.Request) {
req.URL.Scheme = target.Scheme
req.URL.Host = target.Host
req.URL.Path = subPath
req.Host = target.Host
},
}
proxy.ServeHTTP(w, r)
}
func parseCloudPath(path string) (instanceID, subPath string, err error) {
// Expected: /cloud/v1/{instance_id}/rest/of/path
parts := strings.SplitN(strings.TrimPrefix(path, "/"), "/", 4)
if len(parts) < 3 || parts[0] != "cloud" || parts[1] != "v1" {
return "", "", fmt.Errorf("invalid cloud path")
}
instanceID = parts[2]
if len(parts) == 4 {
subPath = "/" + parts[3]
} else {
subPath = "/"
}
return instanceID, subPath, nil
}
package proxy
import (
"sync"
"time"
)
type TokenBucketLimiter struct {
mu sync.Mutex
buckets map[string]*bucket
rates map[string]int // tier -> QPS
}
type bucket struct {
tokens float64
maxTokens float64
refillRate float64
lastRefill time.Time
}
func NewTokenBucketLimiter() *TokenBucketLimiter {
return &TokenBucketLimiter{
buckets: make(map[string]*bucket),
rates: map[string]int{
"free": 10,
"pro": 1000,
"enterprise": 50000,
},
}
}
func (l *TokenBucketLimiter) Allow(orgID, tier string) bool {
l.mu.Lock()
defer l.mu.Unlock()
b, ok := l.buckets[orgID]
if !ok {
rate := float64(l.rates[tier])
b = &bucket{
tokens: rate,
maxTokens: rate * 2,
refillRate: rate,
lastRefill: time.Now(),
}
l.buckets[orgID] = b
}
now := time.Now()
elapsed := now.Sub(b.lastRefill).Seconds()
b.tokens += elapsed * b.refillRate
if b.tokens > b.maxTokens {
b.tokens = b.maxTokens
}
b.lastRefill = now
if b.tokens < 1 {
return false
}
b.tokens--
return true
}
The cloud proxy is the public-facing entry point for all cloud database API traffic. Every query, insert, and schema operation goes through it. The proxy handles authentication once at the edge so that backend database instances don’t need to implement their own API key validation—they simply trust the X-DB-Principal header. This separation of concerns means the database engine can focus on query execution while the proxy handles cross-cutting concerns like auth, rate limiting, and request logging.
The proxy also serves as the natural point for request-level observability. Every request is logged with the tenant org ID, instance ID, latency, status code, and request size. These structured logs feed into the monitoring system, enabling per-tenant dashboards, alerting on error rate spikes for individual customers, and usage analytics that drive the billing system.
During rolling updates of backend instances, the proxy provides a stable endpoint. When an instance transitions to “updating” state, the proxy can either queue requests briefly (for sub-second updates) or return 503 with a Retry-After header. The client SDK handles retries transparently, so end users experience brief latency spikes rather than errors during upgrades.
The proxy strips all X-DB-* headers from incoming requests before injecting authenticated values. This is non-negotiable: any header the backend trusts must be proxy-injected, never client-supplied. A common mistake is only stripping known internal headers (allowlist approach)—if a new internal header is added and the strip list isn’t updated, clients can inject it. The prefix-based blocklist approach (strip everything starting with X-DB-) is safer because new headers are automatically protected.
When a request targets an instance owned by a different tenant, the proxy returns 404 Not Found, not 403 Forbidden. Returning 403 leaks information: the attacker learns that the instance exists but they don’t have access, which is useful for enumeration attacks. 404 reveals nothing—the instance might not exist at all. This is a standard practice in multi-tenant APIs (GitHub uses this pattern for private repositories).
API key lookups are cached with a short TTL (30–60 seconds) to avoid hitting the database on every request. But when a key is revoked, the system must invalidate the cache immediately—a 60-second window where a revoked key still works is a security risk. The solution is a pub/sub invalidation channel: when the control plane revokes a key, it publishes an invalidation event that all proxy replicas consume, evicting the key from their local caches within milliseconds.
X-DB-Principal without the proxy stripping client-supplied versions, any client can impersonate any tenant. Header stripping is a security-critical path.