How do I configure Postfix as a send-only mail server that requires TLS?

I'm trying to configure Postfix as a send-only mail server for administrative email alerts. I also want to require TLS (since I can guarantee that my personal email address, the only recipient, supports it) and follow the current recommendations for port usage of the SMTP spec. My account predates Linode's outgoing SMTP port restrictions, so I don't believe those are relevant. I should mention I'm using Postfix 3.4.13 on Ubuntu 20.04.2 LTS.

Here's the first thing I'm wondering. Suppose I've got something like this in /etc/aliases.

postmaster my-personal-address@gmail.com

Should I expect email addressed to postmaster@mydomain.net to get sent to my-personal-address@gmail.com via mail submission on port 587, transfer / relay on port 25, or either depending on other factors? Currently, my logs indicate that Postfix is only ever using port 25. It seems this can be altered by setting the relayhost directive, but I'm not sure I totally understand what this affects.

If alias resolution is usable for email submission on port 587, what configuration changes from an initial installation of Postfix are needed for it to do this? In particular, are client ("smtp") or server ("smtpd") configurations relevant in this case?

If not, can I still have Postfix require TLS over port 25 for outgoing transfers? This seems like it's atypical, so perhaps there are some best practices I'm unaware of for email transfer / relay over port 25 when external servers are involved. Note that in this case, I'd still potentially have applications submitting outgoing mail on port 587 directly (with no aliases involved), so the same concerns about how to configure Postfix for that would still apply.

11 Replies

Easy peasy!

Change

inet_interfaces = all

to

inet_interfaces = localhost

You should probably change

mynetworks = ...
mydestination = ...

to

mynetworks = 127.0.0.0/8, [ffff:127.0.0.0]/104, [::1]/128
#                                   ^
#                                   +-- IPv4 'localhost' wrapped in IPv6
#
mydestination = localhost.$mydomain, localhost, $myhostname

as well. Make sure, relay_host and relay_domains are set to nothing so your server can't relay mail on behalf of others:

relay_host =
relay_domains = 

For TLS, you want to set:

# Set up certificate
#
smtp_use_tls=yes
smtp_tls_cert_file=/path/to/cert.pem
smtp_tls_key_file=/path/to/privkey.pem 
smtp_tls_CAfile=/path/to/chain.pem
# 
smtpd_use_tls=yes
smtpd_tls_cert_file=/path/to/cert.pem
smtpd_tls_key_file=/path/to/privkey.pem 
smtpd_tls_CAfile=/path/to/chain.pem
#
smtpd_tls_received_header = yes
smtpd_tls_loglevel = 1
smtpd_tls_auth_only = no
smtpd_tls_security_level = may
smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3

smtp_* settings are for outbound; smtpd_* settings are for inbound (I think).

To use outbound TLS when the listener has it available, set

smtp       inet  n       -       y       -       -       smtpd
  -o smtpd_tls_security_level=may
  -o smtpd_sasl_auth_enable=no

in /etc/postfix/master.cf.

You don't really need submission (port 587)…unless you don't trust yourself. However, to enable it, set

submission inet  n       -       y       -       -       smtpd
  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject

in /etc/postfix/master.cf.

Whew! I think that's it… YMMV. This should get you started…

-- sw

Set up SMTP server for Postfix multi-domain sending only on Ubuntu 20.04, 18.04, 16.04

Step 1: Enter the host name and PTR record.
Step 3: Disable Postfix Email Receiving.
Step 4: Install and Configure OpenDKIM.
Step 5: Connect Postfix with OpenDKIM.
Step 6: Create SPF DNS Record.

@simba223 writes:

Set up SMTP server for Postfix multi-domain sending only on Ubuntu 20.04, 18.04, 16.04…

Step 7: Create DMARC DNS Record
Step 8: Install & configure OpenDMARC
Step 9: Connect Postfix with OpenDMARC

OpenDKIM and OpenDMARC are milters. Therefore, Steps 5 & 9 are accomplished my adding/modifying:

# milter configuraton
#
milter_default_action = accept
milter_protocol = 6
smtpd_milters = 
        unix:var/run/opendkim/opendkim.sock,
    unix:var/run/opendmarc/opendmarc.sock
