OpenSMTPD

These notes demonstrate basic configurations for running the SMTP server smtpd from OpenSMTPD. The context comprises exploration and testing within the comfort of a LAN suitably sheltered by firewall. There's some consideration for dispatching messages beyond the LAN via an email provider elsewhere but no consideration for receiving messages from the outside. The underlying motivation is to setup a local server for trying out email clients and for understanding connections to an external SMTP provider.

Preliminaries

OpenSMTPD implements an SMTP server for relaying email messages from a LAN and for accepting messages bound for the LAN. Use systemctl to start, query, or stop the server; for example:

=> systemctl start opensmtpd
=> systemctl status opensmtpd --full
‚óŹ opensmtpd.service - OpenSMTPD mail daemon
   Loaded: loaded (/usr/lib/systemd/system/opensmtpd.service; disabled; vendor preset: disabled)
   Active: active (running) since Thu 2015-07-30 10:31:28 EDT; 19s ago
  Process: 2907 ExecStart=/usr/sbin/smtpd (code=exited, status=0/SUCCESS)

Alternatively, you can start OpenSMTPD explicitly. For example, to have the server log its messages to stderr while you watch the action:

=> smtpd -d
info: OpenSMTPD 5.4.6p1 starting
info: startup
smtp-in: New session 91afa280f5997bf1 from host localhost.localdomain [127.0.0.1]
smtp-in: Started TLS on session 91afa280f5997bf1: version=TLSv1/SSLv3, cipher=ECDHE-RSA-AES128-GCM-SHA256, bits=128
smtp-in: Accepted message 8a6676f5 on session 91afa280f5997bf1: from=<alice@desktop.home>, to=<bob@desktop.home>, size=210, ndest=1, proto=ESMTP
smtp-in: Closing session 91afa280f5997bf1
delivery: Ok for 8a6676f59df96566: from=<alice@desktop.home>, to=<bob@desktop.home>, user=alice, method=mbox, delay=0s, stat=Delivered

See man page smtpd for other options.

Use smtpctl to control or query a running server. For example:

=> smtpctl show stats
control.session=1
uptime=2024
uptime.human=33m44s
=> smtpctl stop

The default configuration file is /etc/opensmtpd/smtpd.conf. See its man page smtpd.conf for documentation.

You can ask smtpd to check your configuration file after you've tinkered with it:

=> smtpd -n
configuration OK

Restart a running server to activate any modifications made to the configuration file after the server started:

=> systemctl restart opensmtpd

You can also specify an alternative configuration file when you invoke smtpd directly:

=> smtpd -f smtpd-25.conf -n
configuration OK
=> smtpd -f smtpd-25.conf -d
info: OpenSMTPD 5.7.1p1 starting
info: startup

Hello World

Here is complete configuration file for a server listening on port 25 for mail exchanged between accounts on the local host:

 => cat smtpd-25.conf
listen on localhost port 25
accept from local  for local  deliver to mbox

This server accepts mail sent from a local user to a local user. It delivers accepted email by adding a message to the receiver's mbox file (e.g. /var/spool/mail/$USER). It's a simple setup, but you can seek reassurance all the same:

=> smtpd -f smtpd-25.conf -n
configuration OK

Note the absence of support for TLS, authentication, and relaying.

You can observe a server running this configuration in action. First, start the server like so:

=> smtpd -f smtpd-25.conf -d
info: OpenSMTPD 5.4.6p1 starting
info: startup

Next, open another terminal, as an ordinary user, and send a message; for example:

-> swaks --server localhost --port 25 --suppress-data --to ray@localhost

Finally, return to the server's terminal to see the transcript of the exchange. In this instance:

smtp-in: session 8a998478df220bdb: connection from host localhost.localdomain [127.0.0.1] established
smtp-in: session 8a998478df220bdb: msgid=82dbdf15, status=Ok, from=<ray@desktop.home>, to=<ray@localhost>, size=384, ndest=1, proto=ESMTP
smtp-in: session 8a998478df220bdb: connection from host localhost.localdomain [127.0.0.1] closed (client sent QUIT)
delivery: Ok for 82dbdf15d30611c8: from=<ray@desktop.home>, to=<ray@localhost>, user=ray, method=maildir, delay=0s, stat=Delivered

For the record:

-> hostname --long
desktop.home

