Can't access UI after setting up x-forwarded-for

Issue Description

Just upgraded from 0.15.5 to 0.16.4. The upgrade process went smoothly, but then I configured the proxy (stalwart http running behind caddy reverse proxy, but SMTP/IMAP ports are exposed directly). I cannot access the admin neither through browser or through stalwart-cli, both report errors:

2026-05-10T17:24:26Z WARN X-Forwarded-For header is missing (http.x-forwarded-missing) listenerId = “https”, localPort = 443, remoteIp = 127.0.0.1, remotePort = 50740
2026-05-10T17:24:44Z WARN Proxy protocol error (network.proxy-error) listenerId = “http”, localIp = ::, localPort = 8080, tls = false, reason = “invalid proxy header”
2026-05-10T17:24:44Z WARN Proxy protocol error (network.proxy-error) listenerId = “http”, localIp = ::, localPort = 8080, tls = false, reason = “invalid proxy header”

The system is using RocksDB as a backend.

Is there a way to revert configuration and enable the admin on port 8080?

Expected Behavior

The x-forwarded-for feature should be optional and the system should work without it, IMO.

At the very least, there should be a way to reconfigure a misconfigured system without web ui.

Actual Behavior

I can not access the admin UI; the stalwart-cli does not work (produces the same error). When accessing through caddy reverse proxy, the proxy return 502 because stalwart resets the connection (read: connection reset by peer).

Reproduction Steps

  1. Start stalwart with configured trusted network for reverse proxy (172.18.0.0/24 in my case)
  2. Observe the error and inability to connect to server.

Relevant Log Output

2026-05-10T17:29:27Z WARN X-Forwarded-For header is missing (http.x-forwarded-missing) listenerId = “https”, localPort = 443, remoteIp = 127.0.0.1, remotePort = 51490
2026-05-10T17:29:57Z WARN X-Forwarded-For header is missing (http.x-forwarded-missing) listenerId = “https”, localPort = 443, remoteIp = 127.0.0.1, remotePort = 33042
2026-05-10T17:30:02Z WARN Proxy protocol error (network.proxy-error) listenerId = “https”, localIp = ::, localPort = 443, tls = true, reason = “invalid proxy header”
2026-05-10T17:30:02Z WARN Proxy protocol error (network.proxy-error) listenerId = “https”, localIp = ::, localPort = 443, tls = true, reason = “invalid proxy header”
2026-05-10T17:30:27Z WARN X-Forwarded-For header is missing (http.x-forwarded-missing) listenerId = “https”, localPort = 443, remoteIp = 127.0.0.1, remotePort = 59846
2026-05-10T17:30:57Z WARN X-Forwarded-For header is missing (http.x-forwarded-missing) listenerId = “https”, localPort = 443, remoteIp = 127.0.0.1, remotePort = 54750
2026-05-10T17:31:27Z WARN X-Forwarded-For header is missing (http.x-forwarded-missing) listenerId = “https”, localPort = 443, remoteIp = 127.0.0.1, remotePort = 36364
2026-05-10T17:31:57Z WARN X-Forwarded-For header is missing (http.x-forwarded-missing) listenerId = “https”, localPort = 443, remoteIp = 127.0.0.1, remotePort = 47418

Stalwart Version

v0.16.x

Installation Method

Docker

Database Backend

RocksDB

Blob Storage

RocksDB

Search Engine

Internal

Directory Backend

Internal

Additional Context

Stalwart running behind caddy reverse proxy, which proxies to port 443, volumes are bind-mounted:

volumes:
  - ./data:/var/lib/stalwart
  - ./config:/etc/stalwart
  - ./logs:/var/log/stalwart

I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.

on

I have searched this support forum (open and closed topics) and confirm this is not a duplicate.

on

I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.

on

There are two separate problems here, and there’s a clear recovery path for both.


What’s happening

You have a configuration conflict between two IP-forwarding mechanisms:

  1. The https listener on port 443 is configured with proxyTrustedNetworks (or similar) and expects X-Forwarded-For headers, but Caddy isn’t sending them (or they’re not being accepted), hence the X-Forwarded-For header is missing warning.
  2. The http listener on port 8080 appears to be configured to expect Proxy Protocol (the TCP-level header), but Caddy is sending plain HTTP, hence invalid proxy header. This is what kills the connection and blocks both browser and stalwart-cli access.

The two mechanisms are not interchangeable on the same listener – you can only use one per listener.


How to recover access right now (Docker)

Since your volumes are bind-mounted, use Recovery Mode to regain access to the admin UI:

  1. Add two environment variables to your Docker Compose file:
    environment:
      - STALWART_RECOVERY_MODE=1
      - STALWART_RECOVERY_ADMIN=admin:yourTemporaryPassword
    
  2. Restart the container: docker compose restart stalwart
  3. Access http://127.0.0.1:8080/admin directly (bypassing Caddy) using those credentials. Recovery mode starts only the HTTP management listener on port 8080, with no proxy filtering applied.
  4. Fix your listener configuration (see below), then restart without the recovery env vars.

What to fix in the configuration

For a Caddy + HTTP upstream setup (Caddy terminates TLS, forwards plain HTTP to Stalwart on port 8080), the correct approach is:

  • On the http listener (port 8080): set useXForwarded: true on the Http settings object and make sure proxyTrustedNetworks includes Caddy’s internal IP (e.g. 172.18.0.0/24). Do not enable Proxy Protocol on this listener.
  • On the https listener (port 443): if Caddy is doing TCP passthrough to this port, use Proxy Protocol (and configure Caddy to emit it). If Caddy is terminating TLS and forwarding HTTP, route to port 8080 instead.

The docs note: “A single listener should be configured for one of these mechanisms; Stalwart cannot decode both on the same connection.”

For a complete Caddy-specific example, see the Reverse proxy overview and the Proxy Protocol docs. The recovery mode docs cover the STALWART_RECOVERY_MODE / STALWART_RECOVERY_ADMIN variables in full.

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong. If you found it useful, mention @helpbot in any reply to ask follow-up questions and the bot will respond again.

A very helpful hint was that configuring trusted network actually enables proxy protocol.

I don’t see where the trusted networks are configured (for non proxy protocol reverse proxy).

Additional gotcha was from the upgrade docs: http://…:8080 is no longer the right URL for day-to-day administration. Port 8080 carries the recovery / bootstrap HTTP listener and is intended for the migration window; once the server is running normally it stops being a valid sign-in entry point

But, apparently, if running behind reverse proxy (without proxy protocol v2), the correct way to configure it is to upstream to port 8080, not to 443 (as before in v0.15.5).