HTTP server
Stalwart’s HTTP server serves JMAP, CalDAV, CardDAV and WebDAV, the WebUI, OAuth and the management API on every HTTP listener. A few decisions concern all of these at once: how the server learns the client’s real address behind a proxy, which headers it adds to its responses, and how fast a client may send requests. They are made on the Http singleton (found in the WebUI under Settings › Network › HTTP › General, Settings › Network › HTTP › Security), whose settings apply to all HTTP listeners and take effect after a settings reload.
Two related settings live elsewhere. The ports and the TLS of the listeners are set on the listeners. Which paths are reachable by whom is set with access control rules, in the same object’s allowedEndpoints.
The base URL that the server publishes in its discovery documents is not an Http setting either. It comes from STALWART_PUBLIC_URL or from the server hostname, as described in Public URLs.
Client IP address
Section titled “Client IP address”The server uses the client’s address for rate limiting and banning, so it needs the real one. Normally it takes the address from the TCP connection, or from the PROXY protocol header of a trusted proxy. Behind a reverse proxy that terminates TLS and forwards HTTP requests, however, every connection comes from the proxy. The client’s address then travels in a forwarding header that the proxy adds. clientIpSource selects where the address is taken from:
peer: the connection’s address, ignoring forwarding headers.xForwardedFor: theX-Forwarded-Forheader.forwarded: thefor=parameter of theForwardedheader defined in RFC 7239.
The default is peer. Behind such a proxy, every request therefore appears to come from the proxy until the field is changed.
Forwarding headers can contain several addresses, and not all of them can be trusted. Each proxy appends the address it received the request from to the end of the header. The rightmost entries are therefore written by trusted proxies, and the leftmost by whoever sent the request. forwardedHops is the number of proxies in front of Stalwart, at most 16. The client address is taken that many entries from the right, and entries further left are ignored. The default of 1 suits a single proxy, whose last entry is used. With a CDN in front of the proxy, set it to 2.
Only the selected header is read, and malformed headers are refused. A request is refused with HTTP 400, and logs http.x-forwarded-invalid, when its selected entry is missing, cannot be parsed or names a loopback address, or when its header has fewer entries than forwardedHops. A request without the header is processed with the connection’s address. That case is logged as http.x-forwarded-missing, unless the connection comes from a loopback address. Requests from banned addresses receive HTTP 403.
Forwarding headers carry a risk. Anyone who can connect to an HTTP listener directly can send a forwarding header with any address, including an allowed one. When clientIpSource is not peer, make every HTTP listener reachable only through the proxy. When the address comes from a forwarding header, malformed requests also do not count towards the scanning ban of auto-ban.
Root redirect
Section titled “Root redirect”A request for / is redirected with HTTP 302 to the path in redirectRoot, which is /account unless changed. When the field is unset, / returns 404.
Response headers
Section titled “Response headers”responseHeaders adds headers to every response, as a map from header name to value. A typical use is a Content-Security-Policy, or a header that identifies the server behind a load balancer:
{ "responseHeaders": { "X-Served-By": "mail1" }}A configured header replaces any header of the same name that the server would otherwise send. Overriding headers such as Cache-Control or Content-Type can therefore break clients.
Security
Section titled “Security”Two settings add security headers to every response. The first is enableHsts, which adds HTTP Strict Transport Security (RFC 6797): Strict-Transport-Security: max-age=31536000; includeSubDomains. Browsers that receive this header use only HTTPS for the host and its subdomains, for a year. The setting is disabled until it is turned on. Because the commitment lasts a year and covers subdomains, enable it only when every name under the host is served over HTTPS, and not on a server whose WebUI is still used over plain HTTP.
The second is usePermissiveCors, disabled until it is turned on. Browsers normally stop a web page from calling a server on another origin, unless the server allows it with Cross-Origin Resource Sharing (CORS) headers. With this setting, the server adds the following CORS headers to every response, which allow web pages on any origin to call it from a browser:
Access-Control-Allow-Origin: *Access-Control-Allow-Headers: Authorization, Content-Type, Accept, X-Requested-WithAccess-Control-Allow-Methods: POST, GET, PATCH, PUT, DELETE, HEAD, OPTIONSThe setting is needed when a web application hosted on another origin calls the JMAP or management API directly. It does not open any endpoint, because authentication still applies to every one.
Permissive CORS is sometimes enabled regardless of the setting. In bootstrap mode, which runs the setup wizard on a new installation, and in recovery mode, the maintenance mode for repairing a configuration, it is always enabled. Some public discovery documents, such as the autoconfiguration and OAuth metadata, also carry these headers whatever the setting.
Rate limiting
Section titled “Rate limiting”Two request rates limit how fast a client can send HTTP requests, one for anonymous clients and one for authenticated accounts. rateLimitAnonymous applies per client IP address to unauthenticated requests, including failed sign-ins, and allows 100 per minute unless changed. rateLimitAuthenticated applies per account to authenticated requests, and allows 1000 per minute unless changed.
Each rate is a count of requests within a period in milliseconds, and setting it to null removes the limit:
{ "rateLimitAnonymous": {"count": 100, "period": 60000}, "rateLimitAuthenticated": {"count": 1000, "period": 60000}}A request over the limit is refused with HTTP 429. The response carries a Retry-After header with the seconds until the window ends, RateLimit-Policy and RateLimit headers, and a JSON problem body, and the server logs a limit.too-many-requests event.
Some clients are exempt. Addresses in the allowed IP list and the networks in SystemSettings.proxyTrustedNetworks are not limited, and neither are accounts holding the unlimitedRequests permission. Loopback addresses are not exempt. In recovery mode, the limits are not applied.