#21: make the stack deployable, and document how
Three things stood between this and a real deployment. A missing JWT_SECRET fell back to a constant that is published in this repository, so any deployment that forgot the variable would have accepted session tokens anyone could mint, including an admin one. The auth service now refuses to start without a secret of at least 32 characters. The test suites set it through vitest config, because ESM hoists imports and an assignment inside a test file runs too late. Session cookies were issued without the Secure attribute: it is driven by NODE_ENV, which compose never set. Behind a TLS terminator that is a downgrade waiting to happen. Nothing created the first mailbox. A fresh install has no domain and no account, and every route into the console needs one - so there was no first login. There is now a create-admin command, which also provisions the standard folders and the four system aliases. It is a deliberate command rather than a bootstrap on boot: creating an administrator as a side effect of starting a container is how a deleted account quietly comes back. Also: the production images were shipping compiled test files, since the build compiled everything under src. A separate build config excludes them while lint still type-checks them. Documentation. README.md covers what this is, how it is put together, and what is not finished - stated plainly, because a mail server that half works loses mail. docs/DEPLOYMENT.md is the Coolify path in the order it has to happen, starting with the three prerequisites no code can satisfy: outbound port 25, the PTR record, and the DNSSEC delegation. docs/OPERATIONS.md is the runbook - health states, certificates, backups, reading a delivery failure, DKIM rotation, and an explicit list of what is not automated. .env.production.example replaces guessing which of the dev defaults are safe: none of them are. docs/ was gitignored as a place for internal notes; only docs/internal/ is now, so these guides ship with the code. Every command the documentation tells an operator to run was executed against a running stack: create-admin, preflight, the health endpoint, the published mail ports, and the Secure cookie. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
30014f0f41
commit
c0387f6961
9 files changed
+641
-4
No files matched your search
@@ -0,0 +1,148 @@
|
||||
# LambdaMail
|
||||
|
||||
A self-hosted mail server with an integrated webmail and admin console, built to
|
||||
run on [Coolify](https://coolify.io) from a single `docker-compose.yaml`.
|
||||
|
||||
SMTP, IMAP, POP3 and ManageSieve, with SPF, DKIM, DMARC, ARC, MTA-STS and
|
||||
TLS-RPT, DNS automation through Cloudflare, and spam and virus filtering. No
|
||||
paid tiers and no artificial limits on domains or mailboxes.
|
||||
|
||||
> **Status: pre-release.** The mail path, the security posture and the web
|
||||
> interfaces work end to end, and there is an automated test suite behind them.
|
||||
> Several console panels are still read-only, and some features are recorded but
|
||||
> not yet acted on — see [What is not finished](#what-is-not-finished) before
|
||||
> putting real mail through it.
|
||||
|
||||
---
|
||||
|
||||
## What it does
|
||||
|
||||
| Area | |
|
||||
| :--- | :--- |
|
||||
| **Protocols** | SMTP MX (25), Submission (587 STARTTLS, 465 implicit TLS), IMAP (143/993) with UID, flags, IDLE and CONDSTORE, POP3 (110/995), ManageSieve (4190) |
|
||||
| **Authentication** | SPF, DKIM (Ed25519 + RSA dual signature), DMARC, ARC sealing, DANE/TLSA and MTA-STS on delivery, TLS-RPT ingestion |
|
||||
| **Filtering** | Rspamd scoring with greylisting, RBLs and rate limiting; ClamAV; Sieve |
|
||||
| **Delivery** | Durable outbound queue in Postgres with the retry ladder and RFC 3464 bounces; smarthost fallback for hosts with port 25 blocked |
|
||||
| **DNS** | Cloudflare reconciliation of the 13 records, idempotent, with drift detection |
|
||||
| **Web** | Webmail at `/user/*` and an admin console at `/admin/*`, isolated by session audience, with TOTP two-factor, recovery codes, app passwords, and English, Portuguese and Spanish |
|
||||
|
||||
## Architecture
|
||||
|
||||
Two runtimes, split by what each is good at:
|
||||
|
||||
- **`protocols`** (Go) — every protocol listener, the delivery worker, TLS
|
||||
certificate handling, DNS reconciliation, and the webmail's mail API. It owns
|
||||
folder, UID, flag and blob semantics.
|
||||
- **`auth`** (TypeScript) — logins, sessions, two-factor, account management and
|
||||
the admin console API.
|
||||
- **`web`** (Next.js) — both web surfaces, and the proxy that gives the browser a
|
||||
single origin.
|
||||
- **`storage`** (TypeScript) — health only today.
|
||||
|
||||
Postgres is the source of truth for everything, including the outbound queue.
|
||||
Redis is a cache and dispatch index; losing it loses no mail.
|
||||
|
||||
```
|
||||
┌──────────┐
|
||||
browser ─────────▶│ web │──┬─▶ auth (sessions, accounts, admin)
|
||||
└──────────┘ └─▶ protocols (mail read/send, DKIM, TLS)
|
||||
│
|
||||
internet ──25/587/465/143/993──────────┘
|
||||
│
|
||||
Postgres ─┴─ Redis ─ Rspamd ─ ClamAV
|
||||
```
|
||||
|
||||
## Deploying
|
||||
|
||||
For a real VPS, follow **[docs/DEPLOYMENT.md](docs/DEPLOYMENT.md)**. It covers the
|
||||
three things no code can do for you — reverse DNS, the DNSSEC delegation and
|
||||
getting port 25 unblocked — and the order the rest has to happen in.
|
||||
|
||||
The short version:
|
||||
|
||||
```bash
|
||||
cp .env.production.example .env # then replace every CHANGE
|
||||
make preflight # checks port 25, PTR, RBLs, token scope
|
||||
docker compose up -d
|
||||
make create-admin EMAIL=you@example.com PASSWORD='...'
|
||||
```
|
||||
|
||||
`make create-admin` is not optional: a fresh install has no mailbox, and every
|
||||
route into the console needs one.
|
||||
|
||||
## Running locally
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
make gen-dev-cert # self-signed certificate for the listeners
|
||||
docker compose up --build
|
||||
make seed # a postmaster@example.test fixture
|
||||
```
|
||||
|
||||
The webmail is on `http://localhost:3000`. The seeded password is
|
||||
`dev-password-only` — it is a fixture, not a credential.
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
make test # Go suite, auth and webmail
|
||||
make lint # go vet, tsc, and the language lint
|
||||
```
|
||||
|
||||
The Go and auth suites include integration tests that need Postgres. They skip
|
||||
without `TEST_DATABASE_URL` and run against a real database in CI, which is
|
||||
where the security regressions are enforced — wrong password, wrong TOTP code,
|
||||
session revocation, surface isolation, cross-mailbox reads.
|
||||
|
||||
## Configuration
|
||||
|
||||
Every variable is documented inline in
|
||||
[`.env.production.example`](.env.production.example). The ones without a safe
|
||||
default:
|
||||
|
||||
| Variable | |
|
||||
| :--- | :--- |
|
||||
| `JWT_SECRET` | Signs sessions. At least 32 characters; the service refuses to start below that rather than fall back to a constant. |
|
||||
| `LAMBDAMAIL_MASTER_KEY` | Encrypts DKIM keys, TOTP secrets and provider tokens. **Back it up separately from the database** — losing it makes every stored secret unrecoverable. |
|
||||
| `POSTGRES_PASSWORD`, `REDIS_PASSWORD` | No defaults; compose fails without them. |
|
||||
| `MAIL_DOMAIN`, `PRIMARY_MAIL_HOST`, `PUBLIC_IPV4` | Identity. `PRIMARY_MAIL_HOST` must match the PTR of `PUBLIC_IPV4`. |
|
||||
|
||||
## Operating
|
||||
|
||||
| | |
|
||||
| :--- | :--- |
|
||||
| `make preflight` | Port 25 egress, PTR and FCrDNS, RBL listings, Cloudflare token scope, certificate presence |
|
||||
| `make create-admin EMAIL=… PASSWORD='…'` | First administrator on a fresh install |
|
||||
| `make reset-password EMAIL=… PASSWORD='…'` | Recovers a locked-out account and signs out its sessions |
|
||||
| `docker compose logs -f protocols` | Delivery, TLS and DNS reconciliation |
|
||||
| `GET /health` on `protocols` | `200` healthy, `200` with `X-LambdaMail-Health: degraded` when a certificate needs attention, `503` when a dependency is down |
|
||||
|
||||
[docs/OPERATIONS.md](docs/OPERATIONS.md) covers backups, certificate rotation,
|
||||
what the alerts mean and how to read a delivery failure.
|
||||
|
||||
## What is not finished
|
||||
|
||||
Stated plainly, because a mail server that half works loses mail:
|
||||
|
||||
- **Recorded but not acted on.** Vacation responder, signature and the Sieve
|
||||
rules built in the UI are stored, but nothing consumes them yet — enabling a
|
||||
vacation reply does not send one.
|
||||
- **Read-only console panels.** The guided domain onboarding checklist, the DNS
|
||||
desired-vs-actual diff and the CSV import report on state rather than driving
|
||||
it.
|
||||
- **Not implemented.** JMAP, BIMI, CalDAV/CardDAV, multi-node. Backups are
|
||||
documented but not automated.
|
||||
- **DANE** is off unless `TLS_MODE=acme`, and deliberately so: under a
|
||||
Traefik-managed certificate the key changes at every renewal and the published
|
||||
TLSA record stops matching, which permanently rejects mail at every validating
|
||||
MTA.
|
||||
|
||||
## Contributing
|
||||
|
||||
- All code, comments and identifiers are in English. `make lint` enforces it.
|
||||
- Tests come with the change that needs them; the suites above must stay green.
|
||||
- Commits reference their issue: `#N: what changed and why`.
|
||||
|
||||
## License
|
||||
|
||||
MIT — see [LICENSE](LICENSE).
|
||||
Reference in new issue
Block a user