Your question
I tested Bulwarkmail webmail client and it works really well with Stalwart. Love how I have emails, calendars and contacts in one place. Works fast with JMAP.
I do have a question that I hope some of you may help me with.
My setup:
Server A
I have Stalwart installed in one server. I use that server only for email. Nothing else. Example IP: 123.456
Server B
I have Nginx Proxy Manager. There I have setup Bulwarkmail in docker compose and it works perfectly with Nginx Proxy Manager. No need to open ports (setup environment).
Example IP: 678.910
All works well when I have Permissive CORS policy enabled in Stalwart. But when I disable Permissive CORS policy, then I get an error.
For security reasons, and if I can avoid it, I don’t want to enable Permissive CORS policy in Stalwart.
Here is my docker compose file in Nginx Proxy Manager server
services:
bulwark:
image: Package webmail · GitHub
container_name: bulwark-webmail
networks:
- proxy
environment:
- PORT=3000
- JMAP_SERVER_URL=https://mail.domain.com
restart: unless-stopped
networks:
proxy:
external: true
Any suggestions? Thanks!
This is a classic cross-origin scenario: Bulwarkmail is served from Server B (a different origin than your Stalwart instance on Server A), so the browser enforces CORS and blocks the requests without the permissive header.
The current state of Stalwart’s CORS setting is all-or-nothing. The usePermissiveCors flag (in the WebUI under Settings › Network › HTTP › Security) either allows all origins (true) or none (false). There is no built-in way to allowlist a specific origin like https://bulwark.yourdomain.com.
Your practical options are:
-
Enable usePermissiveCors and accept the trade-off. The docs specifically note this is the intended use case (“useful when the Stalwart server is managed through a WebUI hosted on a different domain”). The risk is mitigated by the fact that all JMAP endpoints already require authentication — an attacker’s malicious page can’t do anything without valid credentials. This is the simplest path.
-
Proxy Bulwarkmail through the same origin as Stalwart. If you reverse-proxy Bulwarkmail so it appears to the browser to be served from mail.domain.com (e.g., at mail.domain.com/webmail/), the browser sees it as same-origin and CORS is never triggered. You could do this with an additional proxy pass rule in Nginx Proxy Manager pointing to Server B’s Bulwark container.
-
Terminate CORS at your Nginx reverse proxy. Nginx Proxy Manager can be configured to inject Access-Control-Allow-Origin: https://bulwark.yourdomain.com (and the other required preflight headers) into responses from Stalwart. This gives you origin-specific CORS without touching Stalwart’s setting. This is a standard Nginx add_header / proxy_hide_header pattern, though it’s outside Stalwart’s own docs.
Option 2 (same-origin proxy) or option 3 (Nginx-level CORS header injection) are the cleanest approaches if you want usePermissiveCors to stay false. Option 1 is safe in practice as long as authentication is enforced on every endpoint (which it is by default in Stalwart).
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.
So to quote what AI always says… “This is a classic issue…”
Stalwart, to the best of my knowledge, has two options for CORS: Permissive and Blocked. Basically, it’s an all-or-nothing approach. You either have to allow ‘*’ everything, or allow nothing, which is very surprising to me.
The main issues I read about during the Stalwart v0.15 to v0.16 migration mostly revolved around obtaining and maintaining connectivity to the frontend and OID/OAuth issues. Most of the frontend issues seemed to involve people like myself who use a reverse proxy of some sort and the proper configuration of CORS, Proxy Protocol, and Proxy Header Forwarding.
I believe adding a new feature to allow defining networks for CORS is in order and would help quite a few people like yourself who do not want to just open CORS up (removing the benefit it provides) and can’t just turn it off because they are using a reverse proxy and the Origin is never going to be the same (Bulwark to Stalwart, for example).
I wish there was a way for Stalwart to allow CORS only from one domain. say webmail.example.com (the one that is in Server B).
I stopped Stalwart, edited the config.toml file and restarted and no luck. I am using v.15. I added this to the config.toml
Add to confirmg.toml
[server.http]
cors.allowed-origins = [“https://webmail.example.com”]
cors.allowed-methods = [“GET”, “POST”, “OPTIONS”, “PUT”, “DELETE”]
cors.allowed-headers = [“Content-Type”, “Authorization”, “X-JMAP-Framework”]
cors.allow-credentials = true
Didn’d work.
I really don’t want to allow all ‘*’ but if I have to I have no other choice. Is it okay to leave it all? Are you all using it that way?
Thanks!
Found the solution. This worked for me thanks to Gemini AI:
-
Log in to your Stalwart Admin UI.
-
Navigate to Settings > Network > HTTP.
-
Find the Security or CORS section.
-
Disable “Permissive CORS”: If there is a toggle for “Permissive CORS policy,” turn it OFF. (Permissive mode often uses the wildcard *, which causes the block).
-
Add Custom Response Headers: Look for the Response Headers or Custom Headers field and add the following:
-
Access-Control-Allow-Origin: https://your-bulwark-domain.com
-
Access-Control-Allow-Credentials: true
-
Access-Control-Allow-Methods: GET, POST, OPTIONS, DELETE, PUT
-
Access-Control-Allow-Headers: Authorization, Content-Type, X-JMAP-Prefix