SpamGate

A self-hosted SMTP filtering gateway for Debian 13.

SpamGate sits in front of your existing mail server, scans every inbound message with Rspamd, holds spam and review-grade mail in a MariaDB quarantine, and auto-releases clean mail downstream. Operators manage releases, lists and domains from a Flask admin UI.

  • Every message recorded - nothing silently dropped
  • Clustering - one main + any number of secondary filter nodes
  • Self-hostable - no third-party SaaS dependency for filtering
  • Operator-tunable scoring - three thresholds, all live-editable

How a message flows through SpamGate

Internet -> port 25 -> Postfix
                        |  milter: 127.0.0.1:11332
                        v
                      Rspamd  (adds X-Rspamd-Score / -Action / -Symbols)
                        |
                        |  smtpd_proxy_filter: 127.0.0.1:10025
                        v
              quarantine/filter.py  (aiosmtpd)
                        |
                        +-- INSERT into MariaDB  (always, every message)
                        |
                        +-- spam / review -> hold, return 250 OK to Postfix
                        |
                        +-- clean -> reinject -> port 10026 -> downstream

Three score thresholds, all stored in the settings table and editable in the admin UI:

  • spam_score_reject default 15.0 - held as spam
  • spam_score_review default 6.0 - held for manual review
  • spam_score_tag default 4.0 - tagged in the headers, auto-released

Port 10026 is a Postfix bypass port with no milter and no proxy filter, so reinjected mail cannot loop back through Rspamd.

Features

Quarantine with full message history

Every inbound message is written to MariaDB and the on-disk spool at /var/spool/spamgate/quarantine/. Review-grade items are never auto-purged. HTML body preview in the admin UI renders inside a sandboxed iframe.

Three account roles

Superadmin manages all domains and settings. Domain admin manages quarantine and per-domain lists for one domain. Mailbox user can release messages addressed to their own mailbox.

Per-domain hold-all-mail

Superadmin-only switch to pause downstream delivery for a single domain (useful during mailbox migrations). Clean mail to that domain stays in quarantine; a one-click "release all held" drains the queue when the migration completes.

Global and per-domain lists

Blacklist and whitelist with four entry kinds: email, domain, ip, cidr. Domain admins manage their own domain's lists independently of the global rules.

Layered network defences

Four kernel ipsets matched by iptables DROP rules on ports 25/80/443:

  • spamgate-scanners - ShadowWhisperer list
  • spamgate-stretchoid - upstream feed + /var/log/mail.log harvest
  • spamgate-internet-census - internet-census.org ranges
  • spamgate-blocked-clients - mirror of the Postfix CIDR table (Censys, Shadowserver)

Plus seven fail2ban jails covering SMTP auth, EHLO floods, DNSBL violators, no-PTR retriers, and web-UI exploit probes.

Phishing intelligence

Daily download of the phishing.mailscanner.info bad-sites list, merged with a local-override file that is preserved across updates. Listed URLs in a message body fire a hard score that cannot be cancelled by other URLs in the same mail.

AbuseIPDB integration (optional)

Daily download of the AbuseIPDB worst-offender blacklist into a static radix map - no per-message API calls, so a free-tier token is sufficient. A listed sender IP pushes the score above the review threshold so the message is held rather than rejected.

Cluster-shared Bayes

Rspamd's Bayes classifier is shared across cluster nodes via Redis replication from the main. Train once, every node benefits. Bulk training and retraining via the train_bayes.py helper.

Audit log and reports

Every state-changing action - account creation, list edit, release, domain hold, password reset - lands in audit_log. The reports page renders quarantine stats with Chart.js.

Drift-safe updater

update.sh uses a three-way diff (baseline vs deployed vs new template) on every config file and only prompts when both the operator and the template have diverged. Pending schema migrations are offered one at a time.

Config backup / restore

backup_restore.py archives the configuration surface - users, lists, settings, domains, cluster nodes, Bayes dump.rdb, system configs, iptables and ipset rules.

Operator-friendly tooling

A root-run reset_password.py rescues lockouts without needing the web UI. A test_dkim_dmarc.py diagnostic prints SPF, DKIM and DMARC results for any domain in one shot.

The admin UI

Operators work from a Flask + nginx web UI accessible at https://<node>/. Login is by full email address; new accounts are forced to change password at first sign-in. Tap any screenshot to view it full-size.

