2026-03-29 19:29:36 +02:00
.
2026-03-29 19:29:36 +02:00
2026-03-29 18:45:25 +02:00
2026-03-29 18:45:25 +02:00
2026-03-29 18:45:25 +02:00
2026-03-29 18:45:25 +02:00
2026-03-29 18:45:25 +02:00
2026-03-29 19:21:17 +02:00
2026-03-29 18:45:25 +02:00
2026-03-29 18:45:25 +02:00
.
2026-03-29 19:29:36 +02:00
2026-03-29 18:45:25 +02:00

Secure Webmail for docker-mailserver

A full-stack webmail application built with Bun, ElysiaJS, React, and Vite. The app is designed for public-internet exposure and treats docker-mailserver as an untrusted integration boundary that must be wrapped carefully.

Stack

  • Backend: Bun + ElysiaJS
  • Frontend: React + Vite running on Bun
  • Storage: Bun SQLite for app metadata, sessions, user preferences, and encrypted TOTP secrets
  • Mail integration: Docker CLI with fixed argument arrays only

Security-first design

Critical risks and mitigations

Risk Mitigation
Command injection through Docker CLI All Docker calls use Bun.spawn() with explicit argument arrays. No shell string concatenation, no sh -c, no raw user input in command strings.
Session theft or token leakage Server-side opaque sessions in httpOnly, SameSite=Strict cookies. The frontend never stores session identifiers in localStorage.
CSRF Every state-changing route requires an in-memory CSRF token in X-CSRF-Token plus the authenticated cookie.
XSS from HTML email HTML email is sanitized on the backend, dangerous tags are removed, remote images are not allowed, and the frontend renders sanitized HTML only in a sandboxed iframe.
Brute-force login Dedicated login rate limiter plus a global limiter.
Unsafe file uploads Avatar uploads are size-limited, magic-byte checked, renamed with random tokens, stored outside the public root, and served through an authenticated endpoint.
Forwarding abuse Forwarding targets are strictly validated, self-forwarding is blocked, and allowed target domains are policy-controlled.
Secret exposure No passwords or TOTP secrets are logged. TOTP secrets are encrypted at rest with AES-GCM via TOTP_ENCRYPTION_KEY.
Information leakage API errors return generic messages for internal failures and use explicit HTTP status codes for validation and auth issues.

Why server-side sessions instead of JWTs

This project uses secure server-side sessions instead of bearer tokens or long-lived JWTs. That keeps revocation simple, reduces token exposure risk in the browser, and avoids putting sensitive auth state in JavaScript storage.

Features

  • Mailbox login against docker-mailserver via doveadm auth test
  • Optional seeded local admin account with Argon2id-hashed password
  • Inbox listing and message preview from Maildir inside the mail container
  • Plain-text compose flow using sendmail inside the container
  • Safe HTML email preview
  • Profile settings, avatar upload, and forwarding management
  • Optional TOTP two-factor authentication
  • Security headers and restricted CORS

Project layout

apps/
  api/          Elysia backend
  web/          React/Vite frontend
packages/
  shared/       Shared TypeScript types
infra/
  nginx/        Reverse proxy example with security headers
data/
  avatars/      Stored outside the public web root

Local development

1. Configure environment

Copy .env.example to .env and update the values:

  • HOSTED_DOMAINS: domains served by docker-mailserver
  • MAILSERVER_CONTAINER: container name to run docker exec against
  • APP_ORIGINS: allowed frontend origins
  • TOTP_ENCRYPTION_KEY: base64-encoded 32-byte key
  • APP_ADMIN_EMAIL / APP_ADMIN_PASSWORD: optional local admin bootstrap account

Generate a 32-byte base64 key:

openssl rand -base64 32

2. Install dependencies

If Bun needs an explicit temp dir on your machine:

BUN_TMPDIR=/tmp bun install

3. Start the apps

Backend:

bun run --cwd apps/api dev

Frontend:

bun run --cwd apps/web dev

The Vite dev server proxies /api to http://localhost:3001.

Important en local:

  • le compte admin local est créé au démarrage de l'API
  • il faut donc avoir un fichier .env à la racine du repo
  • et redémarrer l'API après tout changement de APP_ADMIN_EMAIL ou APP_ADMIN_PASSWORD

Docker CLI assumptions

The backend assumes docker-mailserver is available in a running container and that the following commands are present:

  • doveadm auth test for mailbox authentication
  • find and cat for safe Maildir reads
  • sendmail for sending plain-text messages
  • setup alias add|del for forwarding aliases

The mailbox reader currently targets the standard Maildir layout:

/var/mail/<domain>/<local-part>/Maildir

If your container layout differs, update apps/api/src/docker/mailserver.ts.

Production deployment guidance

Reverse proxy

Use a reverse proxy such as Nginx or Traefik in front of both the frontend and API.

  • Terminate TLS at the proxy
  • Forward only /api to the API container
  • Serve the frontend over HTTPS only
  • Strip hop-by-hop headers
  • Preserve the real client IP only if you trust the proxy chain

A hardened Nginx example lives at infra/nginx/default.conf.

Container and network hardening

  • Run the web and API containers as non-root users
  • Mount only the minimum filesystem paths required
  • Put docker-mailserver on an internal Docker network
  • Do not expose unnecessary internal ports
  • Restrict who can access the Docker socket; ideally expose a tightly scoped Docker API proxy instead of mounting the raw socket directly
  • Set TRUST_PROXY=true only behind a trusted reverse proxy

TLS and browser protections

  • Enforce HTTPS everywhere
  • Keep Strict-Transport-Security enabled in production
  • Preserve X-Frame-Options: DENY
  • Preserve CSP, X-Content-Type-Options: nosniff, and Referrer-Policy: no-referrer
  • Avoid adding third-party scripts unless you also update CSP intentionally

Secret handling

  • Store secrets in environment variables or a secrets manager
  • Rotate TOTP_ENCRYPTION_KEY carefully; rotating it invalidates stored TOTP secrets unless you build a migration path
  • Never log plaintext credentials
  • Treat Docker access as privileged infrastructure access

Verification

Verified locally:

  • bun run typecheck in packages/shared
  • bun run typecheck in apps/api
  • bun run typecheck in apps/web
  • bun run build in apps/api
  • bun run build in apps/web

Residual risks and next steps

  • Direct Docker socket access remains a high-value target. The safest next step is replacing raw socket exposure with a restricted sidecar or service account boundary.
  • Maildir parsing is intentionally conservative and currently optimized for INBOX. If you need full IMAP semantics, move message access behind a dedicated service abstraction instead of expanding ad hoc container reads.
  • Forwarding rules are managed locally and then applied to docker-mailserver. If you need full bidirectional sync with existing aliases, add an audited reconciliation job.
S
Description
No description provided
Readme
169 KiB
Languages
TypeScript 78.9%
CSS 20.1%
HTML 0.5%
JavaScript 0.5%