You can get smtpd to use the Maildir format with a simple adjustment to the accept rule above:

=> cat smtpd-25-maildir.conf 
listen on localhost port 25
accept from local  for local  deliver to maildir

The default directory is ~/Maildir, which smtpd is happy to create and initialize. You can instead specify a path to another location. For example, this rule uses ~/Mail-Test:

accept from local  for local  deliver to maildir "%{user.directory}/Mail-Test"

Use mailx to have a quick look:

-> MAIL=/home/ray/Mail-Test mailx -H -Sheadline="%S from %f"
"test Thu, 27 Aug 2015 11:21:55 -0400" from ray@desktop.home

See man page smtpd.conf for other methods of delivery offered by OpenSMTPD.

Localhost Recipients

Before accepting a message destined for the host machine, smtpd must associate the recipient with a user on the host so that it knows where to deliver messages. You can let smtpd use the system's database of valid users, or you can give it a separate database.

You can trace user look-ups in the server's transcript by adding option -T lookup.

=> smtpd -f smtpd-25.conf -d -T lookup

By default, smtpd defers the question of validity to the host system and thus requires a recipient to have a system account of the same name (cf. man page getpwname). In this way smtpd can properly deliver mail for, say, ray@localhost to /var/spool/mail/ray or /home/ray/Maildir.

Here's an excerpt from the server's transcript when it receives a message for ray@localhost:

lookup: lookup "ray" as USERINFO in table getpwnam:<getpwnam> -> "ray:1000:1000:/home/ray"
delivery: Ok for 9a585326381a0089: from=<ray@desktop.home>, to=<ray@localhost>, user=ray, method=mbox, delay=0s, stat=Delivered

A swaks client sees this:

 -> RCPT TO:<ray@localhost>
<-  250 2.1.5 Destination address valid: Recipient ok

In contrast, here's an excerpt for a message addressed to alice@localhost when the host does not acknowledge username alice:

lookup: lookup "alice" as USERINFO in table getpwnam:<getpwnam> -> 0
smtp-in: session 7ee12e09b3be665c: received invalid command: "RCPT TO:<alice@localhost>"

The sending client sees this:

 -> RCPT TO:<alice@localhost>
<** 550 Invalid recipient

You can tune an accept rule to consult an internal database of users instead of the system's database by adding a userbase filter for a userinfo table. Here is a complete configuration file establishing two users, Alice and Bob:

=> cat smtpd-25-userbase.conf
listen on localhost port 25
table userinfo {\
      alice     = 1000:1000:/home/ray \
      bob       = 1000:1000:/home/ray \
}
accept from local  for local  userbase <userinfo>  deliver to maildir

Each virtual name is associated with a host user by means of the corresponding UID, GID, and home directory. That's how smtpd knows where to deliver messages and what UID and GID to use on the underlying files. Here, messages for both Alice and Bob go to /home/ray/Maildir. Files underlying their messages have 1000 for both UID and GID. Any other recipient is rejected. Now, for example, smtpd drops messages to ray@localhost:

-> swaks --server localhost --port 25 --suppress-data --to ray@localhost

<** 550 Invalid recipient

While messages to alice@localhost and bob@localhost are delivered:

swaks --server localhost --port 25 --suppress-data --to alice@localhost,bob@localhost

 -> RCPT TO:<alice@localhost>
<-  250 2.1.5 Destination address valid: Recipient ok
 -> RCPT TO:<bob@localhost>
<-  250 2.1.5 Destination address valid: Recipient ok

Host-user ray (UID 1000) owns all of Alice's and Bob's files:

-> whoami 
ray
-> ls -l --numeric-uid-gid ~/Maildir/new
total 8
-rw-------. 1 1000 1000 437 Aug 28 11:25 1440775541.3182.desktop.home
-rw-------. 1 1000 1000 439 Aug 28 11:25 1440775541.3183.desktop.home
-> MAIL=~/Maildir mailx -H -Sheadline="%a %S from %f"
N "test Fri, 28 Aug 2015 11:25:41 -0400" from ray@desktop.home  
N "test Fri, 28 Aug 2015 11:25:41 -0400" from ray@desktop.home  

You can easily adjust the previous example to separate inboxes for Alice and Bob. You can also put their inboxes under /tmp for self-cleaning. Modify the method of delivery in the previous accept rule like so:

