mail-forwarding-core

A simple, complete, abuse-aware, open-source mail forwarding stack

mail-forwarding-core (or simply core) is the reference implementation used by Haltman.io to provide a free, public, production-grade mail forwarding service.

It is designed to be:

  • Fully open-source

  • Stateless (no mailbox storage)

  • Abuse-aware by design

  • Deterministic and auditable

  • Scalable by composition, not complexity

There is no open-core, no artificial limitation, and no telemetry.

This document describes how to install, configure, and validate the full stack from scratch.


A public instance is available at https://forward.haltman.io/:


Architecture Overview

mail-forwarding-core is composed of five main building blocks:

Component

Role

Postfix

SMTP engine and policy enforcement

PostSRSd

Sender Rewriting Scheme (SRS)

MariaDB

Domain and alias lookup backend

OpenDKIM

Optional DKIM signing

DNS

Authentication and routing

High-level flow

Inbound Mail
   ↓
Postfix (smtpd)
   ↓  (domain + alias lookup)
MariaDB
   ↓
PostSRSd (SRS rewrite)
   ↓
Postfix (smtp)
   ↓
External Destination

No messages are stored. No queues are inspected. No content is logged.


Installation Order (Important)

The order below is mandatory:

  1. MariaDB (schema + user)

  2. DNS records (MX / SPF / DMARC)

  3. PostSRSd

  4. Postfix

  5. OpenDKIM (optional, last)

Installing components out of order will cause misleading failures.


1. MariaDB (Lookup Backend)

MariaDB is used only as a lookup backend. Postfix never writes to it.

Install

sudo apt install mariadb-server

Create database and user

CREATE DATABASE maildb;

CREATE USER 'maildb'@'localhost' IDENTIFIED BY 'strong-password';
GRANT SELECT, INSERT ON maildb.* TO 'maildb'@'localhost';
FLUSH PRIVILEGES;

Required tables

domain