non_smtpd_milters = $smtpd_milters

I happen to like local-domain sockets for this (for security and performance)…you may not. The milter socket specifications are up to you.

You should probably institute some mechanism for changing the OpenDKIM key periodically. I have a cron(8) job that runs monthly for this.

-- sw

Thanks for the suggestions, but a fair bit of what's been posted so far doesn't really seem to address my questions. Here's what I've worked out so far.

  • Directives prefixed with smtpd are indeed the ones related to server functionality (handling incoming traffic). Similarly, directives prefixed with smtp are the ones related to client functionality (handling outgoing traffic).
  • smtp_use_tls, smtp_enforce_tls, smtpd_use_tls, and smtpd_enforce_tls, are deprecated in favor of smtp_tls_security_level and smtpd_tls_security_level.
  • Setting smtp_tls_security_level or smtpd_tls_security_level to may enables opportunistic TLS on either the client or server end respectively. This is not what I think I want — since I know the only receiving client, TLS should be required. encrypt seems to be the correct setting for this.

After taking all this into account and setting smtp_loglevel to 1, I found that, going by the log messages, TLS connections are indeed being established. However, it also looks like all email is still being relayed over port 25.

I believe this is because the submission configuration in /etc/postfix/master.cf is an smtpd configuration — it doesn't determine client behavior, IE, how outbound mail is sent. So I still don't know how to configure Postfix to actually use port 587 for sending email to an external server, rather than relaying.

By the way, there appears to be at least one practical reason to care about using port 587 for my use case — I think using port 25 may be causing the email alerts to get delivered to my junk mailbox, despite using TLS.

Given the discrepancies above, I'm unclear on whether implementing DMARC is relevant.

@subjectoriented --

You write:

  • Directives prefixed with smtpd are indeed the ones related to server functionality (handling incoming traffic). Similarly, directives prefixed with smtp are the ones related to client functionality (handling outgoing traffic).
  • smtp_use_tls, smtp_enforce_tls, smtpd_use_tls, and smtpd_enforce_tls, are deprecated in favor of smtp_tls_security_level and smtpd_tls_security_level.
  • Setting smtp_tls_security_level or smtpd_tls_security_level to may enables opportunistic TLS on either the client or server end respectively. This is not what I think I want — since I know the only receiving client, TLS should be required. encrypt seems to be the correct setting for this.

You are right about all this. Thanks for the information. I've updated my configuration accordingly. I send/receive mail to/from the outside world.

You also write:

So I still don't know how to configure Postfix to actually use port 587 for sending email to an external server, rather than relaying.

I believe that submission as a protocol is supposed hand off email to smtp for relay to the external world. It's sort of the send version of IMAP…a protocol for dealing with MUAs…not MTAs.

You also write:

Given the discrepancies above, I'm unclear on whether implementing DMARC is relevant.

You're probably right. However, DMARC gives external recipient servers a mechanism to protect themselves. You may not need the DMARC server to handle incoming mail but you should have the DMARC TXT record for the benefit of others who receive mail from you.

-- sw

Thanks for the clarifications. I'll look into DMARC some more.

I believe that submission is as a protocol is supposed hand off email to smtp for relay to the external world. It's sort of the send version of IMAP…a protocol for dealing with MUAs…not MTAs.

OK, interesting. As I've understood it up to this point, submission is intended as the interface for the external world (from MUA to MSA), and relay is intended for use subsequently on the recipient's internal network (from MSA to MTA and from MTA to MTA). Now, I've seen that I can connect directly to external MTAs, but doing so kind of defeats the purpose of having MSAs out there in the first place.

Anyway, this whole description of mine might be wrong. If Postfix can only act as an MTA from the perspective of the remote recipient, then I guess the current behavior I'm seeing would be correct. However, that leaves me wondering when the smtp-prefixed client directives would ever apply. Even if I send email directly to my external address, my logs still only indicate the use of relay / port 25.

echo "This is a test message." | mail -s "Testing" my-personal-address@gmail.com

So, basically…am I just wrong to expect Postfix to ever act as an MUA, submitting my email to the remote MSA? Doesn't seem right…

