Edge and HTTP Ingress Security

July 22, 2026 ยท View on GitHub

Sub2API supports long-lived SSE and WebSocket requests. Protect the request ingress without imposing a response WriteTimeout: a write deadline would terminate healthy long generations and streams.

Application defaults

  • server.max_header_bytes: 65536 limits HTTP/1 request headers to 64 KiB; Go maps it to the corresponding HTTP/2 header-list limit.
  • server.read_header_timeout: 10 bounds slow-header attacks. It does not limit request processing or response streaming.
  • server.max_request_body_size: 268435456 is the absolute 256 MiB safety net.
  • gateway.max_body_size: 268435456 remains available to multimodal, Gemini, image, video, and batch-image endpoints.
  • gateway.text_max_body_size: 33554432 limits the known pure-text /embeddings and /alpha/search endpoints to 32 MiB.
  • H2C defaults to 50 concurrent streams per connection, a 2 MiB connection upload window, and a 512 KiB stream upload window.
  • Invalid credential abuse is limited in process by trusted client IP (IPv6 /64): 120 failures per 60 seconds followed by a 60-second block. This is a per-instance safety net; multi-instance enforcement still belongs at the load balancer, CDN, or WAF.

Do not add a single application-wide request semaphore: an SSE request may legitimately occupy it for many minutes. Apply connection and unauthenticated request controls at the edge; authenticated user/API-key concurrency remains the application's responsibility.

Trusted client IPs

security.trust_forwarded_ip_for_api_key_acl is enabled by default for upgrade compatibility. While enabled, raw forwarding headers take over client-IP resolution for logs and security-sensitive paths. Custom headers from security.forwarded_client_ip_headers are checked in configured order before the built-in CF-Connecting-IP, X-Real-IP, and X-Forwarded-For fallback. Header names are case-insensitive, normalized when loaded, de-duplicated, and limited to 16 unique valid HTTP field names. Header values must contain IP literals; comma-separated values are supported, invalid entries are skipped, and public addresses are preferred over private fallback addresses.

The list can be supplied in YAML or with the comma-separated environment variable SECURITY_FORWARDED_CLIENT_IP_HEADERS; an explicitly empty environment value clears YAML values. It is also editable from the admin security settings and updates at runtime without a restart. A request snapshots the switch and header list together, so one request cannot mix old and new settings. Custom headers are ignored completely when the switch is disabled. In that mode Gin's server.trusted_proxies chain is authoritative: configure only the exact CIDR/IP addresses that connect directly to Sub2API. An explicit empty list trusts no forwarded client IPs.

On the first upgrade to this mode, a legacy false value is changed to true only when server.trusted_proxies was not explicitly configured; explicit proxy policies remain in secure mode. New installations persist the configured custom header list during database initialization. Existing installations backfill a missing database value from the YAML configuration. A hidden migration marker prevents later administrator changes from being overwritten. If settings cannot be read or the persisted custom-header list is malformed, the process fails closed to trusted-proxy mode with no custom headers. If a migration write fails, the computed mode remains active for the current process and startup records a warning.

Compatibility takeover accepts forwarded headers without validating the direct peer, including any configured custom header. Protect the origin from direct access while it is enabled. A CDN deployment must firewall the origin so only the CDN or load balancer can reach it, and that proxy must overwrite every trusted client-IP header rather than append an untrusted client value.

Example for a proxy on the same host:

server:
  trusted_proxies:
    - 127.0.0.1/32
    - ::1/128

Nginx baseline

Define shared zones in the http block. Tune rates to measured legitimate traffic; the values below are conservative starting points, not universal capacity targets.

