re-vibecode
This commit is contained in:
@@ -1,174 +1,191 @@
|
||||
# Secure Webmail for docker-mailserver
|
||||
# Docker Mailserver User Management Panel
|
||||
|
||||
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.
|
||||
A small full-stack admin tool for managing Docker Mailserver accounts with Bun, ElysiaJS, and React.
|
||||
|
||||
## Stack
|
||||
## 1. Proposed 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
|
||||
### Backend
|
||||
|
||||
## Security-first design
|
||||
- Bun
|
||||
- ElysiaJS
|
||||
- Bun SQLite
|
||||
- Docker CLI through fixed argument arrays
|
||||
- Zod for small, explicit validation
|
||||
|
||||
### Critical risks and mitigations
|
||||
Why this backend:
|
||||
|
||||
| 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. |
|
||||
- Bun keeps the runtime, package manager, and SQLite support simple.
|
||||
- ElysiaJS is a good fit for a small internal API without extra framework weight.
|
||||
- SQLite is enough for admin sessions and password reset tokens, and keeps deployment straightforward.
|
||||
- Docker CLI integration stays close to Docker Mailserver's real operational model instead of hiding it behind unnecessary layers.
|
||||
|
||||
### Why server-side sessions instead of JWTs
|
||||
### Frontend
|
||||
|
||||
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.
|
||||
- React
|
||||
- Vite
|
||||
- Plain CSS
|
||||
|
||||
## Features
|
||||
Why React:
|
||||
|
||||
- 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
|
||||
- It is easy for most full-stack teams to read and maintain.
|
||||
- This panel only needs basic local state and forms, so React works well without routers or global state libraries.
|
||||
- The UI stays minimal while still being easy to grow later.
|
||||
|
||||
## Project layout
|
||||
## 2. Folder structure
|
||||
|
||||
```text
|
||||
apps/
|
||||
api/ Elysia backend
|
||||
web/ React/Vite frontend
|
||||
api/
|
||||
src/
|
||||
auth/
|
||||
docker/
|
||||
lib/
|
||||
mail/
|
||||
users/
|
||||
web/
|
||||
src/
|
||||
api/
|
||||
components/
|
||||
pages/
|
||||
styles/
|
||||
packages/
|
||||
shared/ Shared TypeScript types
|
||||
infra/
|
||||
nginx/ Reverse proxy example with security headers
|
||||
shared/
|
||||
data/
|
||||
avatars/ Stored outside the public web root
|
||||
app.db
|
||||
infra/
|
||||
nginx/
|
||||
```
|
||||
|
||||
## Local development
|
||||
## 3. Backend implementation
|
||||
|
||||
### 1. Configure environment
|
||||
The backend is intentionally flat:
|
||||
|
||||
Copy `.env.example` to `.env` and update the values:
|
||||
- `apps/api/src/index.ts`
|
||||
Defines the HTTP routes and keeps request flow obvious.
|
||||
- `apps/api/src/auth/service.ts`
|
||||
Handles admin login, bootstrap admin seeding, session cookies, and session lookup.
|
||||
- `apps/api/src/docker/`
|
||||
Wraps Docker CLI calls with fixed argument arrays only. No shell interpolation.
|
||||
- `apps/api/src/users/service.ts`
|
||||
Contains the mailbox user operations and password reset flow.
|
||||
- `apps/api/src/mail/service.ts`
|
||||
Sends reset emails through `sendmail` inside the Docker Mailserver container.
|
||||
- `apps/api/src/lib/`
|
||||
Holds config parsing, SQLite access, validation, HTTP helpers, and small security utilities.
|
||||
|
||||
- `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
|
||||
### API design
|
||||
|
||||
Generate a 32-byte base64 key:
|
||||
- `GET /api/auth/session`
|
||||
- `POST /api/auth/login`
|
||||
- `POST /api/auth/logout`
|
||||
- `GET /api/users`
|
||||
- `POST /api/users`
|
||||
- `PATCH /api/users/:email/password`
|
||||
- `DELETE /api/users/:email`
|
||||
- `POST /api/users/:email/password-reset`
|
||||
- `POST /api/password-resets/inspect`
|
||||
- `POST /api/password-resets/consume`
|
||||
|
||||
## 4. Frontend implementation
|
||||
|
||||
The frontend is a small React app with three views:
|
||||
|
||||
- Login page
|
||||
- Dashboard page
|
||||
- Public reset-password page
|
||||
|
||||
The dashboard includes:
|
||||
|
||||
- mailbox listing
|
||||
- simple search
|
||||
- create user modal
|
||||
- edit password modal
|
||||
- delete action
|
||||
- send reset action
|
||||
|
||||
The create flow intentionally generates a temporary password on the server and immediately emails a one-time reset link to the user. That means the admin panel never needs to expose an initial mailbox password in the UI.
|
||||
|
||||
## 5. Password reset flow
|
||||
|
||||
The reset flow is built to stay secure without becoming heavy:
|
||||
|
||||
1. An admin triggers a reset for an existing mailbox.
|
||||
2. The backend generates a random opaque token.
|
||||
3. Only the SHA-256 hash of that token is stored in SQLite.
|
||||
4. The user receives an email with the one-time reset link.
|
||||
5. The reset page validates the token before showing the password form.
|
||||
6. On submit, the token is claimed for one use only.
|
||||
7. The backend updates the mailbox password through Docker Mailserver.
|
||||
8. Used or expired tokens are rejected.
|
||||
|
||||
## 6. Docker CLI integration strategy
|
||||
|
||||
The app talks to Docker Mailserver through `docker exec` with explicit argument arrays:
|
||||
|
||||
- `setup email list`
|
||||
- `setup email add <email> <password>`
|
||||
- `setup email update <email> <password>`
|
||||
- `setup email del <email>`
|
||||
- `sendmail -t -i`
|
||||
|
||||
Security choices here:
|
||||
|
||||
- No `sh -c`
|
||||
- No user input inserted into shell strings
|
||||
- Input is validated before command execution
|
||||
- Docker commands have a timeout
|
||||
- Errors are returned as API failures instead of leaking raw implementation detail to the browser
|
||||
|
||||
## 7. Security notes
|
||||
|
||||
- Admin authentication uses an Argon2id-hashed bootstrap account from environment variables.
|
||||
- Sessions are server-side, opaque, and stored in an `HttpOnly` cookie with `SameSite=Strict`.
|
||||
- Password reset tokens are random, one-time, time-limited, and stored only as hashes.
|
||||
- Admin-only routes require a valid session cookie.
|
||||
- Validation is applied to logins, emails, passwords, and reset payloads.
|
||||
- Allowed origins are restricted by `APP_ORIGINS`.
|
||||
- Reset emails are sent from the backend only. No secrets are exposed to the frontend.
|
||||
- Docker Mailserver access is treated as privileged infrastructure access.
|
||||
|
||||
## 8. Run locally
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Bun installed
|
||||
- Docker available locally
|
||||
- A running Docker Mailserver container
|
||||
|
||||
### Setup
|
||||
|
||||
1. Copy `.env.example` to `.env`.
|
||||
2. Update at least:
|
||||
- `HOSTED_DOMAINS`
|
||||
- `MAILSERVER_CONTAINER`
|
||||
- `APP_ADMIN_EMAIL`
|
||||
- `APP_ADMIN_PASSWORD`
|
||||
- `APP_MAIL_FROM`
|
||||
3. Install dependencies:
|
||||
|
||||
```bash
|
||||
openssl rand -base64 32
|
||||
bun install
|
||||
```
|
||||
|
||||
### 2. Install dependencies
|
||||
|
||||
If Bun needs an explicit temp dir on your machine:
|
||||
4. Start the API:
|
||||
|
||||
```bash
|
||||
BUN_TMPDIR=/tmp bun install
|
||||
bun run dev:api
|
||||
```
|
||||
|
||||
### 3. Start the apps
|
||||
|
||||
Backend:
|
||||
5. In another terminal, start the frontend:
|
||||
|
||||
```bash
|
||||
bun run --cwd apps/api dev
|
||||
bun run dev:web
|
||||
```
|
||||
|
||||
Frontend:
|
||||
6. Open `http://localhost:5173`.
|
||||
|
||||
```bash
|
||||
bun run --cwd apps/web dev
|
||||
```
|
||||
## Notes and assumptions
|
||||
|
||||
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.
|
||||
- This panel assumes Docker Mailserver is using the file-based account provisioner and supports the standard `setup email` commands.
|
||||
- The admin account for the panel is separate from mailbox users.
|
||||
- The app stores only panel-specific metadata in SQLite. Mailbox accounts themselves remain managed by Docker Mailserver.
|
||||
|
||||
Reference in New Issue
Block a user