Skip to content

Proxies, Ingress, and Egress Control

Prerequisites: Load Balancing, Kubernetes

← Networking Overview


Why This Exists

Forward proxy: Client intentionally uses a proxy to hide their identity or route through a gateway.
Reverse proxy: Backend hides behind a proxy; client doesn't know it exists.
Ingress: How traffic enters a Kubernetes cluster.
Egress: How traffic leaves a cluster.

Get these confused and you design architecture that cannot actually enforce policy.


Forward Proxy vs. Reverse Proxy

Forward Proxy (Client's Perspective)

Client → Forward Proxy → Internet
         (explicitly configured)

Use cases: - Corporate firewalls: Employees' laptops must go through a corporate proxy to access the internet - Anonymization: Hide client IP from destination (Tor, VPN) - Caching: Proxy caches popular responses (CDN for internal networks) - Filtering: Block malicious domains

Client sees:

# Explicitly set proxy
requests.get("https://example.com", proxies={"https": "http://corporate-proxy:8080"})

# Or environment variable
export HTTPS_PROXY=http://corporate-proxy:8080

Destination sees: Proxy's IP, not client's IP.

Reverse Proxy (Backend's Perspective)

Internet → Reverse Proxy → Backend servers
           (transparent to client)

Use cases: - Load balancing: Distribute traffic across backends - TLS termination: Proxy handles HTTPS; backend is HTTP - Request routing: Route /api to API servers, /media to CDN - Caching: Cache backend responses before sending to clients - WAF (Web Application Firewall): Block malicious requests

Client sees: Only the proxy's IP.

Backend never sees: The client's IP (without a header like X-Forwarded-For).

Visual Comparison

graph LR
    subgraph Forward["Forward Proxy"]
        Client["Client<br/>(configured)"]
        FP["Forward Proxy<br/>transparent to dest"]
        Internet["Internet<br/>Destination"]
        Client -->|intentionally| FP -->|hides client| Internet
    end

    subgraph Reverse["Reverse Proxy"]
        Internet2["Internet<br/>Client"]
        RP["Reverse Proxy<br/>transparent to client"]
        Backends["Backends<br/>(hidden)"]
        Internet2 -->|thinks it's the backend| RP -->|routes to| Backends
    end

Reverse Proxy Deep Dive

A reverse proxy sits between clients and backends, inspecting and modifying requests.

What a Reverse Proxy Can Do

# 1. TLS termination
# Client sends HTTPS to proxy
# Proxy decrypts, inspects, re-encrypts to backend (or sends plain HTTP)

# 2. Request routing (L7)
# GET /api/users → api-servers:8080
# GET /media/image.jpg → cdn-servers:8080
# POST /admin → admin-servers:8080

# 3. Request modification
# Add headers: X-Forwarded-For (client IP), X-Real-IP, X-Request-ID
# Remove headers: Server, X-Internal-ID
# Rewrite path: /api/v1/users → /users (backend is v1 only)

# 4. Response caching
# Cache GET /static/app.js for 1 hour
# Don't cache POST requests

# 5. Rate limiting
# Max 1000 requests/sec per IP

# 6. Access control (basic auth, IP whitelist)
# Only allow 10.0.0.0/8 to access /admin

# 7. Compression
# Compress responses with gzip if client accepts it

# 8. Health checks
# Periodically GET /health from each backend
# Stop routing if a backend fails checks
Proxy Use Case Example
nginx General-purpose, reverse proxy, load balancer Most common; can handle 100k+ RPS on modern hardware
HAProxy High-performance load balancing Often paired with Keepalived for HA
Envoy Service mesh sidecar, microservices proxy Handles retries, timeouts, mTLS at the proxy level
Traefik Kubernetes-native reverse proxy Automatically discovers services from K8s annotations
AWS ALB Cloud-native reverse proxy Managed; integrates with ECS/EKS
NGINX Plus Commercial nginx Adds dynamic upstream configuration (reload without restart)

Real Example: nginx Reverse Proxy Config

# Upstream backend pool
upstream api_backend {
    # Load balancing algorithm (default: round-robin)
    least_conn;  # or ip_hash, least_conn, etc.

    server api-1.internal:8080 max_fails=3 fail_timeout=30s;
    server api-2.internal:8080 max_fails=3 fail_timeout=30s;
    server api-3.internal:8080 max_fails=3 fail_timeout=30s;
}

# Cache configuration (http{} context, same as limit_req_zone)
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=api_cache:10m;

# limit_req_zone MUST be in http{}, not server{}
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/s;

server {
    listen 443 ssl http2;
    server_name api.example.com;

    # TLS termination
    ssl_certificate /etc/ssl/cert.pem;
    ssl_certificate_key /etc/ssl/key.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    # limit_req is allowed in server/location; the zone is not
    limit_req zone=api_limit burst=200;

    # Proxy configuration
    location /api/ {
        # Add headers so backend knows client IP
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;

        # Proxy to backend pool
        proxy_pass http://api_backend;

        # Timeouts (critical!)
        proxy_connect_timeout 5s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;

        # Caching
        proxy_cache api_cache;
        proxy_cache_valid 200 302 10m;
        proxy_cache_methods GET HEAD;
        proxy_cache_bypass $http_pragma $http_authorization;
    }

    # Health check endpoint
    location /health {
        access_log off;
        return 200 "ok";
    }
}

Interview question: "What happens if you set proxy_read_timeout 30s but the backend takes 60s to respond?"

Answer: "nginx waits 30s, then closes with 504. The backend may keep working on a request nobody will see. That is a request-deadline bug: the inner timeout must be shorter than the caller (client > LB > app > dependency, remaining budget). Do not mix this with idle timeouts, where the LB should close idle connections before the app so the app never thinks a dead socket is still live."


Kubernetes Ingress and Ingress Controllers

The Problem Ingress Solves

Without Ingress:

User → AWS ALB (expensive, one per service)
       └→ Service → Deployment
       └→ Service → Deployment
       └→ Service → Deployment

Every service needs its own load balancer = expensive + complex.

With Ingress:

User → Ingress Controller (nginx, Traefik, AWS ALB)
       └→ Ingress objects (YAML, routing rules)
           └→ Route /api → api-service
           └→ Route /web → web-service
           └→ Route /admin → admin-service

How Ingress Works

# Ingress object: declare routing rules
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: shop-ingress
spec:
  ingressClassName: nginx  # Use the nginx controller
  tls:
    - hosts:
        - api.shop.com
      secretName: api-shop-cert
  rules:
    - host: api.shop.com
      http:
        paths:
          - path: /api/v1
            pathType: Prefix
            backend:
              service:
                name: api-service
                port:
                  number: 8080
          - path: /health
            pathType: Exact
            backend:
              service:
                name: api-service
                port:
                  number: 8080
    - host: web.shop.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web-service
                port:
                  number: 3000

Ingress Controller: The Implementation

The Ingress object is just YAML. The controller watches Ingress objects and configures a real reverse proxy.

graph LR
    I["Ingress YAML<br/>(shop-ingress)"]
    C["Ingress Controller<br/>(nginx, Traefik, ALB)"]
    NX["nginx.conf<br/>(or ALB rules)"]
    LB["Real LB"]

    C -->|watches| I
    C -->|generates| NX
    C -->|configures| LB
    LB -->|routes traffic| Backend["Backends"]
Controller Type Pros Cons
nginx-ingress Self-hosted Simple, free, widely used You manage the nginx pods
Traefik Self-hosted Dynamic, supports multiple backends More complex than nginx
AWS ALB Ingress Managed AWS integrates with security groups, IAM Expensive; vendor lock-in
GCP Cloud Load Balancer Managed Managed; scales automatically Expensive
Cert-Manager Not really a controller, but complementary Auto-renews TLS certs via Let's Encrypt One more thing to manage

What Can Go Wrong

Problem 1: Ingress points to Service that doesn't exist

backend:
  service:
    name: api-service-typo  # ← This service doesn't exist
    port:
      number: 8080
# Result: User gets 502 or 503

Problem 2: Service selector doesn't match any Pods

# Service has no endpoints
spec:
  selector:
    app: api  # ← No pods with this label
  ports:
    - port: 80
      targetPort: 8080
# Result: User gets 502 or 503

Problem 3: Ingress controller wasn't installed

You write Ingress YAML.
You expect a reverse proxy to appear.
Nothing happens (because no Ingress controller is running).
User sees "connection refused."


Egress: How Traffic Leaves the Cluster

The Problem

By default, all pods can connect to anywhere outside the cluster. An attacker inside the cluster can: - Exfiltrate data to external servers - Download malware - Connect to C&C servers

Solution: Egress control (restrict outbound traffic).

Methods

1. Network Policy (K8s)

# Block all egress except DNS and specific services
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: checkout-egress
spec:
  podSelector:
    matchLabels:
      app: checkout
  policyTypes:
    - Egress
  egress:
    # Allow DNS (required for service discovery)
    - to:
        - namespaceSelector: {}
      ports:
        - protocol: UDP
          port: 53
    # Allow egress to payment service
    - to:
        - podSelector:
            matchLabels:
              app: payment-service
      ports:
        - protocol: TCP
          port: 8080
    # Allow HTTPS to external services
    - to:
        - podSelector: {}  # Any pod
      ports:
        - protocol: TCP
          port: 443

2. Egress Gateway (Istio, Cilium)

# There is no kind: EgressGateway. Use Gateway (selector istio: egressgateway)
# + VirtualService + ServiceEntry.
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: istio-egressgateway
spec:
  selector:
    istio: egressgateway
  servers:
    - port:
        number: 443
        name: https
        protocol: HTTPS
      hosts:
        - api.example.com
---
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: api-example
spec:
  hosts:
    - api.example.com
  ports:
    - number: 443
      name: https
      protocol: HTTPS
  location: MESH_EXTERNAL
  resolution: DNS
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: external-api
spec:
  hosts:
    - api.example.com
  gateways:
    - mesh
    - istio-egressgateway
  http:
    - match:
        - gateways: [mesh]
      route:
        - destination:
            host: istio-egressgateway.istio-system.svc.cluster.local
            port:
              number: 443
    - match:
        - gateways: [istio-egressgateway]
      route:
        - destination:
            host: api.example.com
            port:
              number: 443

3. Firewall Rules (Cloud Provider)

# AWS Security Group outbound rules
Rule 1: Allow HTTPS to 1.2.3.4 (payment provider)
Rule 2: Allow HTTP to 8.8.8.8 (DNS)
Rule 3: Deny all other egress

AAA (Authentication, Authorization, Accounting) in Proxy Context

AAA is a framework for access control:

  • Authentication (AuthN): Who are you?
  • Authorization (AuthZ): What are you allowed to do?
  • Accounting: Log what you did (audit trail)

Where AAA Fits

A reverse proxy can enforce AAA before requests reach backends:

Client request
Proxy: Who is this? (Authentication)
    ├─ Check API key, JWT, mTLS certificate
    ├─ Reject if invalid
Proxy: Are they allowed? (Authorization)
    ├─ Check roles/permissions
    ├─ Reject if unauthorized
Backend (trusted)
Proxy + Accounting log
    ├─ Log: user_id, endpoint, timestamp, status

Example: API Key Authentication at Proxy

# nginx authenticating with API keys
upstream backend {
    server backend:8080;
}

server {
    listen 443 ssl;

    location /api/ {
        # Check API key in Authorization header
        if ($http_authorization !~ ^Bearer\ .+$) {
            return 401 "Missing or invalid Authorization header";
        }

        # Extract key and validate (in production, check against a service)
        set $api_key $http_authorization;
        # $api_key is now "Bearer xyz123"

        # Log the access (accounting)
        access_log /var/log/nginx/api_access.log 
                   '$remote_addr - $api_key - $request - $status';

        # Proxy to backend
        proxy_pass http://backend;
    }
}

Example: mTLS at Proxy (Authorization)

Client with valid cert
Proxy: Do I trust this certificate? (AuthN)
    ├─ Check client cert is signed by our CA
    ├─ Check cert hasn't expired
    ├─ Reject if invalid
Proxy: What can this client do? (AuthZ)
    ├─ Extract CN or SAN from cert
    ├─ Map to role (e.g., CN="payment-service" → role="payment")
    ├─ Check role has permission for this endpoint
Backend

Real-World Case: Payment API Gateway

Requirements: - External clients call api.shop.com/pay - Only authorized partners can call (API key + IP whitelist) - All requests are logged (audit trail) - Rate-limit to 100 RPS per partner - TLS termination at gateway

Architecture:

Partner
    ↓ HTTPS
Reverse Proxy (nginx)
    ├─ TLS termination
    ├─ Extract API key from header
    ├─ Check rate limit
    ├─ Check IP whitelist
    ├─ Log request (accounting)
Payment Service (internal, HTTP only)
    └─ Trusts proxy did the auth

Config:

# This file is included inside http { }. limit_req_zone is invalid in server {}.
upstream payment_api {
    server payment-service:8080;
}

limit_req_zone $http_authorization zone=api_limit:10m rate=100r/s;

server {
    listen 443 ssl;
    server_name api.shop.com;
    ssl_certificate /etc/ssl/cert.pem;
    ssl_certificate_key /etc/ssl/key.pem;

    # Structured logging for audit
    log_format payment '$remote_addr - $http_authorization - $request - $status - $response_time';
    access_log /var/log/nginx/payment.log payment;

    location /pay {
        # Authentication: check API key
        if ($http_authorization !~ ^Bearer\ [a-z0-9]+$) {
            return 401 "Invalid API key";
        }

        # Authorization: check IP whitelist (mock)
        # In production, this would query a database
        set $ip_allowed "0";
        if ($remote_addr ~ ^203\.0\.113\.) {  # Partner's CIDR
            set $ip_allowed "1";
        }
        if ($ip_allowed = "0") {
            return 403 "IP not whitelisted";
        }

        # Rate limiting
        limit_req zone=api_limit burst=10 nodelay;

        # Add metadata headers for backend
        proxy_set_header X-API-Key $http_authorization;
        proxy_set_header X-Client-IP $remote_addr;

        # Proxy to backend (backend trusts we did auth)
        proxy_pass http://payment_api;
    }
}

Interview Questions

Q: What's the difference between a forward proxy and a reverse proxy?

"A forward proxy is configured by the client and hides the client's IP from the destination (like a corporate proxy or VPN). A reverse proxy sits between clients and backends, and the client doesn't know it's there — it hides the backend's IP from the client. Forward proxy: client sees it. Reverse proxy: backend sees it."

Q: You set up an Ingress object pointing to a Service, but users get 502. How do you debug?

"First, I'd check if the Ingress controller is running (kubectl get pods -l app=ingress-nginx). Then I'd check if the Ingress was reconciled (look at kubectl describe ingress shop-ingress for any errors). Then I'd verify the Service exists and has endpoints: kubectl get svc service-name and kubectl get endpoints service-name. If Endpoints is empty, the Service selector doesn't match any pods — I'd check pod labels. If the Ingress and Service are fine, I'd check the controller's logs to see what it generated (usually stored in a ConfigMap or visible in the controller logs). Finally, I'd curl the Ingress IP directly to see if it's a DNS or proxy issue."

Q: Design an egress control system for a microservices cluster where services should only talk to approved external APIs.

"I'd start with Kubernetes NetworkPolicy for deny-all egress, then allowlist specific external IPs/domains. But NetworkPolicy is IP-based, which breaks if a domain changes IPs. For production, I'd use an egress gateway: all external traffic goes through a centralized pod that checks a policy database (allowed destinations per service). The gateway logs every external call (accounting), and I can easily update policies without redeploying. In Istio that is a Gateway with selector: istio: egressgateway, plus VirtualService and ServiceEntry — there is no kind: EgressGateway. NetworkPolicy alone is brittle if you care about domains, not IPs."


Key Takeaways

Remember

  1. Forward proxy hides client; reverse proxy hides backend
  2. Reverse proxy can do: TLS termination, routing, caching, rate limiting, auth
  3. Ingress is K8s YAML; Ingress controller is the reverse proxy that implements it
  4. Egress control requires NetworkPolicy + egress gateway for production
  5. AAA at the proxy means auth happens before traffic reaches backends (backends can be less trusted)
  6. X-Forwarded-For header preserves client IP through proxies (but can be spoofed; verify at the edge)
  7. Request deadlines: inner shorter than caller (client > LB > app > dependency). Idle: LB closes idle conns before the app. Do not mix the two.
  8. Ingress pointing to nonexistent Service = 502 (debug with kubectl get endpoints)

Previous: Load Balancing