Files
internal-mail-manager/README.md
T
2026-03-29 19:29:36 +02:00

175 lines
6.7 KiB
Markdown

# 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/<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](/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.