SpamGate login screen with email and password fields
Login screen.
SpamGate recent messages view showing the latest mail that arrived
Recent messages - at-a-glance view of the latest mail to arrive, with Rspamd score and current status per row.
SpamGate quarantine page listing held messages with filters and bulk actions
Quarantine - filterable list of every held message; bulk release, mark-as-spam and mark-as-ham actions.
SpamGate message detail page for a high-score spam message
Message detail - a clear-cut spam (score 23.9). Sender, recipient, headers, Rspamd symbols and sandboxed body preview all on one page; release / mark-as-ham / mark-as-spam / delete actions on the side.
SpamGate message detail page for a high-score message that still passed as clean
Message detail - high score (15.7) but still under the reject threshold, so SpamGate auto-released as clean. The symbols panel shows exactly which rules fired, making operator tuning easy.
SpamGate domain management page listing local recipient domains
Domains - local recipient domains. Adding a domain rebuilds the Postfix transport map within 5 minutes.
SpamGate user list page showing administrator and mailbox accounts
Users - all account roles (superadmin, domain admin, mailbox user) listed and filterable.
SpamGate new user creation form
Create a new user - role and domain selection; new accounts get force_pw_change = 1 automatically.
SpamGate blacklist management page listing blocked entries
Blacklist - blocked senders by email, domain, IP or CIDR; global or per-domain scope, with a free-text reason on every entry.
SpamGate whitelist management page listing trusted senders
Whitelist - trusted senders that bypass scoring; same four entry types and per-domain scoping as the blacklist.
SpamGate settings page showing score thresholds and cluster options
Settings - score thresholds, cluster API token, and other live-tunable parameters.

Single node, or main + N secondaries

Single node

Postfix, Rspamd, MariaDB, Redis, the aiosmtpd quarantine filter, the Flask admin UI and nginx all run on one host. Suitable for small-to-medium deployments. The same install path is used and then extended for clustering later if needed.

Cluster

One main node owns the database, the cluster REST API and the Bayes-source Redis. Any number of secondary filter nodes accept inbound mail and pull settings, domains, blacklists and whitelists from the main via HTTPS REST. Releases performed in the main UI for a secondary's message call back over the VPN to the secondary's /api/v1/release/<id> endpoint.

Authentication: bearer token, bcrypt-hashed in the DB. The plain-text token only exists in the secondaries' spamgate.conf.

Automated maintenance

The system maintains itself via cron and systemd timers:

  • Quarantine cleanup of expired rows - daily 02:00.
  • Postfix transport map rebuild - every 5 minutes when the domains table changes.
  • Rspamd local-domains map rebuild for display-name impersonation detection - every 5 minutes.
  • Phishing bad-sites list refresh - daily 03:00.
  • AbuseIPDB blacklist refresh - daily 03:15.
  • Scanner ipset refresh (ShadowWhisperer + Stretchoid + blocked-clients mirror) - weekly Sunday 03:30.
  • Secondary -> main sync pull - every 5 minutes via spamgate-sync.timer.
  • TLS certificate renewal - handled by certbot's own timer with a reload hook for Postfix and nginx.

System requirements (per node)

ResourceMinimumRecommended
CPU2 vCPU4 vCPU
RAM4 GB8 GB
Disk20 GB60 GB or more
NetworkStatic IPv4, correct reverse DNS for the FQDN
IPv6Optional - auto-detected; runs equally well IPv4-only or dual-stack

Operating system

Debian 13 (Trixie), fresh minimal install. The stack is built and tested against Debian 13's package set (Postfix, Rspamd, MariaDB1, Python). Debian 12 and earlier, Ubuntu, and other distributions are not supported by the installer.

Inbound ports

  • 25 - SMTP from the Internet
  • 80 - ACME HTTP-01 only; everything else 301s to HTTPS
  • 443 - admin web UI (public, VPN-only, or loopback-only is configurable)
  • 22 - operator SSH

Built on

  • Postfix - SMTP front end, TLS, DNSBL, header_checks, transport maps.
  • Rspamd - scanner via Postfix milter, Bayes classifier, multimap rules, composites, Lua custom rules.
  • MariaDB - quarantine, domains, lists, settings, audit log, cluster membership.
  • Redis - Bayes backing store; replicated to secondaries.
  • Python with aiosmtpd for the quarantine proxy filter, Flask + gunicorn + SQLAlchemy for the admin UI, bcrypt for credential storage.
  • nginx - HTTPS termination for the admin UI; configurable bind addresses.
  • Let's Encrypt via certbot with a deploy hook that reloads Postfix and nginx automatically on renewal.
  • fail2ban, ipset, iptables, netfilter-persistent - layered abuse defences.

What SpamGate does not do

  • It does not deliver mail to mailboxes. It hands clean mail off to a downstream MTA / mail store on port 25 or your choosing.
  • It does not act as a relay for outbound mail.
  • It does not depend on any cloud or SaaS for filtering decisions. Optional AbuseIPDB integration only downloads a static list once per day.
  • It does not auto-apply schema migrations. Migrations are offered to the operator one at a time during update.