accept from local  for local  userbase <userinfo>  deliver to maildir "/tmp/Maildir/%{user.username}"

That's it. Messages for Alice now go to /tmp/Maildir/alice, and those for Bob go to /tmp/Maildir/bob. Files underlying their messages remain wholly owned and operated by user ray. Have a look:

-> swaks --server localhost --port 25 --suppress-data --to alice@localhost,bob@localhost

-> ls -l --numeric-uid-gid /tmp/Maildir
total 0
drwx------. 5 1000 1000 100 Aug 28 11:35 alice/
drwx------. 5 1000 1000 100 Aug 28 11:35 bob/
-> MAIL=/tmp/Maildir/alice mailx -H -Sheadline="%a %S from %f"
N "test Fri, 28 Aug 2015 11:35:59 -0400" from ray@desktop.home 
-> MAIL=/tmp/Maildir/bob mailx -H -Sheadline="%a %S from %f"
N "test Fri, 28 Aug 2015 11:35:59 -0400" from ray@desktop.home 

It appears that you cannot chain multiple accept rules governing local delivery in hopes of aggregating multiple user databases. For example, this configuration recognizes Alice but rejects Bob:

listen on localhost port 25
table alice { alice = 1000:1000:/home/ray }
table bob   { bob   = 1000:1000:/home/ray }
accept from local  for local  userbase <alice>  deliver to maildir
accept from local  for local  userbase <bob>    deliver to maildir

The server's transcript shows that smtpd (with -T lookup) rejects bob when the first rule fails; the second rule becomes moot:

lookup: lookup "bob" as USERINFO in table static:alice -> 0
smtp-in: session 676590607903181b: received invalid command: "RCPT TO:<bob@localhost>"

TLS Transactions

OpenSMTPD offers both STARTTLS and SMTPS for protecting transactions with TLS.

To enable TLS-protected transactions, you will need to provide a file holding the server's private key and a file holding a certificate for the corresponding public key. You can easily generate such files suitable for a test server using certtool or openssl. Here's how to create a private key in smtp-key.pem and a certificate in smtp-cert.pem using certtool, for example:

=> cd /etc/opensmtpd
=> certtool --generate-privkey --outfile smtp-key.pem
=> certtool --generate-self-signed --load-privkey smtp-key.pem --outfile smtp-cert.pem

Or using openssl:

=> cd /etc/opensmtpd
=> openssl genrsa -out smtp-key.pem 
Generating RSA private key, 1024 bit long modulus

=> openssl req -new -x509 -key smtp-key.pem -out smtp-cert.pem

In either case, you may want to peruse the command's man page to make sure you are happy with default settings. Verify that these files' permissions reflect the privacy or sociability of their respective keys:

=> ls -l smtp-*.pem
-rw-r--r--. 1 root root 1127 Jul 30 13:31 smtp-cert.pem
-rw-------. 1 root root 5829 Jul 30 13:28 smtp-key.pem

You need to tell smtpd about these files because the names and locations are not fixed. Add lines like these to the configuration file ahead of any listen rule that needs the key pair:

pki desktop certificate "/etc/opensmtpd/smtp-cert.pem"
pki desktop key         "/etc/opensmtpd/smtp-key.pem"

A subsequent listen rule can specify this key pair for TLS by including filter pki desktop. Here "desktop" is arbitray; you can use whatever name makes sense.

Here is complete configuration file for a server listening on port 465 for mail exchanged between accounts on the local host. This server requires SMTPS:

=> cat smtpd-465-smtps.conf
pki desktop certificate "/etc/opensmtpd/smtp-cert.pem"
pki desktop key         "/etc/opensmtpd/smtp-key.pem"
listen on localhost port 465 smtps pki desktop
accept from local for local deliver to mbox

You can send a test message like so:

-> swaks --server localhost --port 465 --tls-on-connect --suppress-data --to bob@localhost

The server's transcript looks like this:

smtp-in: New session 5853429930fad5bc from host localhost.localdomain [127.0.0.1]
smtp-in: Started TLS on session 5853429930fad5bc: version=TLSv1/SSLv3, cipher=ECDHE-RSA-AES128-GCM-SHA256, bits=128
smtp-in: Accepted message 64863989 on session 5853429930fad5bc: from=<alice@desktop.home>, to=<bob@localhost>, size=207, ndest=1, proto=ESMTP
smtp-in: Closing session 5853429930fad5bc
delivery: Ok for 6486398931a225b0: from=<alice@desktop.home>, to=<bob@localhost>, user=alice, method=mbox, delay=0s, stat=Delivered

