Skip to content

Config Server

Dejan Strbac edited this page Nov 12, 2026 · 1 revision

This page covers the keys of the [server] section: where ALPS listens, which reverse proxies and origins it trusts, login_key, the users' own CalDAV and CardDAV servers, the temp directory, session lifetimes and caps, HTTP timeouts, attachment limits, new-mail push and IMAP change numbers. Each group has a table (key, default, meaning) and a short example. The full reference is section 1 of docs/CONFIGURATION.md. The login rate limits, [server.rate_limit], are on Config-Rate-Limiting-and-WebAuthn.

How the section is read

  • The keys go under [server] in the file passed with alps -config <path>. ALPS reads the file once, at startup. Restart ALPS to apply a change: SIGHUP only reopens the log file.
  • ALPS ignores a key it does not know and keeps that key's default, but logs a warning for it at startup, naming the key and not its value. A misspelt write_timeout = 60 (for write_timeout_sec) starts ALPS as if the line were not there, and logs Unknown configuration key: server.write_timeout (ignored). Check the startup warnings after each change, and key names against config.example.toml.
  • A known key with a value of the wrong type stops startup with exit status 1, for example:
    Error loading config file: failed to parse TOML config: toml: line 2 (last key "server.max_sessions"): incompatible types: TOML value has type string; destination has type integer
    
  • For the number keys, 0 or a negative value keeps the default. So the timeouts and the attachment limits cannot be turned off. The exceptions are max_session_minutes, where 0 or a negative value means no cap, and absolute_session_hours, max_sessions and max_sessions_per_user, where 0 keeps the default and a negative value, such as -1, removes the limit.

Listening

Key Default Meaning
addr ":1323" The address and port ALPS listens on. ":1323" is every interface. With [tls] enabled = true, the same address serves HTTPS (see Config-TLS).
debug false For development only. Where [logging] does not set them, it changes the default log level to debug and the default format to console text. It logs TLS handshake errors, which are otherwise dropped. It also turns on the IMAP, CalDAV, CardDAV and ManageSieve protocol traces, which go through [logging] as protocol trace lines at debug level, so they appear only while the log level is debug. These include message, contact and calendar content (passwords, and the HTTP Authorization and cookie headers, are redacted). Keep it off in production. See Config-Logging-and-Cache.

When a reverse proxy runs on the same host, listen on the loopback address only, so that only the proxy reaches ALPS:

[server]
addr = "127.0.0.1:1323"

At startup ALPS logs Starting server on 127.0.0.1:1323.

Proxies and origins

Key Default Meaning
trusted_proxies none Addresses or CIDR blocks of your own reverse proxies. A bare address counts as /32 (IPv4) or /128 (IPv6). Blank entries are skipped, and any other entry that does not parse stops startup (invalid trusted_proxies IP/CIDR "10.0.0.300/32"). ALPS reads forwarded headers only on requests whose TCP peer is in this list.
trusted_origins none Extra origins the CSRF check accepts, beside the one ALPS works out from the request. Write each as scheme://host or scheme://host:port. Case does not matter and a trailing / is dropped, but the port must match. Use it when the proxy cannot send X-Forwarded-Proto and X-Forwarded-Host.

On a request from a listed proxy, ALPS reads these headers:

Header What ALPS uses it for
X-Forwarded-For The client's address, as used by the access log, the sign-in log lines and the per-address rate limits. ALPS reads the list from the right, skips the addresses in trusted_proxies, and takes the first address that is not in it.
X-Real-IP The client's address, when X-Forwarded-For gives none.
X-Forwarded-Proto (first value) The scheme of the origin the CSRF check expects, and whether the cookies get the Secure flag.
X-Forwarded-Host (first value) The host of the origin the CSRF check expects.

On a request from any other peer, ALPS ignores these headers. The client's address is then the TCP peer, and the expected origin is the connection's scheme plus the request's Host header.

For POST, PUT, PATCH and DELETE requests, the browser's Origin header must equal the expected origin or an entry of trusted_origins. When the browser sends no Origin, its Referer must start with one of them. Otherwise ALPS answers 403 Invalid origin (or Invalid referer). The log names both origins and often the likely cause. These are the messages of the two log lines (JSON by default, see Config-Logging-and-Cache) for a TLS-terminating proxy at 127.0.0.1 that is not listed:

