210 lines
9.0 KiB
Markdown
210 lines
9.0 KiB
Markdown
# Edge and HTTP Ingress Security
|
|||
|
|
|
||
|
|
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:
|
||
|
|
|
||
|
|
```yaml
|
||
|
|
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.
|
||
|
|
|
||
|
|
```nginx
|
||
|
|
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:
|
||
|
|
|
||
|
|
```nginx
|
||
|
|
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:
|
||
|
|
|
||
|
|
```caddyfile
|
||
|
|
{
|
||
|
|
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.
|