limit_conn_zone $binary_remote_addr zone=sub2api_conn:20m;
limit_req_zone  $binary_remote_addr zone=sub2api_auth:20m rate=5r/s;
limit_req_zone  $binary_remote_addr zone=sub2api_api:40m rate=30r/s;
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

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

    client_header_timeout 10s;
    client_max_body_size 256m;
    large_client_header_buffers 4 16k;
    limit_conn sub2api_conn 40;

    location ~ ^/(auth|api/auth)/ {
        limit_req zone=sub2api_auth burst=10 nodelay;
        proxy_pass http://127.0.0.1:8080;
    }

    location ~ ^/(v1/)?(embeddings|alpha/search)$ {
        client_max_body_size 32m;
        limit_req zone=sub2api_api burst=60 nodelay;
        proxy_pass http://127.0.0.1:8080;
    }

    location / {
        limit_req zone=sub2api_api burst=60 nodelay;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_buffering off;
        proxy_request_buffering off;
        proxy_read_timeout 1800s;
        proxy_send_timeout 1800s;
        proxy_pass http://127.0.0.1:8080;
    }
}

If Nginx gzip is enabled in the http block, keep text/event-stream out of gzip_types and do not use gzip_types * for Sub2API. The proxy_buffering off setting above prevents proxy buffering, but it does not disable the gzip response filter. Use an explicit list for ordinary responses:

gzip on;
gzip_types text/plain text/css application/json application/javascript application/xml image/svg+xml;

If a shared global configuration cannot exclude SSE by content type, set gzip off; in the locations serving streaming API routes. This leaves gzip available for the web UI and static assets.

Do not use an incoming $http_x_forwarded_for value unless Nginx real-IP processing is restricted to explicit trusted proxy CIDRs.

Caddy and CDN

The bundled deploy/Caddyfile sets a 64 KiB header limit, a 10-second header timeout, a 256 MiB absolute body limit, and overwrites forwarded addresses from the TCP peer. It is therefore a direct-to-Caddy baseline. Do not use its {remote_host} forwarding lines unchanged behind a CDN: all clients would be attributed to a CDN egress address, collapsing rejection aggregation and the invalid-auth limiter onto unrelated users.

The bundled Caddy configuration leaves flush_interval unset so Caddy can automatically flush text/event-stream responses while still propagating client cancellation upstream. Do not set it globally: positive values can add streaming latency, while Caddy 2.6.2's special -1 mode also causes reverse-proxied requests to continue after clients disconnect. The configuration uses an explicit response content-type list for compression. Do not replace that list with text/* or the shorthand encode gzip zstd: both match text/event-stream and can buffer SSE until the response ends. Keep streaming responses uncompressed while retaining compression for the web UI, JSON, and static assets.

For a CDN deployment, first firewall the origin so only current CDN egress CIDRs can connect. Then configure those exact ranges as Caddy trusted proxies and derive upstream headers from Caddy's parsed {client_ip}. For example:

{
	servers {
		trusted_proxies static 192.0.2.0/24 2001:db8:1234::/48
		trusted_proxies_strict
		client_ip_headers CF-Connecting-IP X-Forwarded-For
	}
}

api.example.com {
	reverse_proxy 127.0.0.1:8080 {
		header_up X-Real-IP {client_ip}
		header_up X-Forwarded-For {client_ip}
	}
}

Replace the documentation ranges with the CDN's published, automatically maintained egress ranges. CF-Connecting-IP is safe here only because direct origin access is blocked and Caddy trusts only those TCP peers. Configure Sub2API server.trusted_proxies with the Caddy address/private subnet so the application accepts only Caddy's rewritten headers.

Caddy core does not provide a general request-rate limiter; use a trusted CDN/WAF, a supported rate-limit module, or host firewall controls.

At a CDN/WAF, configure connection limits, header/body limits, bot challenges, and per-IP/ASN rates before traffic reaches the origin. Allow origin ingress only from CDN egress CIDRs or a private load balancer. Keep the application port off the public Internet.

DDoS boundary

Application checks reduce amplification after a connection reaches Go. They cannot absorb volumetric attacks, TLS floods, bandwidth saturation, or a large distributed source set. Those require upstream network capacity, CDN/WAF filtering, provider firewall rules, and origin isolation. Avoid high-cardinality metrics or per-request database security logs during rejection storms.