If you forget option --tls-on-connect when calling swaks, you'll see a transcript like this:

=== Trying localhost:465...
=== Connected to localhost.
<** Timeout (30 secs) waiting for server response
 -> QUIT
<** Timeout (30 secs) waiting for server response
=== Connection closed with remote host.

The server's report will look like this:

smtp-in: session 6b927be9aa423958: connection from host localhost.localdomain [127.0.0.1] established
smtp-in: session 6b927be9aa423958: connection from host localhost.localdomain [127.0.0.1] closed (IO error: No SSL error)

Here is complete configuration file for a server listening on port 587 for mail exchanged between accounts on the local host. This server requires STARTTLS.

=> cat smtpd-587-tls-require.conf 
pki desktop certificate "/etc/opensmtpd/smtp-cert.pem"
pki desktop key         "/etc/opensmtpd/smtp-key.pem"
listen on localhost port 587 tls-require pki desktop
accept from local for local deliver to mbox

You can send a test message like so:

-> swaks --server localhost --port 587 --tls --suppress-data --to bob@localhost

The server's transcript looks like this:

smtp-in: New session 359ce604b984d17a from host localhost.localdomain [127.0.0.1]
smtp-in: Started TLS on session 359ce604b984d17a: version=TLSv1/SSLv3, cipher=ECDHE-RSA-AES128-GCM-SHA256, bits=128
smtp-in: Accepted message d76e3feb on session 359ce604b984d17a: from=<alice@desktop.home>, to=<bob@localhost>, size=207, ndest=1, proto=ESMTP
smtp-in: Closing session 359ce604b984d17a
delivery: Ok for d76e3feb5aebd934: from=<alice@desktop.home>, to=<bob@localhost>, user=alice, method=mbox, delay=1s, stat=Delivered

If you forget option --tls when calling swaks, you'll see the following error message in the transcript:

<** 530 5.5.1 Invalid command: Must issue a STARTTLS command first

The server's transcript will look like this:

smtp-in: session 03e3cf859df71c75: received invalid command: "MAIL FROM:<alice@desktop.home>"

If you want to accept STARTTLS connections but not require them, you can replace tls-require with tls in the listen rule above. In this case, calling swaks without option --tls succeeds, but the entire session is exchanged in plaintext.

Your server can offer both SMTPS on port 465 and STARTTLS on port 587 if you include a rule for each combination in your configuration file. For example:

 -> cat smtpd-tls.conf
pki desktop certificate "/etc/opensmtpd/smtp-cert.pem"
pki desktop key         "/etc/opensmtpd/smtp-key.pem"
listen on localhost port 465 smtps       pki desktop
listen on localhost port 587 tls-require pki desktop
accept from local for local deliver to mbox

To be explicit, this configuration requires a client to use either SMTPS on port 465 or STARTTLS on port 587.

In a listen rule, keyword secure opens port 465 to SMTPS connections and opens port 25 to both unencrypted and STARTTLS connections. It does not open port 587. Here is a complete configuration file:

=> cat smtpd-secure.conf
pki desktop certificate "/etc/opensmtpd/smtp-cert.pem"
pki desktop key         "/etc/opensmtpd/smtp-key.pem"
listen on localhost secure pki desktop
accept from local for local deliver to mbox

You can use nmap to see what ports open:

=> smtpd -f smtpd-secure.conf
=> nmap 127.0.0.1 -p T:25,465,587 -sV

PORT    STATE  SERVICE    VERSION
25/tcp  open   smtp       OpenSMTPD
465/tcp open   ssl/smtp   OpenSMTPD
587/tcp closed submission

Here are corresponding swaks commands for testing:

-> swaks --server localhost --port 25 --suppress-data --to bob@localhost

-> swaks --server localhost --port 25 -tls --suppress-data --to bob@localhost

-> swaks --server localhost --port 465 --tls-on-connect --suppress-data --to bob@localhost

It looks like you cannot use keywords port and secure in the same listen rule: "smtpd: invalid listen option: tls/smtps on same port".

