1 TLS Modes
Soli Proxy supports two TLS modes. Switch between them with a single configuration line.
Auto Mode
DefaultGenerates a self-signed certificate for localhost on startup. Perfect for local development -- HTTPS works immediately with no external dependencies.
# Self-signed cert for localhost
[tls]
mode = "auto"
Let's Encrypt Mode
ProductionAutomatic ACME HTTP-01 challenge validation for all configured domains. Certificates are issued, stored, and renewed without any manual intervention.
[tls]
mode = "letsencrypt"
cache_dir = "./certs"
[letsencrypt]
staging = false
email = "admin@example.com"
terms_agreed = true
2 Certificate Auto-Provisioning
The entire certificate lifecycle is fully automated -- from domain discovery to renewal.
Domain Discovery
Domains are collected from two sources: hostnames in proxy.conf routing rules and domain fields in sites/*/app.infos files. All unique domains are queued for certificate provisioning.
ACME Account
An ACME account is created on first run and persisted to certs/account_credentials.json. On subsequent starts, the existing account is restored automatically.
HTTP-01 Challenge Validation
The proxy intercepts requests to /.well-known/acme-challenge/ and serves the ACME token response. This happens transparently before any backend routing.
Certificate Issuance & SNI Routing
Once validated, the certificate and private key are stored per-domain. The TLS acceptor uses SNI (Server Name Indication) to serve the correct certificate for each incoming connection.
Auto-Renewal
A background task checks every 12 hours whether any certificate is within 30 days of expiry. Certificates nearing expiry are renewed automatically with zero downtime.
certs/
account_credentials.json # ACME account keypair
self-signed.cert.pem # Reserved fallback (auto-generated)
self-signed.key.pem
example.com.cert.pem # Per-domain cert (exact-match SNI)
example.com.key.pem
api.example.com.cert.pem
api.example.com.key.pem
_wildcard.example.com.cert.pem # Wildcard cert (covers *.example.com,
_wildcard.example.com.key.pem # one label deep per RFC 6125)
SNI resolution order: exact-match <sni>.cert.pem → wildcard _wildcard.<parent>.cert.pem → self-signed fallback. Each cert is validated against its expected SAN at load time; mismatches log a warning and are skipped.
3 Circuit Breaker
Per-backend circuit breakers track health and automatically stop routing traffic to failing backends.
Closed
Healthy
Traffic flows normally. Failures are counted. When consecutive failures reach failure_threshold, the breaker opens.
Open
Unhealthy
All requests to this backend are rejected immediately (503). After recovery_timeout_secs, the breaker moves to half-open.
Half-Open
Probing
A single probe request is allowed through. If success_threshold consecutive successes occur, the breaker closes. Any failure re-opens it.
State Transitions
# Circuit Breaker Configuration
# Tracks backend health and stops sending traffic to failing backends.
# When all targets for a route are open, returns 503 Service Unavailable.
[circuit_breaker]
failure_threshold = 5 # consecutive failures before opening
recovery_timeout_secs = 30 # seconds before probing an open backend
success_threshold = 2 # successful probes to close breaker
failure_status_codes = [502, 503, 504] # HTTP codes that count as failures
Multi-Target Load Balancing
When a route has multiple targets, Soli Proxy supports three load balancing strategies. Use @lb:round-robin, @lb:weighted, or @lb:failover to specify the strategy. Default is round-robin.
# Round-robin load balancing (default)
api.example.com -> http://backend-1:3000, http://backend-2:3000
# Weighted load balancing
example.com/api/* -> http://api-primary:8080, http://api-secondary:8080 # @lb:weighted
# Failover: use second target only if first fails
example.com/backup/* -> http://primary:8080, http://backup:8080 # @lb:failover
Admin API Endpoints
/api/v1/circuit-breaker
Returns the current state of every tracked backend: state (closed/open/half_open), consecutive failures, and consecutive successes.
/api/v1/circuit-breaker/reset
Resets all circuit breakers to the closed (healthy) state. Useful after deploying a fix to a failing backend.
{
"http://backend-1:3000": {
"state": "closed",
"consecutive_failures": 0,
"consecutive_successes": 12
},
"http://backend-2:3000": {
"state": "open",
"consecutive_failures": 5,
"consecutive_successes": 0
}
}
4 Prometheus Metrics
Soli Proxy exposes a built-in /metrics endpoint in Prometheus text format, ready for scraping by Prometheus, Grafana Agent, or any compatible collector.
[metrics]
enabled = true
endpoint = "/metrics"
Sample Output
# HELP proxy_requests_total Total number of HTTP requests
# TYPE proxy_requests_total counter
proxy_requests_total 48291
# HELP proxy_requests_in_flight Number of requests currently being processed
# TYPE proxy_requests_in_flight gauge
proxy_requests_in_flight 12
# HELP proxy_bytes_received Total bytes received from clients
# TYPE proxy_bytes_received counter
proxy_bytes_received 2847103
# HELP proxy_bytes_sent Total bytes sent to clients
# TYPE proxy_bytes_sent counter
proxy_bytes_sent 91740258
# HELP proxy_response_time_seconds Average response time in seconds
# TYPE proxy_response_time_seconds gauge
proxy_response_time_seconds 0.000284
# HELP proxy_tls_connections_total Total number of TLS connections
# TYPE proxy_tls_connections_total counter
proxy_tls_connections_total 15820
# HELP proxy_errors_total Total number of proxy errors
# TYPE proxy_errors_total counter
proxy_errors_total 3
# HELP proxy_response_status_codes_total HTTP response status codes
# TYPE proxy_response_status_codes_total counter
proxy_response_status_codes_total{code="200"} 45102
proxy_response_status_codes_total{code="301"} 2040
proxy_response_status_codes_total{code="404"} 1146
proxy_response_status_codes_total{code="503"} 3
Metrics Reference
| Metric | Type | Description |
|---|---|---|
proxy_requests_total |
counter | Total number of HTTP requests processed |
proxy_requests_in_flight |
gauge | Number of requests currently being processed |
proxy_bytes_received |
counter | Total bytes received from clients |
proxy_bytes_sent |
counter | Total bytes sent to clients |
proxy_response_time_seconds |
gauge | Average response time in seconds |
proxy_tls_connections_total |
counter | Total number of TLS connections established |
proxy_errors_total |
counter | Total number of proxy errors encountered |
proxy_response_status_codes_total |
counter | HTTP response status codes with code label |
# Worker Privilege Dropping
By default, app workers inherit the same privileges as the Soli Proxy process. If you run the proxy as root (to bind ports 80/443), all workers also run as root. This is a security risk — a compromised app gives attackers full system access.
Configuring Per-App Users
Use the user and group fields in app.infos to run each app as a different user:
# Run app workers as a specific user
name = "myapp"
domain = "myapp.example.com"
start_script = "npm start"
user = "nobody"
group = "nogroup"
Never run app workers as root in production
Always configure a dedicated non-root user for each app. This limits the blast radius if an app is compromised.
Removing the Server Header
By default, Soli Proxy does not add a Server header to responses. This hides your proxy software version from clients, preventing attackers from targeting known vulnerabilities in specific versions. Use Lua scripting to remove additional headers:
-- Remove sensitive headers from responses
function on_response(mods)
mods.remove_headers = {"Server", "X-Powered-By", "X-AspNet-Version"}
end
# Security Hardening
Soli Proxy includes several built-in protections against common attacks. These are configurable via config.toml.
Slowloris Protection
The keep_alive_timeout setting limits how long a client can take to send headers. This prevents slowloris attacks where clients hold connections open indefinitely by sending headers one byte at a time.
[limits]
keep_alive_timeout = 30 # seconds to complete header transmission
Request Size Limits
The max_request_size setting rejects requests whose Content-Length header exceeds the configured limit, protecting against large-payload DoS and memory exhaustion. When this setting is configured, requests using Transfer-Encoding: chunked are also rejected outright (since chunked bodies cannot be size-checked from headers alone); set a non-chunked client or omit max_request_size if your workload genuinely requires chunked uploads.
[limits]
max_request_size = "10MB" # reject requests with Content-Length exceeding this
Backend Request Timeout
The request_timeout setting limits how long a backend request can take before being terminated. This prevents hanging connections and ensures resources are freed for waiting clients.
[limits]
request_timeout = 60 # seconds before backend request is terminated
Rate Limiting (via Lua)
Soli Proxy does not ship a built-in rate limiter, but the Lua scripting API exposes everything needed to implement one: process-shared atomic counters (shared.incr), a millisecond clock (time.now_ms), header access, and early denial via req:deny. The fixed-window per-IP limiter below allows 100 requests per minute per client.
-- Fixed-window per-IP rate limiter: 100 requests per minute
function on_request(req)
local ip = req:header("x-forwarded-for") or "unknown"
local window = math.floor(time.now_ms() / 1000 / 60)
local key = "rl:" .. ip .. ":" .. window
local count = shared.incr(key)
if count > 100 then
return req:deny(429, "rate limit exceeded")
end
end
Limitations to be aware of
sharedstate is in-memory and per-process — counters are not synchronized across multiple proxy instances.- Shared keys have no automatic TTL. Bucketing by time window (as above) prevents unbounded growth in practice, but stale buckets are not reaped.
X-Forwarded-Foris only trustworthy behind a proxy you control; otherwise clients can spoof the header to evade limits.
Always use TLS in production
Without TLS, all traffic including authentication tokens and session cookies are transmitted in plaintext. Configure Let's Encrypt for automatic certificates.