Email Server

Table of Contents

I was thinking that I kind of became dependent of a number of services from big, established providers, including email, and was wondering, what would it take to set up an email server. After musing for a while, and as a new year challenge, I went ahead and got a machine from Hetzner (CX23 | x86 | 40GB), for ~5 EUR per month.

Postfix

For the setup, I went ahead with Postfix - and it’s really cool how the documentation I found was pretty easy to follow. I still need to figure out a way of saving the configuration and setting up backups, for IaaS, but that in a later post.

What I ended up following was:

One small surprise was that Hetzner doesn’t open up ports 25 and 465 until one has their first invoice paid - but this is not really a blocker as one can easily test locally, and that gives one more time to properly document oneself and secure the server.

SPF, DKIM & DMARC

For an email server, at least at the time of writing, securing means setting up SPF, DKIM and DMARC. Again, online one can find really good documentation on what each of these does - nice and short explained, as video, here.

SPF

Stands for , standing for Sender Policy Framework, provides validation that the sender is authorized to sent that given email (the from field in the header), by checking the DNS record and validating against that.

$ dig -t txt chirila.me
...
chirila.me.		300	IN	TXT	"v=spf1 mx a include:_spf.chirila.me -all"
...

$ dig -t txt _spf.chirila.me
...
_spf.chirila.me.	300	IN	TXT	"v=spf1 ip4:188.245.214.163 -all"
...

saying in this case that, when emails on the chirila.me domain come from the ipv4 address above, they should be accepted, and if they come from any other address, they should be considered as failing the SPF check. Interesting enough, Google has quite a list of addresses and they don’t straight fail, but soft fail.

$ dig -t txt _spf.google.com