CSRF: Origin mismatch: got "https://webmail.example.com", expected "http://webmail.example.com" (scheme differs and the peer 127.0.0.1 is not in trusted_proxies; behind a TLS-terminating proxy, forward X-Forwarded-Proto and list the proxy in trusted_proxies, or set trusted_origins)
Request error: code=403, message=Invalid origin

Advice:

  • List only proxies you run. This list decides whose word ALPS takes for a client's address, and so keeps the per-address rate limits counting each real client.
  • List every proxy hop in front of ALPS (load balancer and proxy), since the first unlisted address from the right is taken for the client.
  • Prefer trusted_proxies with forwarded headers over trusted_origins alone. With trusted_origins alone, cookies behind a TLS-terminating proxy get no Secure flag, and every user shows up with the proxy's address, so they share one per-address rate limit.
  • Pass the browser's Origin header through unchanged. Do not rewrite it in the proxy.

For nginx on the same host, speaking HTTP to ALPS:

[server]
addr = "127.0.0.1:1323"
trusted_proxies = ["127.0.0.1", "::1"]

The proxy side, with the headers to set, is on Setup-Behind-a-Reverse-Proxy and in docs/TLS.md. For a proxy that cannot send the forwarded headers:

[server]
trusted_origins = ["https://webmail.example.com"]

login_key

Key Default Meaning
login_key none A Fernet key: 32 random bytes, written as base64 (standard or URL-safe) or as 64 hex characters. It encrypts the alps_login_token cookie, the stored passwords of linked accounts, and the separate CalDAV and CardDAV passwords users save in Settings. A value that does not decode to 32 bytes stops startup with Error generating options: invalid login key.

With login_key set:

  • Every sign-in also writes the alps_login_token cookie. When the in-memory session is gone, after a restart for example, the browser's next request opens a new session from the token, without a new sign-in.
  • "Stay signed in after closing the browser" keeps the cookies for 30 days. A token is accepted for at most 30 days after it was written.
  • Users can link accounts. Without the key, linking is refused.
  • Under Settings > Calendar and Settings > Contacts, users can save a separate username and password for a CalDAV or CardDAV server. Without the key, those connections can use only the mail username and password.

Without it, every restart signs everyone out.

Generate a key with:

openssl rand -base64 32
[server]
login_key = "paste-the-generated-value-here"
  • Give every node that serves the same hostname the same key (see Config-Cluster).
  • Keep it secret. It protects the passwords inside the login tokens, the linked accounts and the saved DAV connections, so make the configuration file readable only by ALPS's user and by administrators.
  • Changing the key signs everyone out, users must remove each linked account and add it again, and users must enter their separate CalDAV and CardDAV passwords again. Until they do, those connections fail. Change it only when that is what you want, for example to make every kept sign-in invalid at once.

Token lifetime, restores and linked accounts are covered on Sessions-and-Sign-in and in docs/SESSIONS.md.

dav_allow_private_servers

Key Default Meaning
dav_allow_private_servers false Lets the CalDAV and CardDAV servers that users enter in Settings use plain HTTP and loopback, private and other non-public addresses.

While the caldav or carddav plugin is loaded, Settings > Calendar and Settings > Contacts let each user enter a server of their own, a personal server, in place of the one this file sets (see Config-Plugins). Any signed-in user can enter one, so by default ALPS reaches personal servers only over HTTPS and only at public addresses. It refuses the same addresses as the remote-content proxy, when it connects and on every redirect, and uses no HTTP proxy for them (The Address Check). A mail account therefore cannot use the setting to reach services on the network ALPS runs in.

Set it to true when your users keep their own DAV servers on a trusted intranet:

[server]
dav_allow_private_servers = true

Personal URLs may then also use http+insecure://, caldav+insecure:// and carddav+insecure://, at any address, and HTTPS_PROXY, HTTP_PROXY and NO_PROXY apply to them. The servers set in [plugin.caldav], [plugin.carddav] and the multi backends are never restricted, whatever this key says. While it is off, a saved connection that needs it stops working, and Settings offers to remove it. In a cluster, give every node the same value. The rules for personal servers are in CDAV.md.

temp_dir

