Webmail (Bulwark)
larakube webmail:init deploys Bulwark — a modern, JMAP-native
webmail client built specifically for Stalwart — into the larakube-shared
namespace. It's the browser-based mailbox your team uses, the same way chat:init gives them
a webmail UI. Stalwart itself ships without webmail (its own web UI is for administration),
so this is what turns "you need Apple Mail / Thunderbird" into "just open a URL".
Why Bulwark (and not Roundcube / SnappyMail)
The webmail question is really "IMAP client vs JMAP client":
| Protocol to Stalwart | Extra storage | Fit | |
|---|---|---|---|
| Bulwark ✅ | JMAP — Stalwart's native, modern protocol | A small config PVC — no database | Built for Stalwart; headless deploy; OIDC support that plugs into sso:wire. |
| Roundcube | IMAP + SMTP | Needs a database (SQLite/Postgres/MySQL) | Mature, but everything it does well is generic to IMAP servers; against a JMAP-native Stalwart it needs a TLS-cert workaround for the in-cluster connection. |
| SnappyMail | IMAP + SMTP | Filesystem prefs | Lightweight, but IMAP-only — ignores what Stalwart does best. |
Bulwark wins on native protocol (one HTTP endpoint, no IMAPS-cert gymnastics), zero extra datastore, and first-class OIDC.
Bulwark is AGPL-3.0. LaraKube CLI runs the official unmodified image, so the copyleft trigger (modify + redistribute as a service) doesn't apply — same posture as Zitadel.
larakube webmail:init
larakube webmail:init [environment] [options]
| Option | Description |
|---|---|
environment | local (default) or a cloud env such as production. |
--domain= | Raw domain override → webmail.<domain> (skips the host prompt). |
--app-name= | Branding shown on the login/app (default: Webmail). |
--context= | Target a specific kube-context. |
--vpn-only | Restrict access behind NetBird VPN IP whitelisting. |
--remove | Tear down Bulwark (Deployment, Service, ingress, PVC, secret). |
webmail:init refuses to deploy unless Stalwart is installed and a mail host is configured
for the environment — Bulwark is a client for Stalwart, so there's nothing to point it at
otherwise. Run mail:init first.
It deploys Bulwark configured headlessly (no setup wizard) — JMAP_SERVER_URL is pointed at
your public Stalwart host (https://mail.<domain>), a generated session secret is synced,
and a small PVC persists Bulwark's own admin/settings config across restarts.
Enabling Permissive CORS in Stalwart
Bulwark's browser interface communicates directly with Stalwart's JMAP API (cross-origin from https://webmail.<domain>). To allow logins:
- Open Stalwart Admin (
https://mail.<domain>/admin). - Navigate to Network → HTTP → Security.
- Toggle Permissive CORS policy to ON.
- Click Save in the bottom-right corner of the page.
- Apply the updated store configuration by restarting Stalwart:
larakube mail:restart <env>
Stalwart's toggle is * (any origin). This is safe specifically because JMAP authentication uses Bearer/Basic headers rather than cookies, so cross-origin requests cannot execute unauthorized operations.
Bulwark Admin Portal (v1.7.8+)
Bulwark v1.7.8+ includes its own dedicated Admin Portal located at https://webmail.<domain>/admin:
- Admin Credentials: The admin password is automatically generated, stored in Kubernetes secret
webmail-secrets, and synced to Infisical asWEBMAIL_ADMIN_PASSWORD. - Custom Branding: Pre-configure organization logos, themes, and global default signatures for all team members.
- Session Control: Audit and revoke active webmail sessions across the organization.
Logging in
By default, team members log in at https://webmail.<domain> using the mailbox address + password generated via mail:create. Bulwark securely authenticates directly with Stalwart over JMAP. Once installed, mail:create and mail:show <email> display the webmail URL alongside IMAP/SMTP credentials.
Single sign-on (optional)
If you've deployed Zitadel (sso:init), you can put webmail behind SSO:
larakube sso:wire webmail
This registers Bulwark as an OIDC client in Zitadel and patches its OAUTH_* env — it adds
an SSO button alongside the password login (it doesn't disable basic auth, so mailbox-password
and Apple Mail / Thunderbird keep working).
Bulwark logging a user in via Zitadel isn't enough on its own: Bulwark then hands the Zitadel
token to Stalwart over JMAP, and Stalwart rejects it unless Stalwart itself has an OIDC
directory pointing at the same Zitadel. sso:wire webmail prints the exact step and does not
auto-apply it, because that Stalwart change (storage.directory) switches its primary auth
backend and, done wrong, breaks every existing password/IMAP/SMTP login.
In Stalwart admin (Settings → Authentication → Directories) add an OIDC directory using
the userinfo endpoint https://sso.<domain>/oidc/v1/userinfo, mapping email → email and
username → preferred_username. Keep your existing password directory as the primary and add
OIDC alongside it — don't replace it.
Bulwark's Next.js callback can carry a locale prefix (/en/auth/callback, …) that varies by
browser. sso:wire registers the no-prefix https://webmail.<domain>/auth/callback; if your
browser adds a locale segment, register that variant in Zitadel too.
VPN-only access
webmail:init --vpn-only (or vpn:wire webmail after the fact) restricts the
webmail ingress to NetBird VPN peers via a Traefik ipAllowList middleware — the same mechanism
every other --vpn-only tool uses.