Skip to main content

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 StalwartExtra storageFit
BulwarkJMAP — Stalwart's native, modern protocolA small config PVC — no databaseBuilt for Stalwart; headless deploy; OIDC support that plugs into sso:wire.
RoundcubeIMAP + SMTPNeeds 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.
SnappyMailIMAP + SMTPFilesystem prefsLightweight, 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.

License

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]
OptionDescription
environmentlocal (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-onlyRestrict access behind NetBird VPN IP whitelisting.
--removeTear 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:

  1. Open Stalwart Admin (https://mail.<domain>/admin).
  2. Navigate to Network → HTTP → Security.
  3. Toggle Permissive CORS policy to ON.
  4. Click Save in the bottom-right corner of the page.
  5. Apply the updated store configuration by restarting Stalwart:
    larakube mail:restart <env>
Permissive CORS is all-origins

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 as WEBMAIL_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).

SSO is two-sided — Stalwart must also trust Zitadel

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.