...
_spf.google.com.	300	IN	TXT	"v=spf1 ip4:74.125.0.0/16 ip4:209.85.128.0/17 ip6:2001:4860:4000::/36 ip6:2404:6800:4000::/36 ip6:2607:f8b0:4000::/36 ip6:2800:3f0:4000::/36 ip6:2a00:1450:4000::/36 ip6:2c0f:fb50:4000::/36 ~all"
  flowchart LR
    A[Sender]
    B[Receiver server]

    A -->|email| B
    B --> C{lookup SPF
    entry in DNS}
    C -->|found and matching| P[Pass]
    C -->|not found or not matching| F[Fail or Softfail]
    P --> E[Forward to user's mailbox]

DKIM

Stands for DomainKey Idenfitied Mail and uses public-private key cryptography to sign the messages sent. DKIM uses, like SPF, DNS as storage. The short version on how DKIM works is: a message (headers & body) is transformed, signed with the sender’s private key, and gets an extra header with the signature. Upon receiving, the receiver can retrieve the public key for the domain and, applying the same transformation to the message (headers & body again), can verify that the message hasn’t been tampered with in transit, as well as that the message is coming from the actual sender.

$ dig -t txt default._domainkey.chirila.me
...
;; ANSWER SECTION:
default._domainkey.chirila.me. 300 IN	TXT	"v=DKIM1; h=sha256; k=rsa; s=email; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl7PxO5hxtCZ2SoOkxGlOtvMlyEsKDTH1NSo0ucYWv70df8hjvA1Qz/X0Rqq8UaHB6XbeAhi/9082PeDQQSS0YHFfYU+d9O3QTPvaTZEYP8t51dViHDZr5qfRj/CLTWIJo+C191hCZr7vTC3JUY3bbkoY4vsvzYMLjzeAKf6HCZx+Jz" "ELbwg4RDtUp9zk4sQsw5qdqfbgwFegpMZzZxwRnCG82Acqk4dz13Q/07+H2YI9MJDGSxS6jTp4FbONcuMjviV1y5PBQBMpk7fgO2Dxv63ro1fU31Ks+/a/46kDsi6SdxP0nYhJ+eGIEzM2c2jk5U1qAjOgi8DaD7Az3wYxTQIDAQAB"
...

For DKIM the solution I went for is opendkim, also following the instructions from https://easydmarc.com/blog/how-to-configure-dkim-opendkim-with-postfix/.

Testing the OpenDKIM key

OpenDKIM comes with a nifty helper, in opendkim-tools: opendkim-testkey.

$ opendkim-testkey -d chirila.me -s default -vvv
opendkim-testkey: using default configfile /etc/opendkim.conf
opendkim-testkey: checking key 'default._domainkey.chirila.me'
opendkim-testkey: key secure
opendkim-testkey: key OK

The key secure part is here: OpenDKIM was able to verify the key with DNSSEC. Initially this was listing key OK, but key not secure (as DNSSEC was not enabled on the domain).

Testing a full message

Locally you can send a message to yourself, pick it up from the Maildir and send it to opendkim-testmsg. Sadly the tool isn’t that verbose and the error messages are … a bit cryptic.

First send a test message:

echo "This is another friendly example, with relaxed/relaxed DKIM setup." | \
    mail -s "Testing email" andrei@chirila.me

Verify the raw message:

$ cat Maildir/cur/1770242318.V801I3f5e0M389267.mail\:2\,S 
Return-Path: <andrei@chirila.me>
X-Original-To: andrei@chirila.me
Delivered-To: andrei@chirila.me
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=chirila.me;
        s=default; t=1770242318;
        bh=OgL+DCTx9FtuTZdiINRkTe+lBeFoQrV7/aj/sJc1SoQ=;
        h=Subject:To:Date:From:From;
        b=T2+fO1lgZz4iJD+hdWgP2grDG7d7N6OVwBCrnl7rVgKbkHZ2F6mOchjq2mUO4Dmr3
         jneuEeku2vpxkqjtpt5UwfA8RKntLiwlJUpgI57VYI0TEE83EUzWIZ4A8AJJ6dUorT
         aqrW6FwFt1lyQicfHwl+tdXHvyZoLV6upmQFWlqke0rGWS+rw4H+0Qp3xamppD39zE
         0U782M6Lir2P5uT+5TGtYXiGf4j2toYTljr/74+vLuGZ1KrFDXkICmc6fiR5xPRRCb
         23gFxNyFZs2DK87jQbfA6pXKjKSyxT0EtX3Lw4Nm2qF2yVAXtAE0fzJxnr451jf914
         lBeykk99a5e0w==
Received: by mail.chirila.me (Postfix, from userid 1000)
        id 4FF2E3FB48; Wed,  4 Feb 2026 21:58:38 +0000 (UTC)
Subject: Testing email
To: <andrei@chirila.me>
User-Agent: mail (GNU Mailutils 3.17)
Date: Wed,  4 Feb 2026 21:58:38 +0000
Message-Id: <20260204215838.4FF2E3FB48@mail.chirila.me>
From: Andrei Chirila <andrei@chirila.me>

This is another friendly example, with relaxed/relaxed DKIM setup.

We can already see that DKIM-Signature header is there – so setup is good.

And one more step, pipe it through opendkim-testmsg, which would return 0 if everything is fine.

$ cat Maildir/cur/1770242318.V801I3f5e0M389267.mail\:2\,S  | opendkim-testmsg 
$ echo $?
0

DMARC

Domain-based Message Authentication, Reporting and Conformance ties together SPF and DKIM. The idea is that via DMARC one is specifying a policy (none, quarantine or reject) for emails that pass or fail SPF and DKIM checks. In addition to the policy, DMARC also provides a way of reporting back violations so you, as domain owner, know if folks are trying to spoof emails from your domain.

Below is the initial configuration I’m running with, with the caveat that, as this is the first time I’m setting it, I go with a none policy, to try and see if the emails I’m sending are actually arriving. Once I’m happy with the setup I can switch to reject.

DMARC provides also some fine-tuning options, below for example you can see that I have failure reporting turned on fo=1, and I want the verification for both SPF (aspf) and DKIM (adkim) to be **s**trict (as opposed to relaxed).

$ dig -t txt _dmarc.chirila.me
...

;; ANSWER SECTION:
_dmarc.chirila.me.	300	IN	TXT	"v=DMARC1; p=none; rua=mailto:dmarc-reports@chirila.me; ruf=mailto:dmarc-reports-forensics@chirila.me; fo=1; adkim=s; aspf=s;"
...

I have yet to see the reports, but once I’ll have them I’ll update here (or make a new post), showing them.

And with this, there should be a complete setup for the email server. One can at the end run a checker, e.g. the one from easyDMARC or from MXToolbox.

What could still be improved (or added later):

  • DKIM key could be rotated (and this might be worth another post),
  • backing up the email .. somehow, somewhere,
  • backing up the server configuration,
  • monitoring & alerting: making sure that email actually flows.