No description
  • Jinja 79.1%
  • Python 11.1%
  • Shell 9.8%
Find a file
2026-06-25 04:56:55 +01:00
.github/workflows test: implement integration harness + wire all 16 roles into molecule CI 2026-06-23 22:34:14 +01:00
changelogs feat: implement remaining roles + CI fixes (phase 3) 2026-06-23 21:52:13 +01:00
ci feat: scaffold albient.mailstack collection skeleton (phase 1) 2026-06-23 21:29:24 +01:00
docs feat(scripts): auto-generate internal secrets in bootstrap 2026-06-24 00:20:53 +01:00
inventories Split Roundcube plugins + provisioning API into external repos; Zitadel update/delete sync; HAProxy webmail-TLS port fix; identity reconcile tooling 2026-06-24 23:47:58 +01:00
meta feat: scaffold albient.mailstack collection skeleton (phase 1) 2026-06-23 21:29:24 +01:00
playbooks Split Roundcube plugins + provisioning API into external repos; Zitadel update/delete sync; HAProxy webmail-TLS port fix; identity reconcile tooling 2026-06-24 23:47:58 +01:00
plugins feat: rework to two-domain audience split + aarch64 targeting 2026-06-23 22:11:55 +01:00
roles roundcube: pin albient_sso v0.5.0 (Mailvelope pgpmime capability repair) 2026-06-25 04:56:55 +01:00
scripts chore(secrets): real age recipient + MinIO-aware secret lengths 2026-06-24 12:02:55 +01:00
tests/integration test: implement integration harness + wire all 16 roles into molecule CI 2026-06-23 22:34:14 +01:00
.ansible-lint fix(provisioning): make first bring-up reachable + lint-skip encrypted secrets 2026-06-24 00:53:31 +01:00
.gitignore chore(lint): ignore generated DNS-contract artifacts 2026-06-24 12:02:55 +01:00
.pre-commit-config.yaml feat: scaffold albient.mailstack collection skeleton (phase 1) 2026-06-23 21:29:24 +01:00
.sops.yaml chore(secrets): real age recipient + MinIO-aware secret lengths 2026-06-24 12:02:55 +01:00
.yamllint chore(lint): ignore generated DNS-contract artifacts 2026-06-24 12:02:55 +01:00
ansible.cfg Fix SOPS decryption, floating-IP failover, and live mail-auth wiring 2026-06-24 17:20:23 +01:00
galaxy.yml chore: modern stdout callback + slim collection builds 2026-06-24 14:01:33 +01:00
HANDOFF.md docs: internal CA reconciled to canonical ca.mail.albient.cloud 2026-06-24 13:07:30 +01:00
README.md docs(README): reconcile brand link + note EU infra residency 2026-06-23 22:31:31 +01:00
requirements.yml chore: integrate parallel build-out and enforce zero-warning lint 2026-06-23 23:39:58 +01:00

albient.mailstack

A production-grade Ansible Collection that deploys a highly available, secure, sovereign e-mail platform for Albient — a UK hosting provider that prioritises privacy, data sovereignty and open source. (v1 infra runs in Hetzner Falkenstein/EU — Hetzner has no UK region; the design is provider-agnostic for a later UK move.)

The stack is built around Stalwart as the mail engine, with PostgreSQL/Patroni for metadata, MinIO for blob storage, 389 Directory Server as the canonical directory, and HAProxy for protocol-aware ingress + failover. Everything is inventory-driven, platform-agnostic (Hetzner Cloud, libvirt VMs, or bare metal), and designed for zero-downtime operations.

Status: active build. See docs/build-plan.md for the per-role implementation status (complete vs stub).


What you get

Capability Implementation
SMTP (25/465/587), IMAPS (993), JMAP, POP3S, Sieve Stalwart, strict modern TLS
Spam / RBL / greylisting / malware Stalwart native filter + ClamAV
Metadata store PostgreSQL + Patroni + etcd, R/W VIP via HAProxy, WAL PITR
Blob store MinIO distributed erasure-coded, versioned, S3-compatible
Directory & auth 389-DS multi-supplier replication (OIDC/IdP path documented)
Ingress / HA HAProxy (PROXY protocol) + Hetzner floating-IP / keepalived VRRP
Deliverability SPF/DKIM/DMARC/MTA-STS/TLS-RPT emitted for ansible-dns, egress IP pool
PKI step-ca internal CA (short-lived intra-cluster certs) + Let's Encrypt (DNS-01)
Provisioning API-first service + CLI, single write-path to 389-DS, WHMCS seam
Webmail Roundcube (HA) + Autoconfig / Autodiscover / .mobileconfig
Observability Prometheus + Grafana + Alertmanager + Loki + synthetic mail probes
Security SELinux enforcing, firewalld, CIS baseline, CrowdSec
Backups / DR restic encrypted off-site, PG PITR, MinIO versioning, automated restore drills
Governance quotas, send-rate limits, outbound abuse heuristics, GDPR/DSAR tooling