Authenticated Transactions

OpenSMTPD supports authenticated sessions, wherein a client must provide the name and password of an eligible user. OpenSMTPD mandates a TLS connection (either STARTTLS or SMTPS) for authentication.

You can add authentication to a TLS connection by providing a file of names and passwords for eligible users. In your configuration file, a table directive identifies the file, and a listen rule invokes an auth filter to deputize the file:

table users "file:/etc/opensmtpd/users-lan.conf"
listen on localhost port 587 tls pki desktop auth <users>

The structure of the file is simple. Each line lists a user name and encrypted password separated by whitespace. You delegate encryption to smptctl. For example, here's a file establishing user alice with password "123" and user bob with password "abc" (excluding quotation marks):

=> cd /etc/opensmtpd
=> echo alice `smtpctl encrypt 123` >  users-lan.conf
=> echo bob   `smtpctl encrypt abc` >> users-lan.conf
=> cat users-lan.conf
alice $6$9W/lcP4b0mWuw44j$cSJU3T/uepnF56v7yyZn7l6BOE51cNyDi3wdi.zJEmnVEw1JjHDcEGY9NHlen1gtMvKQ4Is2Lbvr8mMFTpnvh0
bob $6$G85H/Z0PnF6ZyXha$T614udxonNZEvZNgkg52QrUgVC90tV/FDOeNbsr3hL9pmjaR1odL5Ow/P9yK3EPIxCv4G3ZTAwMq8Nk0yhNLU/

OpenSMPTD does not specify the name or location of the user-password file. Nor does it specify the name of the table referring to the file.

Here is a complete configuration file for a server listening on port 587 and expecting both STARTTLS and authentication:

=> cat smtpd-auth-file.conf
pki desktop certificate "/etc/opensmtpd/smtp-cert.pem"
pki desktop key         "/etc/opensmtpd/smtp-key.pem"
table users "file:/etc/opensmtpd/users-lan.conf"
listen on localhost port 587 tls pki desktop auth <users>
accept from local for local deliver to mbox

If you prefer, you can embed the table of user names and passwords in the configuration file itself by replacing the filename with an explicit mapping:

=> cat smtpd-auth-embed.conf 
pki desktop certificate "/etc/opensmtpd/smtp-cert.pem"
pki desktop key         "/etc/opensmtpd/smtp-key.pem"
table users {\
   alice = '$6$9W/lcP4b0mWuw44j$cSJU3T/uepnF56v7yyZn7l6BOE51cNyDi3wdi.zJEmnVEw1JjHDcEGY9NHlen1gtMvKQ4Is2Lbvr8mMFTpnvh0' \
   bob   = '$6$G85H/Z0PnF6ZyXha$T614udxonNZEvZNgkg52QrUgVC90tV/FDOeNbsr3hL9pmjaR1odL5Ow/P9yK3EPIxCv4G3ZTAwMq8Nk0yhNLU/' \
}
listen on localhost port 587 tls pki desktop auth <users>
accept from local for local deliver to mbox

For readability, you can add newlines with backslash escapes, as above. Apparently, you need to put the password string between quotation marks (single or double).

The table of user names and passwords is a credentials table, here in listener context because the listen rule applies the referring auth filter.

You can send a test message like so:

-> swaks --server localhost --port 587 --tls --auth --auth-user alice --auth-pass 123 --suppress-data --to bob@localhost

 ~> AUTH LOGIN
<~  334 VXNlcm5hbWU6
 ~> YWxpY2U= | 
<~  334 UGFzc3dvcmQ6
 ~> MTIz
<~  235 2.0.0: Authentication succeeded

Note that both sides encode the authentication dialog in base64:

-> echo VXNlcm5hbWU6 | base64 --decode; echo
Username:
-> echo YWxpY2U=  | base64 --decode ; echo
alice
-> echo UGFzc3dvcmQ6 | base64 --decode; echo
Password:
-> echo MTIz | base64 --decode; echo
123

Alternatively, you can have swaks prompt for the user, password, or both by omitting the corresponding options --auth-user and --auth-password. For example:

-> swaks --server localhost --port 587 --tls --auth --suppress-data --to bob@localhost
Username: alice
Password: 123

The server's transcript notes the authentication with little ado:


 authentication successful for user alice 