You write:

…am I just wrong to expect Postfix to ever act as an MUA, submitting my email to the remote MSA? Doesn't seem right…

Postfix is an MSA/MTA. Period. Look at the diagram at the top of this page:

https://afreshcloud.com/sysadmin/mail-terminology-mta-mua-msa-mda-smtp-dkim-spf-dmarc

and imagine a message flowing from the sender on the left to the recipient on the right. It's pretty clear (at least to me) what functions postfix performs: MSA and MTA. This is exactly what you want (send only).

Port 587 is only used for the MUA to communicate with the MSA. There is no port required between MSA & MTA (this is postfix-internal IPC…you only need a port when crossing a system boundary). MTA-MTA communication across server boundaries happens on port 25 (otherwise your mail server won't play nice with the rest of the world). There is no short-circuit path between port 587 and the recipient's MTA on port 25.

Keys are negotiated between MUA & MSA on port 587. Keys MAY be negotiated on port 25 between MTAs…depending on the security setup for each MTA.

In my server, the MDA & POP/IMAP server are what dovecot does for me. There is no key negotiation between MTA & MDA in my world because the "port" is a local-domain socket. Keys are negotiated between the recipient's MUA and POP/IMAP on port 993.

-- sw

P.S. I would personally FIRE a sysadmin who let your MUA submit mail to my local (but remote from you) MSA… There's a name for people who try stuff like this…spammers.

Got it, looks like the root of my confusion was just misunderstanding the role of MSAs in relation to SMTP. I'll think through my configuration in terms of these corrected details and post what I come up with for any future forum searchers.

FWIW, users of the MSA on my email server are required to authenticate prior to submitting messages. The password is SHA-512 hashed & then looked up in a database. The same mechanism is used to authenticate IMAPs logins to collect received email.

The cert I use for postfix TLS is a Lets Encrypt cert (the same one I use for my web server).

You write:

looks like the root of my confusion was just misunderstanding the role of MSAs in relation to SMTP.

It's probably more useful to think of the components as MSA, MTA, MUA, MDA, etc…not as "the protocol" (SMTP). Variations of the same communication protocol are used for both internal and external communications among and between all the components.

-- sw

I got back to working on this, and was wondering if you could go into more detail on something you mentioned.

You should probably institute some mechanism for changing the OpenDKIM key periodically. I have a cron(8) job that runs monthly for this.

Could you share more about how that works for you?

Here's the script (with semi-literate documentation):

#!/bin/bash
#

ME=$(basename $0)

# Report Linode error
#
function linodeerror()
{
    local e

    e=$(echo "$2" | $JQ '.errors')
    if [ "$e" != "null" ]
    then
        echo $ME: Error on "'$1':" 1>&2
        rsn=$(echo "$e" | $JQ '.[].reason' | sed -e 's/\"//g')
        fld=$(echo "$e" | $JQ '.[].field' | sed -e 's/\"//g')
        echo "           Reason: ${rsn}." 1>&2
        [ "$fld" == "null" ] || echo "           Field(s): "$fld 1>&2
        exit 1
    fi
}

# Indent lines
#
function indent() 
{
  local indentSize=2
  local indent=1
  if [ -n "$1" ]; then indent=$1; fi
  pr -to $(($indent * $indentSize))
}

# Report JSON query error
#
function jqerror()
{
    echo $ME: JQ Error: 1>&2
    echo "$(cat $1 | indent 3)"
    echo "           Input: ${2}." 1>&2
    echo "           Query: ${3}." 1>&2
    rm -f $1
    exit 1
}

# domain name
#
DOMAIN=domain.com

# OpenDKIM user/group
#
USER=opendkim
GROUP=opendkim

# Token never expires
#
TOKEN='your Linode API token'

# you must install these if you don't have them
#
CURL=/usr/bin/curl
JQ=/usr/bin/jq      # 'jq' is a query tool for JSON

# Home directory
#
home=/home/stevewi
DIR=$home/tmp       # some /tmp directory

# file name components
#
KEY=mailkey
KEYLEN=1024  # you can change this to 2048 for a stronger key...not supported but works...
             # if you do this, you must change the two symlinks referenced below.

SELECTOR=${KEY}.${KEYLEN}
TXT=${SELECTOR}.txt
PRIVATE=${SELECTOR}.private
TXTREC=${SELECTOR}.txtrec

# key for DNS text record
#
TXTKEY='mailkey._domainkey'

# Generate the new keys
#
rm -f $DIR/${KEY}*
opendkim-genkey -s $SELECTOR -d $DOMAIN -b $KEYLEN -D $DIR

# form the TXT record contents 
#   ("sed -e ':a;/0$/!{N;s/\n//;ba}'" splits the public key 
#    into acceptable chunks)
#
grep -o '".*"' ${DIR}/${SELECTOR}.txt | \
    sed -e 's/"//g' | \
    sed -e ':a;/0$/!{N;s/\n//;ba}' | \
    sed -e 's/k\=rsa\;/k\=rsa\; c=relaxed\/simple\;/g' >$DIR/$TXTREC

# Ask linode for all my domains
#
req="https://api.linode.com/v4/domains"
resp=$($CURL -s -H "Authorization: Bearer $TOKEN" $req)
linodeerror "$req" "$resp"

# Find the domain I'm looking for
#
out=$(mktemp -p "$DIR" "XXXXXXX")
qry=".data[] | select(.domain == \"$DOMAIN\").id"
domid=$(echo "$resp" | $JQ "$qry" 2>>$out)
[ $? == 0 ] || jqerror "$out" "$resp" "$qry" && rm -f $out

# Ask Linode for all the records for the domain I'm interested in
#
req="https://api.linode.com/v4/domains/$domid/records"
resp=$($CURL -s -H "Authorization: Bearer $TOKEN" $req)
linodeerror "$req" "$resp"

# find the TXT record named 'mailkey._domainkey'
#
out=$(mktemp -p "$DIR" "XXXXXXX")
qry=".data[] | select(.type == \"TXT\") | select(.name == \"$TXTKEY\").id"
recid=$(echo "$resp" | $JQ "$qry" 2>>$out)
[ $? == 0 ] || jqerror "$out" "$resp" "$qry" && rm -f $out

# Update the TXT record with the new contents
#
resp=$($CURL -s -H "Content-Type: application/json" \
                -H "Authorization: Bearer $TOKEN" \
                -X PUT -d "{ \"target\": \"$(cat $DIR/$TXTREC)\" }" \
                https://api.linode.com/v4/domains/$domid/records/$recid)
linodeerror "$req" "$resp"

# Copy the new keys & change the permissions
#
mv $DIR/${TXT} /etc/opendkim
mv $DIR/${PRIVATE} /etc/opendkim
chown $USER:$GROUP /etc/opendkim/${TXT} /etc/opendkim/${PRIVATE}
rm -f $DIR/$TXTREC

exit 0

…and the crontab(5) entry:

# update the DKIM signing key (monthly)
#
@monthly  nice -n 19 sudo $HOME/bin/cron/updatedkim

You must either run this as root or sudo. I chose sudo. Here's the /etc/sudoer.d/stevewi entry:

stevewi ALL=(ALL) NOPASSWD: /home/stevewi/bin/cron/updatedkim

In /etc/opendkim, you must put the following symlinks (see script comment above about 2048-bit keys). You only have to do this once.

sudo ln -s mailkey.1024.private mailkey.private
sudo ln -s mailkey.1024.txt mailkey.txt

/etc/opendkim/mailkey.private is the private key file referenced in /etc/opendkim/KeyTable. /etc/opendkim/mailkey.txt is the public key file…it's used to create the file /etc/opendkim/mailkey.${KEYLEN}.txtrec which holds the DNS TXT record contents and is eventually removed after the DNS TXT record is installed.

The normal DNS propagation time applies.

Use at your own risk!

-- sw

Reply

Please enter an answer
Tips:

You can mention users to notify them: @username

You can use Markdown to format your question. For more examples see the Markdown Cheatsheet.

> I’m a blockquote.

I’m a blockquote.

[I'm a link] (https://www.google.com)

I'm a link

**I am bold** I am bold

*I am italicized* I am italicized

Community Code of Conduct