CREATE TABLE `domain` (
  `id` int(11) NOT NULL,
  `name` varchar(255) NOT NULL,
  `active` tinyint(1) DEFAULT 1,
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

alias

CREATE TABLE `alias` (
  `id` int(11) NOT NULL,
  `address` varchar(255) NOT NULL,
  `goto` varchar(255) NOT NULL,
  `active` tinyint(1) DEFAULT 1,
  `domain_id` int(11) NOT NULL,
  `created` timestamp NULL DEFAULT current_timestamp(),
  `modified` timestamp NULL DEFAULT current_timestamp()
    ON UPDATE current_timestamp(),
  PRIMARY KEY (`id`),
  UNIQUE KEY `uq_alias_address` (`address`),
  KEY `address` (`address`),
  KEY `domain_id` (`domain_id`),
  CONSTRAINT `alias_ibfk_1`
    FOREIGN KEY (`domain_id`) REFERENCES `domain` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

MariaDB setup is now complete.


2. DNS Configuration

Each forwarded domain must define DNS records correctly.

Required records

MX

example.org. MX 10 mail.example.net.

SPF

example.org. TXT "v=spf1 mx -all"

DMARC

_dmarc.example.org. TXT "v=DMARC1; p=none"

If OpenDKIM is enabled later, a DKIM record will be required.


3. PostSRSd (Mandatory)

Mail forwarding requires SRS to preserve SPF alignment.

Install

sudo apt install postsrsd

Configuration

Edit /etc/default/postsrsd:

SRS_DOMAIN=example.org
SRS_SEPARATOR==
SRS_SECRET=/etc/postsrsd.secret
SRS_HASHLENGTH=4
SRS_HASHMIN=4
SRS_FORWARD_PORT=10001
SRS_REVERSE_PORT=10002
RUN_AS=postsrsd
SRS_LISTEN_ADDR=127.0.0.1
CHROOT=/var/lib/postsrsd

Generate secret

openssl rand -hex 32 | sudo tee /etc/postsrsd.secret
sudo chmod 600 /etc/postsrsd.secret

Start service

sudo systemctl enable postsrsd
sudo systemctl restart postsrsd

Validate:

ss -ltnp | grep 1000

4. Postfix (Core Engine)

Postfix acts strictly as a forwarding MTA.

Install

sudo apt install postfix postfix-mysql

Choose Internet Site, but delivery will be disabled.

Key principles

  • No local delivery

  • No open relay

  • MySQL-backed domains and aliases

  • Sender spoofing protection

  • Mandatory SRS integration

Files Overview

/etc/postfix/main.cf

Primary Postfix configuration.

Key characteristics:

  • No mydestination → no local delivery

  • Virtual domains and aliases backed by MySQL

  • Explicit relay and recipient restrictions

  • HELO/EHLO hygiene

  • Integration points for:

    • SRS (TCP maps)

    • DKIM (milter)

TLS settings are intentionally commented out.

# Postfix compatibility level (after major upgrades)
compatibility_level = 3.6

# SMTP Identity
myhostname = mail.example.com
mydomain = example.com
myorigin = $mydomain

# Networking listen interfaces
inet_interfaces = all
inet_protocols = ipv4

# The hostname to send on SMTP HELO or EHLO command
smtp_helo_name = mail.example.com

# Disable local email delivery
mydestination =

# Virtual domains are managed by MySQL
virtual_alias_domains = mysql:/etc/postfix/mysql-virtual-domains.cf

# Virtual aliases are managed by MySQL
virtual_alias_maps = mysql:/etc/postfix/mysql-virtual-aliases.cf

# Disable stack details on unknown alias addresses errors
show_user_unknown_table_name = no

# Disable VRFY command to prevent alias enumeration
disable_vrfy_command = yes

# Disable SASL for security
smtpd_sasl_auth_enable = no

# The numerical Postfix SMTP server response code when a recipient address is local
unknown_local_recipient_reject_code = 550

# Avoid information disclosure to SMTP client
smtpd_banner = $myhostname ESMTP

# Wait to reject
smtpd_delay_reject = yes

# -------------------------------------------------
# Anti open-relay
# -------------------------------------------------
smtpd_relay_restrictions =
    permit_mynetworks,
    reject_unauth_destination

# -------------------------------------------------
# Recipient restrictions
# -------------------------------------------------
smtpd_recipient_restrictions =
    permit_mynetworks,
    reject_non_fqdn_recipient,
    reject_unknown_recipient_domain,
    reject_unlisted_recipient

# -------------------------------------------------
# Sender restrictions (ANTI-SPOOFING DINÂMICO)
# -------------------------------------------------
smtpd_sender_restrictions =
    permit_mynetworks,
    check_sender_access regexp:/etc/postfix/block_srs_inbound.regexp,
    check_sender_access mysql:/etc/postfix/mysql-block-local-senders.cf,
    permit

# -------------------------------------------------
# Client limits
# -------------------------------------------------
smtpd_client_connection_count_limit = 15
smtpd_client_connection_rate_limit = 60

# -------------------------------------------------
# HELO / EHLO hygiene
# -------------------------------------------------
smtpd_helo_required = yes
smtpd_helo_restrictions =
    permit_mynetworks,
    reject_invalid_helo_hostname

# -------------------------------------------------
# TLS 
# -------------------------------------------------
# TLS is intentionally commented out in this example.
# Enable only after valid certificates are provisioned.
#smtpd_tls_security_level = may
#smtp_tls_security_level = may
#smtpd_tls_cert_file = /etc/letsencrypt/live/mail.example.com/fullchain.pem
#smtpd_tls_key_file  = /etc/letsencrypt/live/mail.example.com/privkey.pem

smtpd_tls_received_header = no

# -------------------------------------------------
# SRS (forwarding)
# -------------------------------------------------
sender_canonical_maps = tcp:localhost:10001
sender_canonical_classes = envelope_sender

recipient_canonical_maps = tcp:localhost:10002
recipient_canonical_classes = envelope_recipient

# -------------------------------------------------
# OpenDKIM
# -------------------------------------------------
# OpenDKIM is intentionally commented out in this example.
# Enable only after valid DKIM signatures on DNS are provisioned.
# smtpd_milters = inet:127.0.0.1:8891
# non_smtpd_milters = $smtpd_milters
# milter_default_action = tempfail

/etc/postfix/master.cf

Service definitions used by Postfix.

  • Largely defaults

  • No unsafe overrides

  • No custom listeners exposed

Provided for completeness and transparency.

smtp      inet  n       -       y       -       -       smtpd

pickup    unix  n       -       y       60      1       pickup
cleanup   unix  n       -       y       -       0       cleanup
qmgr      unix  n       -       n       300     1       qmgr
tlsmgr    unix  -       -       y       1000?   1       tlsmgr
rewrite   unix  -       -       y       -       -       trivial-rewrite
bounce    unix  -       -       y       -       0       bounce
defer     unix  -       -       y       -       0       bounce
trace     unix  -       -       y       -       0       bounce
verify    unix  -       -       y       -       1       verify
flush     unix  n       -       y       1000?   0       flush
proxymap  unix  -       -       n       -       -       proxymap
proxywrite unix -       -       n       -       1       proxymap
smtp      unix  -       -       y       -       -       smtp
relay     unix  -       -       y       -       -       smtp
        -o syslog_name=${multi_instance_name?{$multi_instance_name}:{postfix}}/$service_name
showq     unix  n       -       y       -       -       showq
error     unix  -       -       y       -       -       error
retry     unix  -       -       y       -       -       error
discard   unix  -       -       y       -       -       discard
local     unix  -       n       n       -       -       local
virtual   unix  -       n       n       -       -       virtual
lmtp      unix  -       -       y       -       -       lmtp
anvil     unix  -       -       y       -       1       anvil
scache    unix  -       -       y       -       1       scache
postlog   unix-dgram n  -       n       -       1       postlogd

/etc/postfix/mysql-virtual-domains.cf

Defines how Postfix queries MySQL to determine which domains are accepted.

This file controls:

  • Which domains are considered local/virtual

  • Whether a domain is active

user = <MARIADB_USERNAME>
password = <MARIADB_PASSWORD>
hosts = 127.0.0.1
dbname = <MARIADB_DATABASE>
query = SELECT 1 FROM domain WHERE name='%s' AND active=1

/etc/postfix/mysql-virtual-aliases.cf

Defines how Postfix resolves email aliases via MySQL.

This file controls:

  • Address → destination mappings

  • Forwarding behavior

user = <MARIADB_USERNAME>
password = <MARIADB_PASSWORD>
hosts = 127.0.0.1
dbname = <MARIADB_DATABASE>
query = SELECT goto FROM alias WHERE address='%s' AND active=1

/etc/postfix/mysql-block-local-senders.cf

Implements dynamic sender spoofing protection.

Instead of returning data, this query intentionally returns a static REJECT when the sender domain matches an active local domain.

This prevents external clients from forging MAIL FROM addresses belonging to hosted domains.

This behavior is deliberate.

user = <MARIADB_USERNAME>
password = <MARIADB_PASSWORD>
hosts = 127.0.0.1
dbname = <MARIADB_DATABASE>
query = SELECT 'REJECT forged local sender' FROM domain WHERE active=1 AND name='%d' LIMIT 1

/etc/postfix/block_srs_inbound.regexp

Rejects inbound messages with SRS-formatted senders.

This ensures:

  • SRS is only used internally for forwarding

  • External SRS traffic is not accepted blindly

/^SRS[01]=/    REJECT SRS sender not accepted inbound

OpenDKIM signs outbound mail to improve deliverability.

Install

sudo apt install opendkim opendkim-tools

Socket

OpenDKIM listens on:

inet:127.0.0.1:8891

Postfix connects via milter.

Tables

  • KeyTable

  • SigningTable

  • TrustedHosts

Private keys are deployment-specific and must never be committed.

If OpenDKIM is not installed, simply remove milter directives from Postfix.

Files Overview

/etc/opendkim.conf

Main OpenDKIM daemon configuration.

Notable characteristics:

  • Runs as an unprivileged user

  • Logs to syslog with success and failure visibility

  • Uses relaxed/simple canonicalization

  • Signs only selected headers

  • Listens locally via an INET socket

Relevant excerpts:

Syslog                  yes
SyslogSuccess           yes
LogWhy                  yes

Canonicalization        relaxed/simple
SubDomains              no
OversignHeaders         From

UserID                  opendkim
UMask                   007

Socket                  inet:8891@127.0.0.1
PidFile                 /run/opendkim/opendkim.pid

KeyTable                /etc/opendkim/KeyTable
SigningTable            refile:/etc/opendkim/SigningTable
ExternalIgnoreList      /etc/opendkim/TrustedHosts
InternalHosts           /etc/opendkim/TrustedHosts

/etc/opendkim/KeyTable

Maps DKIM selectors and domains to private key files.

Example structure:

selector1._domainkey.example.org example.org:selector1:/etc/opendkim/keys/example.org/selector1.private

Notes:

  • Private key files must never be committed

  • File paths are deployment-specific

  • One selector per domain is typical, but multiple are supported


/etc/opendkim/SigningTable

Defines which domains or addresses should be DKIM-signed and which selector should be used.

Example structure:

*@example.org selector1._domainkey.example.org

This allows:

  • Per-domain signing rules

  • Flexible expansion to multiple domains


/etc/opendkim/TrustedHosts

Defines hosts and networks trusted by OpenDKIM.

This file is used for both:

  • InternalHosts

  • ExternalIgnoreList

Typical entries include:

127.0.0.1
localhost
::1

Only mail originating from trusted sources will be signed.


Validation Checklist

  • MX resolves to Postfix host

  • SPF passes on forwarded mail

  • SRS rewrite visible in headers

  • No local delivery occurs

  • External spoofing is rejected

  • DKIM (if enabled) signs correctly


FAQ

Is this an open relay?

No. Relay and recipient restrictions are explicit.

Are mails logged?

No message content is logged. Only minimal MTA operational logs exist.

Is PostSRSd optional?

No. Forwarding without SRS breaks SPF.

Why reject inbound SRS addresses?

To prevent external SRS forgery and abuse.

Can I use multiple domains?

Yes. Domains and aliases are unlimited.

Is OpenDKIM mandatory?

No, but strongly recommended for production.

Does this store mailboxes?

No. This is forwarding only.


Security & Disclosure

If you find a vulnerability or misconfiguration:

📧 security@haltman.io

We respond as fast as possible.


Community & Support

Join the Haltman.io Telegram group for:

  • Questions

  • Networking

  • Design discussions

  • Operational feedback

👉 https://t.me/haltman_group


References


Final Notes

mail-forwarding-core is intentionally boring.

No magic. No abstractions. No vendor lock-in.

If you understand SMTP, you can understand — and trust — this stack.

Updated on