Opt-in per-mailbox encryption at rest using a customer-supplied PGP/S-MIME public key (zero-access for Albient) is exposed as a provisioning flag.


Topology (v1)

Three converged nodes, each running Stalwart, PostgreSQL (Patroni member), 389-DS (supplier), MinIO and supporting agents, with HAProxy on all three behind a floating IP. Tolerates the loss of one node with no data loss and minimal disruption.

Node roles are tag/group-driven, so every tier (edge/LB, mail app, Postgres, LDAP, object store, observability) can later be peeled onto dedicated hosts purely by editing inventory — no re-platforming. See docs/architecture.md.

                       Internet
                          │
                 ┌────────┴─────────┐
                 │  Hetzner Floating IP (or VRRP VIP)
                 └────────┬─────────┘
        ┌─────────────────┼─────────────────┐
   ┌────┴────┐       ┌────┴────┐       ┌────┴────┐
   │ node-1  │       │ node-2  │       │ node-3  │
   │ HAProxy │       │ HAProxy │       │ HAProxy │
   │ Stalwart│       │ Stalwart│       │ Stalwart│
   │ Patroni │◄─etcd─►│ Patroni │◄─etcd─►│ Patroni │
   │ 389-DS  │◄─repl─►│ 389-DS  │◄─repl─►│ 389-DS  │
   │ MinIO   │◄─EC───►│ MinIO   │◄─EC───►│ MinIO   │
   └─────────┘       └─────────┘       └─────────┘
            private network (all inter-node traffic)

Repository layout

galaxy.yml              Collection metadata
ansible.cfg             Control-node defaults
requirements.yml        Galaxy collection dependencies
.sops.yaml              SOPS+age encryption rules
roles/                  All deployment roles (see docs/build-plan.md)
playbooks/              site.yml, provision.yml, per-tier and ops playbooks
plugins/                modules (emit_dns_records, …), filters, inventory
inventories/{dev,staging,prod}/   group_vars/host_vars, SOPS-encrypted secrets
docs/                   architecture, build-plan, DNS contract, runbooks
ci/                     Execution Environment definition, CI helpers
molecule/               Shared molecule resources (per-role scenarios live in roles/*)
tests/                  Integration harness

Quick start

0. Prerequisites (control node)

python3 -m venv .venv && source .venv/bin/activate
pip install ansible-core ansible-lint yamllint molecule molecule-plugins[docker] sops
ansible-galaxy collection install -r requirements.yml
# age key for SOPS decryption
mkdir -p ~/.config/sops/age && age-keygen -o ~/.config/sops/age/keys.txt

1. Configure inventory & secrets

Edit inventories/<env>/hosts.yml and the SOPS-encrypted inventories/<env>/group_vars/all/secrets.sops.yaml (real tokens for hcloud, ClouDNS, DB/LDAP binds, DKIM/TLS keys). Never commit plaintext secrets — see docs/runbooks/secret-rotation.md.

2. Provision infrastructure (Hetzner) — optional

ansible-playbook -i inventories/prod/hosts.yml playbooks/provision.yml

For metal/VMs set provisioning_provider: none and supply a static inventory.

3. Deploy the platform

ansible-playbook -i inventories/prod/hosts.yml playbooks/site.yml

site.yml is serialised and health-gated for zero-downtime rolling convergence.

4. Validate locally (no live deploy)

yamllint .
ansible-lint
ansible-playbook playbooks/site.yml --syntax-check -i inventories/dev/hosts.yml
molecule test -s default   # inside a given role directory

DNS contract

This collection does not manage DNS. It emits the records it requires (SPF, DKIM, DMARC, MTA-STS, TLS-RPT, MX, A/AAAA, PTR requests, SRV autoconfig, CAA) as a structured artifact consumed by the separate ansible-dns repo (octoDNS → ClouDNS). The schema and a worked sample live in docs/dns-contract.md.


Documentation

License

AGPL-3.0-or-later. © Albient.