It looks like OpenSMTPD offers only types PLAIN and LOGIN for SMTP authentication. But it restricts authentication to TLS transactions and thus protects these otherwise insecure methods.

swaks supports other types of SMTP authentication in addition to PLAIN and LOGIN. By default, it tries the methods remitted by the server until it succeeds or exhausts the choices in failure. But you can control what authentication methods swaks tries and their order by specifying a suitable argument to the --auth option.

Relay to SMTP Provider

You can instruct your local SMTP server to relay messages to your email provider on the Internet and thus dispatch them beyond the LAN. You will need the URL of the provider's SMTP server, the server's port and security protocol, and the username and password for your external account—just as you would for configuring Thunderbird, Claws, Evolution, etc.

Here is a complete configuration file for a local server that relays its messages to external server smtp.example.net at a fictitious email provider. For initial simplicity, the local server skips both TLS and authentication for itself. The external SMTP provider does require authentication, however, and the configuration file thereby includes the username and password associated with that account. Three lines do the job:

=> cat smtpd-relay.conf
listen on localhost port 25
table accounts { alice = AliceSmith:Secret1 }
accept  from local  for any  relay via tls+auth://alice@smtp.example.net:587  auth <accounts>

The credentials table "accounts" associates a label (e.g. "alice") with a username (e.g. "AliceSmith") and password (e.g. "Secret1") together identifying an account with the email provider at example.net. OpenSMTPD does not stipulate the name for this table. And note that the password is plaintext, so exercise due caution. In the accept rule, all of the relay information gets packed into the funky URL-like string introduced by the via keyword. This string tells smtpd what server to contact and how to negotiate with it. Prefix tls+auth indicates that smtp.example.net requires STARTTLS and authentication, while suffix 587 gives the server's port. In between, the label alice tells smtpd what credentials to submit. smtpd looks these up in the accounts table as directed by the subsequent auth keyword.

You can send a test message like so:

-> swaks --server localhost --port 25 --suppress-data --from AliceSmith@example.com --to Charlotte@spider.web

The server's transcript notes both the local and relay transactions—something like this:

 connection from host localhost.localdomain [127.0.0.1] established
 msgid=45a77033, status=Ok, from=<AliceSmith@example.net>, to=<Charlotte@spider.web>, 
 connection from host localhost.localdomain [127.0.0.1] closed (client sent QUIT)
 connecting to tls://93.184.216.34:587 (smtp.example.net)
 TLS started version=TLSv1/SSLv3 (), cipher=DHE-RSA-AES256-SHA, bits=256
 server certificate verification succeeded
 status=Ok, from=<AliceSmith@example.net>, to=<Charlotte@spider.web>,  relay=93.184.216.34 (smtp.example.net), 
 connection closed, 1 message sent.

To modify the previous configuration for an external server requiring SMTPS on port 465 instead of STARTTLS on port 587, replace its portmanteau URL for the relay with this:

smtps+auth://alice@smtp.example.net:465

OpenSMTPD supports several more types of connection to the relay host. See the "relay via" method of delivery in the man page smtpd.conf.

Here is a complete configuration file that has smtpd relay messages from Alice through her email provider (e.g. example.com) and messages from Bob through his email provider (e.g. forinstance.web):

=> cat smtpd-relay-multi.conf
listen on localhost port 25
table AliceSmith { AliceSmith@example.com }
table BobJohnson { BobJohnson@forinstance.web }
table accounts { alice = AliceSmith:Secret1, bob = BobJohnson:Secret2 }
accept  for any  sender <AliceSmith>  relay via   tls+auth://alice@smtp.example.net:587    auth <accounts>
accept  for any  sender <BobJohnson>  relay via smtps+auth://bob@smtp.forinstance.web:465  auth <accounts>

Just for concision, both accept rules implicitly use the filter from local for the client's IP address. Each rule adds a sender filter that limits the rule's applicability to the sender listed in the specified table, a mailaddr table. You can list multiple addresses in a mailaddr table, too, should you need to relay multiple users to the same external account.

You can send a test messages like so:

-> swaks --server localhost --port 25 --suppress-data --from AliceSmith@example.com --to Charlotte@spider.web

-> swaks --server localhost --port 25 --suppress-data --from BobJohnson@forinstance.web --to Charlie@tuna.web