# 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 ```text 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: ```bash openssl rand -base64 32 ``` ### 2. Install dependencies If Bun needs an explicit temp dir on your machine: ```bash BUN_TMPDIR=/tmp bun install ``` ### 3. Start the apps Backend: ```bash bun run --cwd apps/api dev ``` Frontend: ```bash 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: ```text /var/mail///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](/home/kisakay/Documents/GitLab/internal-mail-server/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.