Key Default Meaning
temp_dir $TMPDIR, else /tmp The directory for uploaded attachments waiting to be sent or saved in a draft (multipart-* files, mode 0600) and for the BIMI logo cache (alps-avatars). ALPS sets TMPDIR to it for its own process at startup.
  • ALPS does not create the directory at startup. Until it exists, every upload whose files together exceed 32 KiB fails with 500 and the code upload_storage_failed, and ALPS logs upload multipart: ... at error level with the cause. Smaller uploads stay in memory and still work, so the problem shows only with real attachments. Create the directory beforehand, owned by ALPS's user and closed to others:
    install -d -o alps -g alps -m 700 /var/lib/alps/tmp
    [server]
    temp_dir = "/var/lib/alps/tmp"
  • The shipped systemd unit (dist/alps.service) sets PrivateTmp=true and ProtectSystem=strict. So a directory under the host's /tmp or /var/tmp does not exist for ALPS, and nothing outside them can be written. With that unit, either leave temp_dir unset, or point it outside /tmp and /var/tmp and list it in ReadWritePaths (see docs/OPERATIONS.md).
  • Leave room above max_global_attachment_mib. An upload's body is written to the directory before it counts against the attachment limits.
  • A crash or a kill leaves multipart-* files behind, and so can a shutdown, which waits up to 5 seconds in all for the sessions' cleanup. A running ALPS sweeps them: once an hour, the first time an hour after startup, it removes every multipart-* file at the top level of the temp directory that is more than 7 days old. It keeps the files of the uploads it still holds, however old. The sweep cannot tell ALPS's files from another program's files of the same name, which is one more reason to give ALPS a directory of its own. No upload survives a restart, so while ALPS is stopped, every multipart-* file in its own temp directory is a leftover and can be deleted. Under the shipped systemd unit with temp_dir unset, PrivateTmp=true has systemd remove them when the service stops.

Session lifetimes

Key Default Meaning
session_minutes 30 The server's idle window for an account whose saved settings hold no Auto-logout time. A session that gets no request for this long ends (session ended, reason idle).
max_session_minutes 0 (no cap) The cap on a user's Auto-logout choice. A longer choice, "Never" included, is lowered to it (1440 caps it at 24 hours). Without a cap, "Never" means one year. 0 or a negative value means no cap. It does not cap session_minutes.
absolute_session_hours 168 (7 days) The longest a session lives, however much it is used (reason max-age). It is never shorter than session_minutes. 0 keeps the default, and a negative value, such as -1, removes the limit.
  • Every request resets the idle window, including the requests an open page makes by itself, such as its checks for new mail. So while a page is open, the server's idle window does not end its session: the page's own auto-logout (next point) or absolute_session_hours does.
  • session_minutes is also the Auto-logout time of an account that has not picked one. Settings shows it, as an extra choice when it is not one of the listed times (15, 30, 60, 120 or 360 minutes), and an open page signs the user out after that many minutes with no activity in any open tab. Saving other settings leaves it unset, so a later change of session_minutes reaches the account. Once the user picks an Auto-logout time, that time replaces session_minutes for the account, capped by max_session_minutes.
  • With login_key set, a session that ends at one of these limits is opened again from the login token on the browser's next request. The user signs in again only when the browser no longer holds a valid token: it is more than 30 days old, the browser dropped it on closing (without "Stay signed in after closing the browser"), or the user signed out. The page's own auto-logout counts as signing out. See Sessions-and-Sign-in for how long a token stays valid.
[server]
session_minutes = 30
max_session_minutes = 1440      # "Never" and longer choices become 24 hours
absolute_session_hours = 168

How the page's idle timer and the server's window work together is on Sessions-and-Sign-in.

Session caps and the background connection

Key Default Meaning
max_sessions 10000 Sessions on one node, all users together. At the cap a sign-in is not refused. The least recently used session is ended instead (reason evicted). 0 keeps the default, and a negative value, such as -1, turns the cap off.
max_sessions_per_user 10 Sessions of one user on one node. At the cap, that user's least recently used session is ended. 0 keeps the default, and a negative value, such as -1, turns the cap off.
background_connection true Each session opens a second mail connection when it first needs one, for work the user is not waiting for: checks for new mail, sender verdicts, the search of Sent for a conversation's replies, draft saves, and the messages a conversation reads ahead. ALPS closes it after a minute without use. false keeps one connection per session.
  • A session holds up to two mail connections, so one user's sessions on a node can hold up to twice max_sessions_per_user, all from ALPS's address. Mail servers limit the connections of one user from one address (Dovecot: mail_max_userip_connections, 10 by default), so size the two settings together. If the mail server refuses the second connection, the work runs on the session's own connection and only speed suffers, except that with push_new_mail that session gets no push. See Mail-Server-Requirements.
  • With login_key set, a browser whose session was ended at a cap opens a new session from its token at its next request, and that can end another session of the same user. Set max_sessions_per_user above the number of browsers and devices a user has open at once.
  • A cap turned off no longer bounds the memory and mail connections the sessions hold. Keep both caps on for a server that users reach over a network, and raise them rather than turn them off.
