Turning Protections On and Off
Every protection and feature GioJS turns on by default, the gio.toml key that turns it off or loosens it, what that costs, and the few behaviors that stay fixed.
A new GioJS app is locked down without any configuration: cross-site requests are refused, responses carry security headers, connections and bodies are bounded, and the dev tools answer only your own machine. Each of these is a key in gio.toml, so an app with a reason to differ can turn one off - and the server tells you when you have. This page is the map: one table per area, with the key, its default, the value that turns it off, what you give up, and whether startup warns. Each section links to the full reference for its keys.
The rules every switch follows
- On by default. A feature section has
enabled = trueunless you write otherwise; sub-features have their own booleans, alsotrue. 0lifts a limit. Every numeric limit uses0for unlimited and every timeout uses0for none, in every section. To turn a feature off, use itsenabledkey, not a limit of0. The exceptions:[cache] memory_max_entriesmust be at least1([cache] enabled = falseis the page cache's off switch); in[[rate_limits]],per_ip = 0means no refill (onlyburstrequests, ever: the strictest setting) andwindow_seconds = 0is treated as 1 second; and[images] quality = 0is treated as1.- Never silent. Turning a protection off, or lifting a limit that guards memory or connections to
0, logs onewarnline at startup that names the key and what it costs - the Warns column below says which keys do. Timeouts and the prefetch budget lift without a warning, except[server] render_timeout_secsand[images] remote_timeout_secs, which warn: at0, a render that never answers holds its connection and a worker slot indefinitely, and a slow remote source holds its request open.giojs-server --check-configandgio doctorreport the same text underwarnings, so CI can catch it before a deploy. - Misspellings stop the server. An unknown key anywhere in
gio.tomlis a startup error naming the file, the line and the closest valid key. A typo such asenabeld = falsecan never leave a protection on that you meant to turn off, or the reverse.
# Print what startup would decide, without binding a port: errors (unknown keys,
# invalid values, rules that cannot be enforced) and one warning per loosened protection.
npx giojs-server --check-configThe command loads the .env files and gio.toml exactly as startup does, prints a JSON report and exits with 1 when the server would refuse to start. It never prints secrets.
Request protections
Enforced in the Rust server before any of your Node code runs. Reference: [security], [security.csrf], [security.websocket].
| Protection | Key and default | Off or loosened | What you give up | Warns |
|---|---|---|---|---|
CSRF check on POST, PUT, PATCH, DELETE | [security.csrf] enabled = true | false; or list origins in trusted_origins and paths in exempt | Any website can submit forms and other unsafe requests to your app with your visitors' cookies. | yes (off) |
WebSocket Origin check | [security.websocket] check_origin = true | false | Any website can open a WebSocket to your app as the visitor (cross-site WebSocket hijacking). Independent of the CSRF switch. | yes |
Default security headers: X-Content-Type-Options: nosniff, X-Frame-Options: SAMEORIGIN, Referrer-Policy: strict-origin-when-cross-origin | [security] default_headers = true | false drops all three; [security.headers] name = "" drops one | MIME sniffing, framing by other sites (clickjacking) and full-URL referrers come back. | yes (default_headers = false) |
HSTS (Strict-Transport-Security) | [security] hsts unset: sent only when [server.tls] is on | false or "" never sends it; true, a string or a table sends it behind a TLS proxy | Browsers may reach the site over plain HTTP first. | no |
| Content-Security-Policy | off: [security] csp and csp_report_only unset | Opt-in - see Content Security Policy | - | - |
# Loosen instead of turning off, wherever you can.
[security.csrf]
trusted_origins = ["https://admin.example.com"] # another origin of yours
exempt = ["/api/webhooks/*rest", "/saml/acs"] # endpoints other sites post to
[[headers]]
path = "/embed/*rest"
[headers.headers]
x-frame-options = "" # let partners frame one sectionConnection and request limits
These bound what one client, or a flood of them, can make the server hold. Reference: [server] and [[rate_limits]].
| Limit | Key and default | Off | What you give up | Warns |
|---|---|---|---|---|
Request body size (413 past it) | [server] max_body_bytes = 2097152 (2 MiB) | 0 | Bodies are buffered in memory up to the worker's 64 MiB message cap (about 48 MiB of binary body), which still answers 413 above it. | yes (also when set above what a message can carry) |
| Open connections, server-wide | [server] max_connections = 10000 | 0 | A connection flood can exhaust file descriptors and memory. | yes |
Worker answer deadline (504 past it) | [server] render_timeout_secs = 30 | 0 | A render that never answers holds its connection and a worker slot for good. | yes |
Slow clients: TLS handshake, request head, request body (408), idle connections | tls_handshake_timeout_secs = 10, header_read_timeout_secs = 10, request_body_timeout_secs = 30, idle_timeout_secs = 60 | 0 each | Slowloris-style clients can keep connections open indefinitely. | no |
| HTTP/2 streams per connection, keep-alive pings | http2_max_concurrent_streams = 250, http2_keep_alive_interval_secs = 20, http2_keep_alive_timeout_secs = 20 | 0 each | One connection can open any number of streams; dead peers are noticed later. | no |
| Rate-limit buckets kept in memory | [server] rate_limit_max_buckets = 100000 | 0 | Clients rotating addresses grow memory without bound. Warns only when [[rate_limits]] rules exist. | yes |
key_header values one client may hold a budget for | [[rate_limits]] max_keys_per_client = 64 | 0 | One client can mint a fresh budget for every header value it sends. Warns only for rules with a key_header. | yes |
Prefetch budget per client (429 past it) | [prefetch] max_concurrent = 5, max_per_second = 20 | 0 each; [prefetch] enabled = false refuses every prefetch | Prefetching links can render pages for one client without limit. | no |
WebSocket connections (closed with 1013 past it) | [websocket] max_connections = 1000 | 0 | Every open socket holds memory and a file descriptor, without a cap. | yes |
[[rate_limits]] rules themselves are opt-in: no rule, no limit. See Rate limits.
Client identity
Who the client is decides rate limits, the metrics allowlist, logs and ctx.ip. Reference: [server].
| Behavior | Key and default | Loosened | What you give up | Warns |
|---|---|---|---|---|
| Forwarding headers are ignored unless a trusted proxy sent them | [server] trusted_proxies = [] (trust nobody) | a list of your proxies; an entry covering everything (0.0.0.0/0, ::/0) | With a /0 entry, any client can pick its own IP, rate-limit bucket and request id. | yes (/0 only) |
An incoming X-Request-Id is kept only from a trusted proxy | [server] accept_request_id = true | false is stricter: every id is generated here | - | no |
Operations endpoints
Reference: [metrics], [health], [revalidate]. See Endpoints & Headers for every /_gio path.
| Endpoint | Key and default | Off or loosened | What you give up | Warns |
|---|---|---|---|---|
/_gio/metrics (Prometheus) | off without a [metrics] section; with one and no token or ip_allowlist, loopback clients only | ip_allowlist = ["0.0.0.0/0", "::/0"] opens it to everyone; enabled = false turns it off | Anyone can read your traffic, routes and worker state. | yes (open to everyone without a token) |
/_gio/health | [health] enabled = true, details = true | enabled = false answers 404; details = false answers only {"status":"ok","nodeReady":...} | Load balancers lose the endpoint; gio dev, gio start and the testing kit then treat any answer as ready. | no |
POST /_gio/revalidate | off: exists only with [revalidate] token or GIO_REVALIDATE_TOKEN | Opt-in | - | - |
Development server
These keys apply only when the server runs with NODE_ENV=development (gio dev). In production /_gio/devtools* always answers 404. Reference: [dev].
| Behavior | Key and default | Off or loosened | What you give up | Warns |
|---|---|---|---|---|
| Dev endpoints and error details answer only local hosts (DNS-rebinding protection) | [dev] allowed_hosts = [] | list hostnames you browse from; ["*"] answers any Host from any machine | With "*", any site can read your source codeframes and live server state through DNS rebinding. | yes ("*"; invalid entries are ignored with a warning) |
| Dev dashboard, codeframes, open-in-editor, live reload | [dev] devtools = true | false: /_gio/devtools* is not routed, and the error overlay shows no codeframes or editor links and does not live-reload | The overlay tooling. | no (logged as info) |
| Restart the worker when a source file changes | [dev] watch = true | false; or keep it and list paths in watch_ignore | Edits need a manual restart. | no |
Features
Turning one of these off saves work or hands it to something else (a CDN, an image service). None of them warns, except skew_protection = false and the image limits at 0.
| Feature | Key and default | Off | What happens instead |
|---|---|---|---|
| Page cache (memory and disk) | [cache] enabled = true | false | Every request renders (X-Gio-Cache: bypass). Cache-Control still follows revalidate, so a CDN can keep caching. |
| Disk tier of the page cache | [cache] disk_enabled = true | false | Memory only; nothing written, nothing survives a restart. |
Page ETag and 304 | [cache] etag = true | false | Pages are always sent in full. |
| Serving stale pages while one refresh runs | [cache] swr_multiplier = 10 | 0 | A stale page is never served; Cache-Control drops stale-while-revalidate. |
Image optimizer (/_gio/image) | [images] enabled = true | false | /_gio/image answers 404; <GioImage> renders its plain src without a srcset. |
| Image optimizer limits | max_remote_bytes = 20971520, remote_timeout_secs = 30, max_source_dimension = 10000, max_decode_bytes = 268435456 | 0 each (warns) | Large or slow sources, or a small file declaring huge dimensions, can exhaust memory and CPU. |
| Prefetching | [prefetch] enabled = true | false | Every prefetch gets 429 before it renders; links still navigate. |
| Compression (gzip, Brotli) | [compression] enabled = true | false | Responses go out uncompressed (a proxy in front may compress them). |
| CSS served by path, minification, critical CSS | [css] enabled, minify, critical_extraction, all true | false each | enabled covers path-served app/*.css only; imported CSS is always bundled. |
| Font preload | [[fonts]] preload = true | false per entry | The @font-face stays; the browser fetches the file when text needs it. |
| WebSocket routes | [websocket] enabled = true | false | Upgrades get 501. ping_interval_secs = 0 keeps sockets but sends no pings. |
| Deployment skew protection | [server] skew_protection = true | false (warns) | x-deployment-id is ignored: a tab running an older build keeps navigating softly, with no 409 hard reload. |
.env file loading | [env] files = true | false, or GIO_ENV_FILES=0 | The process environment is all there is. GIO_ENV_FILES wins over the key (1 forces loading on). |
| HTTP/2 | [server] http2 = true | false | HTTP/1.1 only. |
What stays fixed, and why
A few behaviors have no switch. Each one either protects every other rule on this page or keeps one visitor's data away from another, and none blocks something an app legitimately needs:
- Canonical request paths. Repeated and trailing slashes collapse and escaped unreserved characters are decoded before any rule matches; dot segments, a raw
\and invalid%escapes get400. Every guard, rate limit, header rule and CSRF exemption relies on one spelling of a path - with it off,/api//loginor/api/%6Coginwould slip past a rule for/api/login. Browsers never send the refused forms. - The closed
/_gionamespace. Paths under/_gio/that are not built-in endpoints answer404from Rust and never reach the app, so a dynamic route likeapp/[org]/settingscannot be rendered as/_gio/settingspast its guard. Use any other prefix for your own routes. - No error details in production. A failed render answers a generic page with a digest, and the message and stack are logged under that digest. Raw messages leak file paths, SQL and sometimes secrets; the digest leads you to the full log line. See Error Handling.
- Personalized renders are never shared. A render that read cookies, credentials, the client's IP or host is never stored, whatever
revalidatesays. A switch would hand one visitor's page to everyone. To cache and personalize, cache the shell withshell = 'cache'and personalize inside Suspense holes. - The server-only import guard is opt-in per module: importing
@gio.js/core/server-onlyor naming a file*.server.tsmarks it. Removing the marker is the off switch; a global one would ship the marked modules (database clients, keys) to browsers. Seeserver-only. - Unknown
gio.tomlkeys are errors. Tables for other tools go in[x-...]tables, which the server skips. - Only
GIO_PUBLIC_*variables reach the browser. The prefix is the opt-in; rename a variable to expose it. See Environment Variables.
Parts of switchable features stay fixed too:
- Open-in-editor requires a same-origin request (or
Sec-Fetch-Site: none), andallowed_hosts = ["*"]does not cover it: it answers only localhost hosts from this machine, a specific[server] hostand hosts listed by name, so no website, not even one rebound onto the dev server through DNS, can launch your editor. - The image optimizer always rejects path traversal, never follows redirects for remote sources, and holds a local
srcto the guards of its URL. - An adopted
X-Request-Idmust be 1 to 128 characters of letters, digits,.,_,:and-; anything else is replaced, which keeps log and header injection out. - The revalidation token must be at least 32 bytes: a short bearer token can be guessed, and a long one costs nothing.
Example: an internal app behind a gateway
An admin tool reachable only through a company gateway that terminates TLS, adds its own headers and posts webhooks from a partner origin. Each loosening is deliberate, and startup lists them:
[server]
trusted_proxies = ["10.0.0.0/8"] # the gateway; never 0.0.0.0/0
max_body_bytes = 20971520 # 20 MiB uploads
[security]
default_headers = false # the gateway sets them (warns)
hsts = true # TLS ends at the gateway
[security.csrf]
trusted_origins = ["https://partner.example.com"]
[metrics]
ip_allowlist = ["10.0.0.0/8"] # the Prometheus network; loopback only without it
[health]
details = false # no deployment id or topology on a public probe--check-config accepts it and lists the one protection it turns off (output trimmed):
$ npx giojs-server --check-config
{...,"errors":[],...,"ok":true,...,"trustedProxies":1,"warnings":["[security] default_headers = false: responses no longer carry x-content-type-options, x-frame-options or referrer-policy (MIME sniffing, clickjacking and full-URL referrers are back) unless [security.headers] sets them"]}Related
- Security - how each protection works
- Content Security Policy - the one protection that is opt-in
- gio.toml reference - every key, with the full reference block
- Production Checklist
- Upgrading to beta.8 - defaults that changed
Version history
| Version | Changes |
|---|---|
v0.1.0-beta.8 | The request protections, connection limits, trusted proxies and the dev host check are new, on by default. Introduced the switches [security] default_headers, [server] skew_protection, render_timeout_secs, rate_limit_max_buckets, [dev] devtools, watch, [health], [env], [images] enabled, [cache] enabled, disk_enabled, etag, swr_multiplier, [prefetch] enabled and [[fonts]] preload, and the limits [[rate_limits]] max_keys_per_client, [images] remote_timeout_secs, max_source_dimension and max_decode_bytes. 0 now lifts every limit. One startup warning per loosened protection. |