logo
Mail Forwarding (Self-Host)mail-forwarding-core
Mail Forwarding (Self-Host)

mail-forwarding-core

Complete guide to installing the core mail forwarding stack: MariaDB, DNS, PostSRSd, Postfix, and OpenDKIM. A simple, abuse-aware, open-source solution.

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.

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


Architecture Overview

ComponentRole
PostfixSMTP engine and policy enforcement
PostSRSdSender Rewriting Scheme (SRS)
MariaDBDomain and alias lookup backend
OpenDKIMOptional DKIM signing
DNSAuthentication 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

The order below is mandatory. Installing components out of order will cause misleading failures.

  1. MariaDB (schema + user)
  2. DNS records (MX / SPF / DMARC)
  3. PostSRSd
  4. Postfix
  5. OpenDKIM (optional, last)

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 table

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 table

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;

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 TXT record will also be required. See the full DNS Configuration guide.


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

/etc/postfix/main.cf

Primary Postfix configuration. No mydestination → no local delivery. Virtual domains/aliases backed by MySQL. Explicit relay and recipient restrictions.

# 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 (dynamic anti-spoofing)
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 (enable 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 (enable 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 with no unsafe overrides:

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

Controls which domains are considered local/virtual:

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

Resolves email aliases:

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

Dynamic sender spoofing protection — rejects external messages forging MAIL FROM of hosted domains:

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:

/^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.

/etc/opendkim.conf

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 selectors and domains to private key files:

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

Private key files must never be committed. File paths are deployment-specific.

/etc/opendkim/SigningTable

Defines which domains should be DKIM-signed:

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

/etc/opendkim/TrustedHosts

Defines trusted hosts (used for both InternalHosts and ExternalIgnoreList):

127.0.0.1
localhost
::1

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


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


Security & Disclosure

If you find a vulnerability or misconfiguration, contact security@haltman.io.

Community & Support

Join the Haltman.io Telegram group for questions, networking, and operational feedback.

References

Was this page helpful?
Built with Documentation.AI

Last updated today