[server]
max_sessions = 1000
max_sessions_per_user = 5

HTTP timeouts

Key Default Meaning
read_timeout_sec 10 The time allowed to read a whole request, headers and body. An attachment upload must arrive within it.
write_timeout_sec 60 The time from the end of a request's headers to the end of its response. It covers an upload's body, the handling and the response together, and so also downloads and long answers. config.example.toml sets 30. Keep it at least as long as read_timeout_sec.
idle_timeout_sec 120 How long a keep-alive connection waits for its next request.
smtp_timeout_sec 30 Bounds opening the connection (the TCP connect, with the TLS handshake for smtps://), then for smtp:// the greeting, EHLO and STARTTLS together, and after that each SMTP command and the wait after the message data. See Config-Providers-and-SMTP.
imap_timeout_sec unset An older name for [provider] timeout_sec (default 30). It is read only with [provider] type = "imap" and only when timeout_sec is unset or 0. Set [provider] timeout_sec instead.
  • 0 or a negative value keeps each default.
  • An upload slower than read_timeout_sec fails with 408 and the code upload_timed_out, and ALPS logs upload multipart: ... with the cause. For large attachments over slow links, raise read_timeout_sec and write_timeout_sec, or put a proxy in front that buffers request bodies.
  • On HTTP/1.1, a request whose handler is still running when read_timeout_sec passes is cancelled. Its mail operation then gets up to 10 more seconds before the session's mail connection is let go. If long operations on a slow mail server are cut off, raise read_timeout_sec.
  • The event stream (GET /events) lifts both the read and the write deadline for itself, so these timeouts do not cut it.

For a slow network or large attachments, config.example.toml suggests:

[server]
read_timeout_sec = 30
write_timeout_sec = 120
smtp_timeout_sec = 60

[provider]
timeout_sec = 60

Attachment limits

Key Default Meaning
max_attachment_mib 32 The upper bound on the uploads one session holds for one composer window. The mail server's own message limits can lower it (see below). Each upload request is also capped at the limit in force plus 1 MiB.
max_session_attachment_mib 128 The uploads one session holds across all its composer windows. When an upload would go over it, the session's oldest uploads are dropped to make room.
max_global_attachment_mib 1024 The uploads all sessions on one node hold. When an upload would go over it, the oldest uploads on the node, other users' included, are dropped first.

The mail server's message limits lower the composer limit for an account when they leave less room than max_attachment_mib:

  • ALPS reads the IMAP APPENDLIMIT the server advertises, for the whole server or in the STATUS of the Drafts and Sent folders, and the SMTP SIZE the submission server advertises. It probes SIZE without signing in and keeps the answer for 10 minutes in each session.
  • From the smallest of these limits, it keeps 64 KiB for the headers and the body and allows for base64 encoding. A 25 MiB message limit leaves about 18 MiB for attachments. The composer gets the result, in MiB with a fraction, from GET /session and GET /settings, and checks the message's attachments against it before uploading.
  • Before it sends, ALPS checks the whole encoded message against the limits it knows, the Sent copy included. A message that is too large, or that the mail server refuses as too large (IMAP TOOBIG, SMTP 552), is answered 413 message_too_large. The composer keeps its contents and asks the user to shorten the message or remove attachments.
  • A server that advertises APPENDLIMIT=0 leaves no room for attachments, and sends are refused before they reach SMTP, so that no message is sent without its Sent copy.

So max_attachment_mib above the mail server's limits has no effect. Without advertised limits, it is the limit.

What an upload (POST /attachments) gets:

  • 413 when the request body is over the composer limit in force plus 1 MiB. ALPS stops reading the body there.
  • 400 Your attachments exceed the maximum file size. Remove some and try again. when the upload would take its composer window over the composer limit, or when the files of one request together exceed any of the three limits.
  • 408 upload_timed_out when the body does not arrive within read_timeout_sec, and 500 upload_storage_failed when it cannot be written to the temp directory (see temp_dir).
  • 200 otherwise, with older uploads dropped first when the session or node limit requires it. A composer that lost an upload this way gets 409 missing_attachments at its next save or send, and asks the user to add those files again.

0 or a negative value keeps the default, so none of the limits can be turned off. Uploads live in the memory and temp directory of the node that received them, and a restart loses them. Set the reverse proxy's request-body limit to at least max_attachment_mib + 1 MiB, and no less than 32 MiB, the largest message body ALPS accepts. nginx's default client_max_body_size 1m refuses every upload over 1 MiB. Details are in docs/ATTACHMENTS.md.

[server]
max_attachment_mib = 25
max_session_attachment_mib = 100
max_global_attachment_mib = 4096

New-mail push

Key Default Meaning
push_new_mail false While a mail page is open, its session waits in IMAP IDLE on its background connection, and tells the page over an event stream (GET /events) as soon as a message arrives in the Inbox or leaves it. Without push, pages check only at the user's check-mail interval. The checks keep running with push on too, for flags and the other folders.
push_subscribed_folders false With push_new_mail on and a mail server with NOTIFY (RFC 5465), the session also asks for the news of the subscribed folders, on the same connection, and pushes it as it pushes the Inbox's. The pages' own checks then run a third as often (every 15 minutes at the default 5). Without NOTIFY, or where the server refuses it, the Inbox alone is pushed.

Push needs background_connection on, a mail server with IDLE, a browser in a secure context (HTTPS, or localhost), a reverse proxy that does not buffer /events and keeps it open, and, in a cluster, routing to the node that holds the session. Features explains each requirement and how to check that push works. When a key it depends on is off, ALPS says so at startup:

  • push_new_mail without background_connection logs push_new_mail needs background_connection, which is off: new mail is not pushed, and GET /events answers 404 new mail is not pushed.
  • push_subscribed_folders without push_new_mail logs push_subscribed_folders needs push_new_mail, which is off: no news of the folders is pushed.

What it costs: every session with a mail page open holds two mail connections (its own and the background one in IDLE) for as long as the page is open, all from ALPS's one address. Check the mail server's per-user and per-address connection limits before turning it on. With push_subscribed_folders, the mail server also watches every subscribed folder of every open page. docs/CONFIGURATION.md has the numbers.

[server]
push_new_mail = true
push_subscribed_folders = true

Change numbers (CONDSTORE)

Key Default Meaning
condstore false Each mail connection turns on IMAP CONDSTORE (RFC 7162) where the server lists it. When a folder still holds the messages a cached page was read with, the next listing reads again only the flags that changed (one FETCH with CHANGEDSINCE), so a star or a read made in another client shows at the next check.
  • A server that does not list CONDSTORE is used as with condstore = false. ALPS never turns on QRESYNC.
  • The cost is one round trip as each connection signs in.
  • With it on, GET /mailboxes/{mbox}/status also answers highestmodseq, as a decimal string, for example "highestmodseq":"2".
  • Without it, a flag change made in another client that leaves the unread count as it was, such as a star, shows only when the cached page expires ([cache] ttl_minutes, see Config-Logging-and-Cache).
[server]
condstore = true

docs/CONFIGURATION.md describes each step.

A complete example

One node behind nginx on the same host, which terminates TLS:

[server]
addr = "127.0.0.1:1323"
trusted_proxies = ["127.0.0.1", "::1"]
login_key = "paste-the-output-of-openssl-rand-base64-32"
temp_dir = "/var/lib/alps/tmp"

max_session_minutes = 1440
absolute_session_hours = 168
max_sessions = 10000
max_sessions_per_user = 10

read_timeout_sec = 30
write_timeout_sec = 120

max_attachment_mib = 32

push_new_mail = true
condstore = true

What to check after a change:

  • The log shows Starting server on 127.0.0.1:1323, and no Unknown configuration key warning. A value of the wrong type, an invalid trusted_proxies entry or an invalid login_key exits with status 1, with a line on standard error that starts with Error loading config file: or Error generating options:. See docs/OPERATIONS.md for the other startup failures.
  • Through the proxy, the sign-in's Set-Cookie headers carry Secure.
  • The access log's ip is the browser's address, not the proxy's.
  • An attachment of a few MiB uploads. If it fails, check the proxy's body limit and that temp_dir exists.

More for production is on Running-in-Production.

Related pages

Clone this wiki locally