Documentation
¶
Overview ¶
Package ingress provides HTTP/HTTPS reverse proxy and ingress controller for Warren clusters.
This package implements a full-featured ingress controller that routes external HTTP/HTTPS traffic to services running in the cluster. It includes host and path-based routing, TLS termination with Let's Encrypt ACME support, load balancing across service replicas, and advanced middleware for rate limiting, access control, and header manipulation.
Architecture ¶
Warren's ingress controller consists of five main components working together:
┌─────────────────────────────────────────────────────────────┐
│ Ingress Controller Architecture │
└─────┬───────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ HTTP Proxy │
│ • Listens on :8000 (HTTP) and :8443 (HTTPS) │
│ • TLS termination │
│ • Request routing │
└────────┬─────────────────────────────────────────────────────┘
│
┌────┴────┬──────────┬───────────┬─────────────┐
▼ ▼ ▼ ▼ ▼
┌────────┐┌──────┐┌────────────┐┌────────┐┌────────────┐
│ Router ││ Load ││ Middleware ││ ACME ││ Proxy │
│ ││Balancer│ ││ ││ Backend │
└────────┘└──────┘└────────────┘└────────┘└────────────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
Match Select Rate Limit Let's Forward to
rules backend Access Ctrl Encrypt container
## Request Flow
- Client request → :8000 (HTTP) or :8443 (HTTPS)
- TLS termination (if HTTPS)
- Router matches host + path → IngressPath
- Middleware applies access control, rate limiting
- LoadBalancer selects healthy backend task
- Proxy forwards request to backend
- Response returned to client
Note: Warren uses "task" to refer to running containers internally. DNS uses "instance" for compatibility (e.g., nginx-1.nginx.warren), but the ingress controller routes to "tasks".
Core Components ¶
## Proxy
The Proxy is the main ingress server that coordinates all operations:
proxy := NewProxy(store, managerAddr, grpcClient) err := proxy.Start(ctx) // Starts HTTP and HTTPS servers
The proxy handles:
- HTTP server on port 8000
- HTTPS server on port 8443 (if certificates available)
- TLS configuration and certificate management
- ACME challenge handling for Let's Encrypt
## Router
The Router implements host and path-based routing:
Router matches requests to backends: ├── Host matching: exact or wildcard (*.example.com) ├── Path matching: exact or prefix └── Longest path match wins
Example routing rules:
api.example.com/v1/* → api-service:8080 api.example.com/v2/* → api-v2-service:8080 app.example.com/* → app-service:3000 *.example.com/* → default-service:8080
## LoadBalancer
The LoadBalancer selects backend tasks using round-robin:
Service: api (3 replicas/tasks) ├── Task 1: 192.168.1.10:8080 (healthy) ├── Task 2: 192.168.1.11:8080 (healthy) └── Task 3: 192.168.1.12:8080 (unhealthy - excluded) Request 1 → 192.168.1.10:8080 Request 2 → 192.168.1.11:8080 Request 3 → 192.168.1.10:8080 (wraps around)
Health-aware selection:
- Only routes to healthy tasks
- Automatically excludes failed health checks
- Updates as tasks come and go
## Middleware
The Middleware applies request transformations and policies:
- Access Control: IP whitelist/blacklist
- Rate Limiting: Requests per second per IP
- Header Manipulation: Add/set/remove headers
- Path Rewriting: Strip prefix or replace path
- Proxy Headers: X-Forwarded-For, X-Real-IP, etc.
## ACME Client
The ACME client integrates with Let's Encrypt for automatic HTTPS:
- User creates Ingress with TLS enabled
- ACME client requests certificate from Let's Encrypt
- HTTP-01 challenge: Proxy serves challenge at /.well-known/acme-challenge/
- Let's Encrypt verifies challenge
- Certificate issued and stored
- Auto-renewal 30 days before expiry
Ingress Rules ¶
## Ingress Structure
Ingress {
Name: "myapp-ingress"
Rules: [
{
Host: "api.example.com"
Paths: [
{
Path: "/v1"
PathType: "Prefix"
Backend: {
ServiceName: "api-v1"
Port: 8080
}
RateLimit: {
RequestsPerSecond: 100
Burst: 20
}
AccessControl: {
AllowedIPs: ["10.0.0.0/8"]
}
}
]
}
]
TLS: {
Hosts: ["api.example.com"]
SecretName: "api-tls"
AutoSSL: true // Enable Let's Encrypt
}
}
## Host Matching
Exact match:
Pattern: api.example.com Matches: api.example.com No match: app.example.com, sub.api.example.com
Wildcard match:
Pattern: *.example.com Matches: api.example.com, app.example.com, anything.example.com No match: example.com, sub.api.example.com
Catch-all:
Pattern: "" (empty) Matches: All hosts
## Path Matching
Exact match:
PathType: "Exact" Pattern: /api/v1 Matches: /api/v1 No match: /api/v1/, /api/v1/users
Prefix match:
PathType: "Prefix" Pattern: /api Matches: /api, /api/, /api/v1, /api/v1/users No match: /apiv1, /v1/api
Usage Examples ¶
## Creating a Basic Ingress
import "github.com/cuemby/warren/pkg/types"
ingress := &types.Ingress{
ID: uuid.New().String(),
Name: "myapp-ingress",
Rules: []*types.IngressRule{
{
Host: "myapp.example.com",
Paths: []*types.IngressPath{
{
Path: "/",
PathType: types.PathTypePrefix,
Backend: &types.IngressBackend{
ServiceName: "myapp",
Port: 8080,
},
},
},
},
},
CreatedAt: time.Now(),
}
// Store ingress (manager will pick it up)
err := store.CreateIngress(ingress)
// Now: http://myapp.example.com/ → myapp:8080
## Ingress with Multiple Services
ingress := &types.Ingress{
Name: "multi-service",
Rules: []*types.IngressRule{
{
Host: "example.com",
Paths: []*types.IngressPath{
{
Path: "/api",
PathType: types.PathTypePrefix,
Backend: &types.IngressBackend{
ServiceName: "api",
Port: 8080,
},
},
{
Path: "/app",
PathType: types.PathTypePrefix,
Backend: &types.IngressBackend{
ServiceName: "frontend",
Port: 3000,
},
},
{
Path: "/",
PathType: types.PathTypePrefix,
Backend: &types.IngressBackend{
ServiceName: "landing",
Port: 8080,
},
},
},
},
},
}
// Routing:
// example.com/api/users → api:8080/api/users
// example.com/app/ → frontend:3000/app/
// example.com/ → landing:8080/
## Ingress with TLS (Let's Encrypt)
ingress := &types.Ingress{
Name: "secure-app",
Rules: []*types.IngressRule{
{
Host: "secure.example.com",
Paths: []*types.IngressPath{
{
Path: "/",
PathType: types.PathTypePrefix,
Backend: &types.IngressBackend{
ServiceName: "secure-app",
Port: 443,
},
},
},
},
},
TLS: &types.IngressTLS{
Hosts: []string{"secure.example.com"},
AutoSSL: true, // Enable Let's Encrypt
},
}
// ACME client will:
// 1. Request certificate for secure.example.com
// 2. Complete HTTP-01 challenge
// 3. Store certificate in database
// 4. Enable HTTPS on :8443
// 5. Auto-renew before expiry
## Ingress with Rate Limiting
ingressPath := &types.IngressPath{
Path: "/api",
PathType: types.PathTypePrefix,
Backend: &types.IngressBackend{
ServiceName: "api",
Port: 8080,
},
RateLimit: &types.RateLimit{
RequestsPerSecond: 10, // 10 requests/sec per IP
Burst: 20, // Allow bursts up to 20
},
}
// Rate limiter enforces:
// • Sustained: 10 req/s per IP
// • Burst: Up to 20 simultaneous requests
// • Excess: 429 Too Many Requests
## Ingress with Access Control
ingressPath := &types.IngressPath{
Path: "/admin",
PathType: types.PathTypePrefix,
Backend: &types.IngressBackend{
ServiceName: "admin",
Port: 8080,
},
AccessControl: &types.AccessControl{
AllowedIPs: []string{
"10.0.0.0/8", // Internal network
"203.0.113.0/24", // Office network
},
DeniedIPs: []string{
"10.0.50.0/24", // Specific subnet blocked
},
},
}
// Access control:
// ✓ 10.0.1.100 → Allowed (in 10.0.0.0/8)
// ✗ 10.0.50.10 → Denied (in deny list)
// ✗ 1.2.3.4 → Denied (not in allow list)
## Ingress with Header Manipulation
ingressPath := &types.IngressPath{
Path: "/api",
PathType: types.PathTypePrefix,
Backend: &types.IngressBackend{
ServiceName: "api",
Port: 8080,
},
Headers: &types.HeaderManipulation{
Add: map[string]string{
"X-Custom-Header": "value",
},
Set: map[string]string{
"X-App-Version": "v1.2.3",
},
Remove: []string{
"Server", // Hide server identity
"X-Powered-By", // Hide framework
},
},
}
## Ingress with Path Rewriting
// Strip prefix before forwarding
ingressPath := &types.IngressPath{
Path: "/api/v1",
PathType: types.PathTypePrefix,
Backend: &types.IngressBackend{
ServiceName: "api",
Port: 8080,
},
Rewrite: &types.PathRewrite{
StripPrefix: "/api/v1",
},
}
// Request: /api/v1/users
// Forwarded: /users (prefix stripped)
// Or replace path entirely
ingressPath := &types.IngressPath{
Path: "/old-api",
PathType: types.PathTypePrefix,
Backend: &types.IngressBackend{
ServiceName: "api",
Port: 8080,
},
Rewrite: &types.PathRewrite{
ReplacePath: "/v2",
},
}
// Request: /old-api/users
// Forwarded: /v2
Integration Points ¶
## Manager Integration
The proxy loads configuration from the manager:
- ListIngresses() - Get all ingress rules
- ReloadIngresses() - Refresh routing table
- ListTLSCertificates() - Load certificates for HTTPS
- CreateTLSCertificate() - Store Let's Encrypt certificates
## Service Discovery Integration
The load balancer discovers service tasks:
- ListTasksByService(serviceName) - Get all replicas
- Filter by health status (only healthy tasks)
- Round-robin across available tasks
## DNS Integration
Ingress works with Warren's DNS server:
- DNS resolves service.warren → manager IP
- Client connects to manager:8000 or :8443
- Ingress proxies to actual service tasks
## Storage Integration
Ingress configuration persisted to BoltDB:
Bucket: "ingresses"
Key: ingress.ID
Value: {Rules, TLS, ...}
Bucket: "tls-certificates"
Key: cert.ID
Value: {CertPEM, KeyPEM, Hosts, ...}
Design Patterns ¶
## Reverse Proxy Pattern
Standard reverse proxy architecture:
Client → Proxy (public endpoint) → Backend (internal service)
Benefits:
- SSL termination at proxy
- Centralized access control
- Load balancing
- Backend services don't need TLS
## Middleware Chain Pattern
Request passes through middleware layers:
Request → [Access Control] → [Rate Limit] → [Headers] → [Rewrite] → Backend
Each middleware can:
- Modify request
- Block request (return error)
- Pass to next middleware
## Strategy Pattern for Load Balancing
Different load balancing strategies:
LoadBalancer (interface) ├── RoundRobinBalancer (current) ├── LeastConnectionsBalancer (future) └── WeightedBalancer (future)
## Observer Pattern for Config Updates
Ingress watches for configuration changes:
- Ingress created/updated in storage
- Proxy polls for changes
- Router updates routing table
- No restart required
Performance Characteristics ¶
## Request Throughput
Proxy performance (single manager node):
- Simple proxy: 10,000-20,000 req/s
- With TLS termination: 5,000-10,000 req/s
- With rate limiting: 8,000-15,000 req/s
- With all middleware: 5,000-10,000 req/s
Bottlenecks:
- TLS handshakes (CPU-bound)
- Backend response time
- Network latency
## Latency
Added latency per request:
- Routing: ~100μs
- Load balancing: ~50μs
- Rate limiting: ~10μs
- Access control: ~20μs
- TLS termination: ~1-2ms (first request), ~100μs (resumed)
- Total overhead: ~1-3ms typical
## Memory Usage
- Base proxy: ~50MB
- Per ingress rule: ~1KB
- Per TLS certificate: ~2KB
- Rate limiters: ~100 bytes per IP
- Total: ~100-200MB for typical deployments
## Connection Handling
HTTP server settings:
ReadTimeout: 30 seconds WriteTimeout: 30 seconds IdleTimeout: 120 seconds
Keep-alive connections reused for better performance.
Troubleshooting ¶
## 404 Not Found
If ingress returns 404:
1. Check ingress rules:
- warren ingress ls
- Verify host matches request
- Verify path matches request
2. Check routing:
- Test with curl -H "Host: example.com" http://manager:8000/
- Check for typos in host/path
- Try catch-all rule ("" host)
3. Check ingress loaded:
- Check manager logs for "Reloaded X ingress rules"
- Manually reload: proxy.ReloadIngresses()
## 503 Service Unavailable
If backend is unreachable:
1. Check service running:
- warren service ps <service-name>
- Verify tasks are "running" state
- Check task health status
2. Check load balancer:
- Verify service name matches
- Check healthy tasks available
- Look for "No healthy tasks" in logs
3. Check network connectivity:
- Can manager reach worker nodes?
- Check firewalls, security groups
- Verify service port is correct
## 429 Too Many Requests
If rate limiting is blocking:
1. Check rate limit configuration:
- Verify RequestsPerSecond is appropriate
- Check Burst allows for spikes
- Consider per-IP vs. global limits
2. Adjust rate limits:
- Increase RequestsPerSecond
- Increase Burst for spiky traffic
- Consider disabling for trusted IPs
## 403 Forbidden
If access control is blocking:
1. Check access control rules:
- Verify client IP is in AllowedIPs
- Check for deny list matches
- Test from different IP
2. Debug IP detection:
- Check X-Forwarded-For header
- Verify client IP parsing
- Consider proxy hops
## TLS/HTTPS Issues
If HTTPS not working:
1. Check certificate:
- warren cert ls
- Verify certificate covers host
- Check expiry date
2. Check ACME:
- Verify HTTP-01 challenge accessible
- Check Let's Encrypt rate limits
- Review ACME client logs
3. Check TLS configuration:
- Verify :8443 listening
- Check TLS version support
- Test with: openssl s_client -connect manager:8443
Monitoring Metrics ¶
Key ingress metrics:
- Requests per second by path
- Response latency (p50, p95, p99)
- HTTP status code distribution
- TLS handshake latency
- Rate limit rejections
- Access control denials
- Backend selection time
- Active connections
Best Practices ¶
1. Ingress Design
- One ingress per application (easier management)
- Use descriptive names
- Group related paths together
- Document routing rules
2. TLS Configuration
- Enable AutoSSL for production
- Use staging Let's Encrypt for testing
- Monitor certificate expiry
- Plan for certificate rotation
3. Rate Limiting
- Start conservative (10-100 req/s)
- Monitor and adjust based on traffic
- Use burst for legitimate spikes
- Consider per-endpoint limits
4. Access Control
- Use IP whitelisting for admin interfaces
- Block known bad IPs
- Consider application-level auth
- Log access denials
5. Load Balancing
- Enable health checks on services
- Monitor backend health
- Plan for task failures
- Test failover scenarios
See Also ¶
- pkg/dns - Service discovery and DNS
- pkg/health - Health checking for load balancing
- pkg/manager - Configuration management
- docs/networking.md - Network architecture guide
Index ¶
- type ACMEClient
- func (a *ACMEClient) CheckAndRenewCertificates() error
- func (a *ACMEClient) ObtainCertificate(domains []string) (*types.TLSCertificate, error)
- func (a *ACMEClient) RenewCertificate(cert *types.TLSCertificate) (*types.TLSCertificate, error)
- func (a *ACMEClient) SaveACMEAccount() error
- func (a *ACMEClient) StartRenewalJob()
- type ACMEUser
- type Backend
- type HTTP01Provider
- type LoadBalancer
- type Middleware
- func (m *Middleware) AddProxyHeaders(r *http.Request)
- func (m *Middleware) ApplyHeaderManipulation(r *http.Request, config *types.HeaderManipulation)
- func (m *Middleware) ApplyPathRewrite(r *http.Request, config *types.PathRewrite)
- func (m *Middleware) CheckAccessControl(r *http.Request, config *types.AccessControl) (bool, string)
- func (m *Middleware) CheckRateLimit(r *http.Request, config *types.RateLimit) bool
- func (m *Middleware) CleanupRateLimiters()
- func (m *Middleware) StartCleanupJob()
- type Proxy
- type Router
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type ACMEClient ¶
type ACMEClient struct {
// contains filtered or unexported fields
}
ACMEClient manages Let's Encrypt certificate issuance and renewal
func NewACMEClient ¶
NewACMEClient creates a new ACME client
func (*ACMEClient) CheckAndRenewCertificates ¶
func (a *ACMEClient) CheckAndRenewCertificates() error
CheckAndRenewCertificates checks all auto-renewable certificates and renews if needed
func (*ACMEClient) ObtainCertificate ¶
func (a *ACMEClient) ObtainCertificate(domains []string) (*types.TLSCertificate, error)
ObtainCertificate requests a new certificate from Let's Encrypt
func (*ACMEClient) RenewCertificate ¶
func (a *ACMEClient) RenewCertificate(cert *types.TLSCertificate) (*types.TLSCertificate, error)
RenewCertificate renews an existing certificate
func (*ACMEClient) SaveACMEAccount ¶
func (a *ACMEClient) SaveACMEAccount() error
SaveACMEAccount saves the ACME account to storage as a secret
func (*ACMEClient) StartRenewalJob ¶
func (a *ACMEClient) StartRenewalJob()
StartRenewalJob starts a background job that checks for certificate renewal
type ACMEUser ¶
type ACMEUser struct {
Email string
Registration *registration.Resource
// contains filtered or unexported fields
}
ACMEUser implements the required user interface for ACME registration
func (*ACMEUser) GetPrivateKey ¶
func (u *ACMEUser) GetPrivateKey() crypto.PrivateKey
func (*ACMEUser) GetRegistration ¶
func (u *ACMEUser) GetRegistration() *registration.Resource
type HTTP01Provider ¶
type HTTP01Provider struct {
// contains filtered or unexported fields
}
HTTP01Provider implements the lego HTTP-01 challenge provider interface
func NewHTTP01Provider ¶
func NewHTTP01Provider(proxy *Proxy) *HTTP01Provider
NewHTTP01Provider creates a new HTTP-01 challenge provider
func (*HTTP01Provider) CleanUp ¶
func (p *HTTP01Provider) CleanUp(domain, token, keyAuth string) error
CleanUp removes the HTTP-01 challenge after verification
func (*HTTP01Provider) GetKeyAuth ¶
func (p *HTTP01Provider) GetKeyAuth(domain, token string) (string, bool)
GetKeyAuth retrieves the key authorization for a given domain and token
func (*HTTP01Provider) Present ¶
func (p *HTTP01Provider) Present(domain, token, keyAuth string) error
Present presents the HTTP-01 challenge by storing it for the proxy to serve
type LoadBalancer ¶
type LoadBalancer struct {
// contains filtered or unexported fields
}
LoadBalancer handles backend selection and load balancing
func NewLoadBalancer ¶
func NewLoadBalancer(managerAddr string, grpcClient *grpc.ClientConn) *LoadBalancer
NewLoadBalancer creates a new load balancer
func (*LoadBalancer) SelectBackend ¶
func (lb *LoadBalancer) SelectBackend(ctx context.Context, serviceName string, port int) (string, error)
SelectBackend selects a backend for the given service Returns the backend IP:port or error
type Middleware ¶
type Middleware struct {
// contains filtered or unexported fields
}
Middleware handles request modification, rate limiting, and access control
func NewMiddleware ¶
func NewMiddleware() *Middleware
NewMiddleware creates a new middleware handler
func (*Middleware) AddProxyHeaders ¶
func (m *Middleware) AddProxyHeaders(r *http.Request)
AddProxyHeaders adds standard proxy headers (X-Forwarded-For, X-Real-IP, etc.)
func (*Middleware) ApplyHeaderManipulation ¶
func (m *Middleware) ApplyHeaderManipulation(r *http.Request, config *types.HeaderManipulation)
ApplyHeaderManipulation applies header manipulation rules to the request
func (*Middleware) ApplyPathRewrite ¶
func (m *Middleware) ApplyPathRewrite(r *http.Request, config *types.PathRewrite)
ApplyPathRewrite applies path rewriting rules to the request
func (*Middleware) CheckAccessControl ¶
func (m *Middleware) CheckAccessControl(r *http.Request, config *types.AccessControl) (bool, string)
CheckAccessControl checks if the request is allowed based on IP access control
func (*Middleware) CheckRateLimit ¶
CheckRateLimit checks if the request should be rate limited
func (*Middleware) CleanupRateLimiters ¶
func (m *Middleware) CleanupRateLimiters()
CleanupRateLimiters removes old rate limiters (call periodically)
func (*Middleware) StartCleanupJob ¶
func (m *Middleware) StartCleanupJob()
StartCleanupJob starts a background job to clean up old rate limiters
type Proxy ¶
type Proxy struct {
// contains filtered or unexported fields
}
Proxy is the main HTTP reverse proxy
func (*Proxy) ReloadIngresses ¶
ReloadIngresses reloads the ingress rules from storage
func (*Proxy) ReloadTLSCertificates ¶
ReloadTLSCertificates reloads TLS certificates from storage
type Router ¶
type Router struct {
// contains filtered or unexported fields
}
Router handles request routing based on host and path
func (*Router) Route ¶
func (r *Router) Route(host, path string) *types.IngressPath
Route finds the matching path configuration for the given host and path Returns the matched IngressPath or nil if no match
func (*Router) UpdateIngresses ¶
UpdateIngresses updates the router with a new list of ingresses