<feed xmlns="http://www.w3.org/2005/Atom"><title>BSD Audit</title><link href="https://bsd-audit.com/atom.xml" rel="self" type="application/atom+xml"/><link href="https://bsd-audit.com/"/><id>https://bsd-audit.com/</id><updated>2026-08-15T23:07:04+00:00</updated><author><name>Michael Boelen</name><email>michael.boelen@cisofy.com</email></author><rights>Copyright 2026, Michael Boelen</rights><entry><title>Latest changes</title><link href="https://bsd-audit.com/website/latest-changes/"/><id>https://bsd-audit.com/website/latest-changes/</id><author><name>Michael Boelen</name></author><published>2023-05-13T00:07:52+02:00</published><updated>2026-08-15T23:07:04+00:00</updated><content type="html">&lt;p>This page contains the most recent changes to the blog.&lt;/p>
</content></entry><entry><title>The fw_update utility explained and how it works</title><link href="https://bsd-audit.com/openbsd/fw_update-explained/"/><id>https://bsd-audit.com/openbsd/fw_update-explained/</id><author><name>Michael Boelen</name></author><published>2026-08-15T22:40:33+00:00</published><updated>2026-08-15T23:07:03+00:00</updated><content type="html"><![CDATA[<h2 id="what-is-fw_update">What is fw_update?</h2>
<p>OpenBSD has great man pages, so let&rsquo;s see what fw_update(8) has to say: &ldquo;install non-free firmware&rdquo;. The <strong>fw_update</strong> tool is to deal with firmware, or in other words, low-level software that runs on hardware (a device) itself. Another important part is the <strong>non-free</strong>, meaning firmware components that have a license that does not allow it to be bundled within OpenBSD directly.</p>
<p>The <em>fw_update</em> utility, written in shell script, helps you with downloading, installing, removing, and showing available firmware. Typical moments that the tool is being used is at the first boot of the system, after a system upgrade, or manually when new firmware is needed.</p>
<h2 id="when-to-use-fw_update">When to use fw_update?</h2>
<p>Under normal conditions you don&rsquo;t need to run <kbd class="hl-box hl-c"ZgotmplZ>fw_update</kbd> manually as it is scheduled to do this at the first boot of the system. After a system upgrade or when experiencing problems with firmware, then it might be worth to see if the tool finds new firmware.</p>
<p>An example where <em>fw_update</em> is being executed during the first boot of the system:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plain" data-lang="plain"><span class="line"><span class="cl">starting network daemons: sshd smtpd sndiod.
</span></span><span class="line"><span class="cl">running rc.firsttime
</span></span><span class="line"><span class="cl">fw_update: add vmm; update none; keep intel
</span></span><span class="line"><span class="cl">Checking for available binary patches...
</span></span></code></pre></div><p>In this example the &lsquo;vmm&rsquo; firmware is being added, no firmware is updated, and the &lsquo;intel&rsquo; firmware is kept as-is.</p>
<h2 id="how-is-required-firmware-detected">How is required firmware detected?</h2>
<p>The way <em>fw_update</em> works is like a train of smaller steps. Let&rsquo;s have a look.</p>
<h3 id="pattern-matching-in-kernel-messages">Pattern matching in kernel messages</h3>
<p>OpenBSD will keep a log with kernel messages. You will recognize parts of this log when running the <kbd class="hl-box hl-c"ZgotmplZ>dmesg</kbd> command. Another option is to have a look at the 



<span class="hl-box hl-f">/var/run/dmesg.boot</span>
     file. For <em>fw_update</em> both the output of <em>dmesg</em> as this file will be used as a source to determine available hardware components.</p>
<p>Based on the text strings available from the kernel messages, it will try to do pattern matching for available firmware. This is by doing the text string comparison against the file 



<span class="hl-box hl-f">/usr/share/misc/firmware_patterns</span>
     and see if anything returns. If so, then there is a candidate for installation.</p>
<h3 id="download-and-validation">Download and validation</h3>
<p>Upon the phase of installation, the firmware will be downloaded from a web server. Then it will be checked against a signed database with SHA256 hashes. This way the tool knows there is no corruption or unauthorized alterations of the files.</p>
<h3 id="firmware-installation">Firmware installation</h3>
<p>After the validation step, the downloaded tar file is then extracted under a subdirectory within <span class="hl-box hl-2">/etc/firmware</span>. For tracking purposes, the firmware is then marked &lsquo;installed&rsquo; like as they were normal packages, by creating a dummy package within the <span class="hl-box hl-2">/var/db/pkg</span> directory.</p>
<div class="highlight" hl_line="1"><pre tabindex="0" class="chroma"><code class="language-plain" data-lang="plain"><span class="line"><span class="cl"># ls -l /var/db/pkg/ | grep firmware
</span></span><span class="line"><span class="cl">drwxr-xr-x  2 root  wheel  512 Aug 11 14:03 intel-firmware-20260512v0
</span></span><span class="line"><span class="cl">drwxr-xr-x  2 root  wheel  512 Aug 11 15:31 vmm-firmware-1.16.3p1
</span></span></code></pre></div><p>Within these directories there will be two files:</p>
<ul>
<li><strong>+CONTENTS</strong></li>
<li><strong>+DESC</strong></li>
</ul>
<p>This first file contains the related files on disk, which are typically stored within their respective subdirectory under the <span class="hl-box hl-2">/etc/firmware</span> directory. The second file contains a small description of the firmware, the maintainer, and the location where to find more details.</p>
<p>If you use <kbd class="hl-box hl-c"ZgotmplZ>pkg_info</kbd> command to see what packages are installed, the firmware will show up nicely as well</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line hl"><span class="cl"># pkg_info
</span></span><span class="line"><span class="cl">gettext-runtime-1.0 GNU gettext runtime libraries and programs
</span></span><span class="line"><span class="cl">intel-firmware-20260512v0 microcode update binaries for Intel CPUs
</span></span><span class="line"><span class="cl">libiconv-1.19       character set conversion library
</span></span><span class="line"><span class="cl">libsodium-1.0.22    library for network communications and cryptography
</span></span><span class="line"><span class="cl">quirks-7.194        exceptions to pkg_add rules
</span></span><span class="line"><span class="cl">updatedb-0p0        pkg_add speed up cache
</span></span><span class="line"><span class="cl">vim-9.2.843         vi clone with many additional features
</span></span><span class="line"><span class="cl">vmm-firmware-1.16.3p1 firmware binary images for vmm(4) driver
</span></span></code></pre></div><h2 id="examples-of-using-fw_update">Examples of using fw_update</h2>
<p>To display the currently installed firmware on your system, run it with the <span class="hl-box hl-f">-l</span> option.</p>
<div class="highlight" hl_line="1"><pre tabindex="0" class="chroma"><code class="language-plain" data-lang="plain"><span class="line"><span class="cl"># fw_update -l
</span></span><span class="line"><span class="cl">intel
</span></span><span class="line"><span class="cl">vmm
</span></span></code></pre></div><p>To see the files that would be downloaded, combine options <span class="hl-box hl-f">-F</span> and <span class="hl-box hl-f">-l</span>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line hl"><span class="cl"># fw_update -F -l
</span></span><span class="line"><span class="cl">http://firmware.openbsd.org/firmware/7.9/SHA256.sig
</span></span><span class="line"><span class="cl">http://firmware.openbsd.org/firmware/7.9/intel-firmware-20260512v0.tgz
</span></span><span class="line"><span class="cl">http://firmware.openbsd.org/firmware/7.9/vmm-firmware-1.16.3p1.tgz
</span></span></code></pre></div><p>Manually deleting firmware is easy as well.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line hl"><span class="cl"># fw_update -d intel
</span></span><span class="line"><span class="cl">fw_update: delete intel
</span></span></code></pre></div><p>Need the firmware again? Just run it without any arguments.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line hl"><span class="cl"># fw_update
</span></span><span class="line"><span class="cl">fw_update: add intel; update none; keep vmm
</span></span></code></pre></div><p>Now you know the most important parts of how <em>fw_update</em> operates.</p>
<h2 id="special-thanks">Special thanks</h2>
<ul>
<li>Bryan Steele for providing additional insights</li>
</ul>
<h2 id="resources">Resources</h2>
<ul>
<li><a href="https://man.openbsd.org/fw_update.8">fw_update(8) man page</a><img class="m-l-sm" src="/images/icons/up-right-from-square.svg" height="16" width="16" loading="lazy" alt="External link" title="This link points to an external source">

  </li>
<li>Source file of fw_update</li>
</ul>
]]></content></entry><entry><title>About</title><link href="https://bsd-audit.com/about/"/><id>https://bsd-audit.com/about/</id><author><name>Michael Boelen</name></author><published>2026-07-15T20:38:09+00:00</published><updated>2026-08-13T09:34:28+00:00</updated><content type="html"><![CDATA[<p>If you are interested in BSD, this is the place for you. It&rsquo;s a sister blog of <a href="https://linux-audit.com/">Linux Audit</a><img class="m-l-sm" src="/images/icons/up-right-from-square.svg" height="16" width="16" loading="lazy" alt="External link" title="This link points to an external source">

   :-)</p>
<h2 id="goals">Goals</h2>
<p>The goal for BSD is to provide high-quality content and make it freely available. This way you can use it to administrate and secure your systems.</p>
<h2 id="who-is-writing">Who is writing?</h2>
<p>Many of the articles are written by <a href="https://michaelboelen.com/">Michael Boelen</a><img class="m-l-sm" src="/images/icons/up-right-from-square.svg" height="16" width="16" loading="lazy" alt="External link" title="This link points to an external source">

  . Michael is a security developer and founder of CISOfy. He covers topics within Linux security, from system hardening to compliance and regulations. As he likes BSD as well, this is an additional resource to give back to the community.</p>
<h2 id="feedback">Feedback</h2>
<p>Your comments, ideas, and feedback are welcome! If there is a particular subject you want to see covered in an article, then contact us. Please send us an email at blog @ cisofy.com.</p>
<h2 id="special-thanks">Special thanks</h2>
<p>This blog is possible due to some sponsoring by <a href="https://cisofy.com/">CISOfy</a><img class="m-l-sm" src="/images/icons/up-right-from-square.svg" height="16" width="16" loading="lazy" alt="External link" title="This link points to an external source">

  . Resources, such as hosting and time in particular, are invested into this blog. CISOfy wants to make BSD and Linux security accessible to a wider audience, from system administrators to auditors and developers.</p>
<h3 id="lynis">Lynis</h3>
































<figure>
    <picture>
      <source srcset="images/lynis-screenshot-800x600.webp" type="image/webp">
<img 
  sizes="(min-width: 35em) 1200px, 100vw"
  srcset='
  
    /about/images/lynis-screenshot-800x600_hu_e6c5837968eabfec.png 500w
  
  
    , /about/images/lynis-screenshot-800x600_hu_d9af1e875f50d54c.png 800w
  
  
  '
   src="/about/images/lynis-screenshot-800x600.png"
 alt="Lynis (Linux/Unix auditing tool) screenshot" loading="lazy">
    </picture>
    
    <figcaption>
      <p><i class="caption">Screenshot of a Unix security audit performed with Lynis.</i></p>
    </figcaption>
    
</figure>

<p>Lynis is a battle-tested technical security <a href="https://cisofy.com/lynis/">audit tool</a><img class="m-l-sm" src="/images/icons/up-right-from-square.svg" height="16" width="16" loading="lazy" alt="External link" title="This link points to an external source">

  . It is <a href="https://github.com/CISOfy/lynis/">open source</a><img class="m-l-sm" src="/images/icons/up-right-from-square.svg" height="16" width="16" loading="lazy" alt="External link" title="This link points to an external source">

  , freely available, and used by system administrators all over the world. Other users include IT auditors, security professionals like pentesters. Lynis can also be used as a client component in the Lynis Enterprise solution.</p>
<p>For those who maintain more than 10 systems, there is the Lynis Enterprise Suite. It is much more than just Lynis. It includes a central management interface, additional plugins for Lynis, reporting capabilities, a custom implementation guide and more helpful information. For example, it contains extended information regarding each security control. Besides that, it will show you how to solve a finding, manually or with ready-to-use hardening snippets.</p>
]]></content></entry><entry><title>Running your own mail server with OpenBSD and OpenSMTPD</title><link href="https://bsd-audit.com/self-hosting/running-your-own-mailserver/"/><id>https://bsd-audit.com/self-hosting/running-your-own-mailserver/</id><author><name>Michael Boelen</name></author><published>2026-08-03T15:14:17+00:00</published><updated>2026-08-11T11:49:25+00:00</updated><content type="html"><![CDATA[<h2 id="introduction">Introduction</h2>
<p>Self-hosting your own services such as a blog, website, or email is a valuable skill. It gives you a better understanding in the technology and usually also fun to do. With privacy under pressure and Big Tech continuing to store our data, this might be a good time to host your own mail server.</p>
<p>Email is a great service to host yourself, as it is based on decentral protocols. Each company, society, or individual can run a system of their own. Or, let someone else do it. If you wan to take back some control about your messaging needs, then this is the article for you.</p>
<p>Hosting your own email, together with your own domain name, gives you back the control over your communications. Make as many email addresses as you want and block them when spam comes in. Unique addresses helps also with finding the source of spam and decouple purchased services.</p>
<h2 id="before-we-start">Before we start</h2>
<p>This article is meant for those that really want to set up a secure system that can be fully tailored to their needs. You will learn a lot along the way, from DNS records, using SSL certificates, up to configuring the different software components.</p>
<h3 id="goals">Goals</h3>
<p>When setting up a mail server, you probably have own preferences. This article covers a system that:</p>
<ul>
<li>Accepts incoming email and be able to pull it in with IMAP</li>
<li>Applies basic filter rules to weed out badly configured systems</li>
<li>Scans incoming mail with more extensive spam filtering (Rspamd) as a second layer</li>
<li>Has a flexible configuration for domains, users, email addresses</li>
</ul>
<blockquote>
<p>This configuration uses the current version of OpenBSD at the time of writing.</p>
</blockquote>
<h3 id="target-audience">Target audience</h3>
<ul>
<li>Technical (usually Linux and/or BSD users)</li>
<li>People who like to dig deep and understand the full system</li>
<li>Anyone just learning about the possibilities or learn something new about OpenBSD or spam filtering</li>
</ul>
<h3 id="provide-feedback">Provide feedback</h3>
<p><div class="highlight-box">

We are living in a time that AI is creating content on the web. This article is 100% made by a human being and a way to contribute back to the BSD community. It may contain errors, missing information, or unclear sentences. Do you catch something that is unclear or incorrect? Send me any feedback so this article can be further improved.
</div>
</p>
<h3 id="software-components">Software components</h3>
<p>This article will cover the following software components:</p>
<ul>
<li>acme-client</li>
<li>Dovecot</li>
<li>httpd</li>
<li>OpenBSD</li>
<li>OpenSMTPD</li>
<li>Rspamd</li>
<li>SQLite3</li>
<li>unbound</li>
<li>unwind</li>
</ul>
<p>Most of these components will be covered in-depth, including its configuration. Some will be mentioned, but covered in a separate guide.</p>
<blockquote>
<p>Rationale SQLite: while a configuration can be done perfectly fine in normal text files, the step towards a database engine give additional flexibility, like on-the-fly disabling an account or alias. All data is stored in a single database file (.sqlite), making it easy to backup without any additional SQL dump required. SQLite can handle many connections. If you rather want to use a database engine like PostgreSQL, then you only need to tune a few things compared to a normal text file configuration.</p>
</blockquote>
<h2 id="required-knowledge-and-parts">Required knowledge and parts</h2>
<p>Setting up your own mail server server requires some technical knowledge. Some parts are fairly easy to set up, others require some attention to detail. If you just run a service daemon to accept mail, then you might end up being blocked by most of the internet. So setting up things carefully and correctly from day 1, will save you some trouble later.</p>
<p>This article will use OpenBSD, so if you have already experience with it, great. Used Linux before, then you might need to learn a few more things, mainly for system administration. Besides some operating system knowledge, you need to know or learn about DNS, some generic concepts of a mail server and mail boxes. While it all not being rocket science, there are definitely a few tricky things to get right. But when you do with the help of this article, you got a smooth running mail server.</p>
<p>Requirements:</p>
<ul>
<li>Domain name</li>
<li>Hosted virtual machine</li>
<li>Access to DNS</li>
<li>Time</li>
</ul>
<h3 id="domain-name">Domain name</h3>
<p>If you don&rsquo;t have domain name yet, you may need to require one. Typically this is done with the help of registrar, often a hosting provider. Don&rsquo;t know who to use as a registrar? Ask around with friends or on Mastodon.</p>
<h3 id="hosted-virtual-machine">Hosted virtual machine</h3>
<p>While you can host a virtual machine yourself, that is probably not the wisest thing to do. Internet connections of consumers are usually flagged as suspicious. In the past they were a frequent source of spam, so internet providers started to block outgoing mail from their customers. Also the receiving end might block incoming connections from dynamic blocks intended for home internet connectivity.</p>
<p>It&rsquo;s better to use a dedicated hosting company for your virtual machine. Reliability and trustworthiness are important. IP addresses and network blocks can be compared with good or bad neighborhoods. Are all the IP addresses around you spamming, scanning the web, and post bad stuff? Then this might affect the IP address of your virtual machine as well.</p>
<p>For hosting my mail server, I used <a href="https://openbsd.amsterdam/">OpenBSD Amsterdam</a><img class="m-l-sm" src="/images/icons/up-right-from-square.svg" height="16" width="16" loading="lazy" alt="External link" title="This link points to an external source">

  , which provides virtual systems running OpenBSD. Their audience is technical and being all BSD oriented, they are typically good netizen.</p>
<blockquote>
<p><strong>Disclaimer</strong>: I know the owner of OpenBSD Amsterdam, but he did not know upfront that I would become a customer, nor that I would create this article. While he provided feedback, he has no say in these words.</p>
</blockquote>
<p>After filling in the form to order a virtual machine, a welcome email was received with the details. It includes IP addresses, relevant hostnames, and tips. OpenBSD Amsterdam gives you access using SSH (public key authentication with your provided SSH key) and access to the console. This way you still manage the system if you lock yourself out, for example after applying too tight firewall rules.</p>
<h3 id="dns-configuration">DNS configuration</h3>
<p>An important part of running a mail server is configuring the DNS records correctly. This way other mail servers know how to connect yours and to validate several pieces of a bigger puzzle, such as SPF, DMARC, and more.</p>
<p>There are several types of DNS records. If you are less familiar with them, here is an introduction in several common types.</p>
<table>
	<thead>
			<tr>
					<th>Type</th>
					<th>Purpose</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>A</td>
					<td>Link an IP address to a hostname</td>
			</tr>
			<tr>
					<td>AAAA</td>
					<td>Link an IPv6 address to a hostname</td>
			</tr>
			<tr>
					<td>CAA</td>
					<td>Define entities that are allowed to register a certificate for your domain</td>
			</tr>
			<tr>
					<td>MX</td>
					<td>Define which servers are accepting email for the domain</td>
			</tr>
			<tr>
					<td>PTR</td>
					<td>Translate IP address to a hostname</td>
			</tr>
			<tr>
					<td>TXT</td>
					<td>Contains information with a wide variety of goals, such as domain validation, SPF, DMARC, and others</td>
			</tr>
	</tbody>
</table>
<p>Usually we use a name (hostname) when configuring DNS records. If there is not a specific name, but you want to refer to the domain itself, then use a at-sign (@) as its name. The record type always has to be defined. Depending on the record type, you then fill in also a value, such as an IP-address, hostname, or text string.</p>
<h2 id="first-steps-before-we-begin">First steps before we begin</h2>
<p>Don&rsquo;t just select a cheap hoster and install a system and hope for the best. As shared earlier, the IP address is an important piece in being able to reliably deliver messages to other. Take some preparation steps to validate that nothing is wrong with the IP addresses assigned to your system.</p>
<ul>
<li>Does the hoster already have a bad reputation for hosting spammers?</li>
<li>Is the IP address that will be assigned to your virtual machine dedicated to your system only?</li>
<li>Does the IP exist on a blacklist (RBL), e.g. Spamhaus?</li>
<li>Can you define the reverse DNS record (PTR)?</li>
</ul>
<p>The target audience of OpenBSD Amsterdam are technical people using OpenBSD. They benefit, like myself, if we all behave. Most of the BSD people like to be friendly netizen and don&rsquo;t become a source of trouble, like spam and unauthorized scans. That alone makes me believe that this IP block I&rsquo;m part will have a better-than-average reputation.</p>
<p>There are several online sources to test your IP address and if it listed on a blacklist or blocklist. For example, <a href="https://mxtoolbox.com/blacklists.aspx">MXToolBox</a><img class="m-l-sm" src="/images/icons/up-right-from-square.svg" height="16" width="16" loading="lazy" alt="External link" title="This link points to an external source">

   is well-known for this purpose. Check both the IPv4 and IPV6 address. Another interesting source is the website of <a href="https://www.talosintelligence.com/">Talos Intelligence</a><img class="m-l-sm" src="/images/icons/up-right-from-square.svg" height="16" width="16" loading="lazy" alt="External link" title="This link points to an external source">

   to retrieve information about the IP addresses.</p>
<h2 id="basic-configuration-of-the-system">Basic configuration of the system</h2>
<h3 id="adjust-openssh">Adjust OpenSSH</h3>
<p>Since we use SSH via public key authentication, let&rsquo;s disable normal password authentication.</p>
<p><code>rcctl set sshd flags -o PasswordAuthentication=no</code></p>
<p>Optionally, you can adjust the SSH port in <strong>/etc/ssh/sshd_config</strong>. It will not make the system more secure, but may reduce log clutter by a lot and even reduce some unneeded traffic.</p>
<p>It is possible to use a flag for the port number similar to that of <strong>PasswordAuthentication</strong>. Even if the configuration gets lost, it is easy to restore via console access.</p>
<p>Restart the SSH daemon after the change:</p>
<p><code>rcctl restart sshd</code></p>
<p>This might be a good time to check in a new terminal window that the connection on the new port still works.</p>
<blockquote>
<p>Want to be sure that authentication is turned off? <code>ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password -p PORT USER@HOSTNAME</code></p>
</blockquote>
<h3 id="firewall-pf">Firewall: pf</h3>
<p>Especially when the system is not actively in use yet, it is a great time to set up a firewall. This way we can poke some hole in the configuration based on actual need, instead of closing things down later on. Just make sure that SSH access is allowed for now. Then later add those additional services, like SMTP and IMAP.</p>
<p>OpenBSD uses <strong>pf</strong>, a great filtering solution. With just a few lines you may already have a great functional filter to block out the onslaught of bad bots.</p>
<blockquote>
<p>Want to more about pf? There are a lot of good tutorials available. If time allows, another one will be published here later.</p>
</blockquote>
<p>Good to know and check:</p>
<ul>
<li>Time synchronization: <code>ntpctl -s all</code></li>
<li>Update packages and updates: <code>syspatch -c</code></li>
<li>Retrieve a TXT record to validate DNS lookups functioning: <code>host -t txt openbsd.org</code></li>
</ul>
<h3 id="dns-caching-with-unbound-andor-unwind">DNS caching with unbound and/or unwind</h3>
<p>OpenBSD provides the option to use the <strong>unwind</strong> service. This light daemon helps with <em>caching</em> (remember) of all DNS requests. DNS is used to translate between computer hostnames and their IP addresses. As a mail server, and especially spam filtering, may cause a lot of DNS requests, setting up caching is a requirement. Don&rsquo;t skip it and do it right away.</p>
<p>Open <strong>/etc/unwind.conf</strong> and define the DNS servers that will do DNS lookups. There are few options, but let&rsquo;s start with an example of what the configuration may look like:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">forwarder { 2620:fe::fe DoT 2620:fe::9 DoT }
</span></span><span class="line"><span class="cl">preference { DoT }
</span></span></code></pre></div><p>Confirm that the configuration of unwind is correct with <code>unwind -n</code> before starting it.</p>
<p><code>rcctl enable unwind</code></p>
<p>Start unwind to it becomes active:</p>
<p><code>rcctl start unwind</code></p>
<p>Confirm that <strong>/etc/resolv.conf</strong> was updated and that it points to localhost (127.0.0.1) with a reference to unwind. Example output:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">nameserver 127.0.0.1 # resolvd: unwind
</span></span><span class="line"><span class="cl">lookup file bind
</span></span></code></pre></div><p>If you want to go a step further, using <strong>unbound</strong> is another great option. Configuring unbound is outside the scope of this article. If you decide to take this route, then adjust your <strong>/etc/resolv.conf</strong> file. Most likely you might need to disable <strong>resolvd</strong> to prevent dynamic configuration of the DNS server(s) when DHCP is being used.</p>
<p>Another option is to use both. In that case run Unbound on port 5353 so it does not collide with unwind. By using unwind, you don&rsquo;t have to fiddle with resolvd and when Unbound goes down, Unwind will temporarily select another DNS server. In <strong>/etc/unwind.conf</strong> configure that our instance is listening on the localhost (IPv6) on port <strong>5353</strong>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">forwarder {
</span></span><span class="line"><span class="cl">    ::1 port 5353
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><p>Important note: do not use a generic DNS resolver for your configuration. Your mail server may do a lot of DNS records in a short time, based on the checks that are performed. Common DNS resolvers such as those of Google, Cloudflare, and Quad9 might be rate-limited. This may impact your DNS resolution and proper processing of incoming email.</p>
<h2 id="email-security-privacy-and-anti-spam">Email security, privacy, and anti-spam</h2>
<p>There are several technologies that boost privacy, confidentiality, or the authenticity of email.</p>
<h3 id="spf">SPF</h3>
<p>SPF, short for Sender Policy Framework, tells other servers which servers are allowed to send mail for your domain. It is like an allow list and only trusted systems are included.</p>
<h3 id="dkim">DKIM</h3>
<p>DKIM (DomainKeys Identified Mail) helps by digitally signing messages. Only signed email with the right key, is considered to be legitimate. Your mail server will need to be configured to use the secret key to sign, and the public key is then used to see if it matches. The DKIM signature will be included in the headers of email messages. The receiving system, can then use DKIM to do the validation. If there is a match, you can be (more) sure that the right entity send out the email. A bit like SPF, but on a different level.</p>
<h3 id="dmarc">DMARC</h3>
<p>Using SPF and DKIM is a good start. The receiving server can do several checks and see if things look good on a first glance. With DMARC on top, we can also instruct the receiving party what to do if something is wrong, like an incorrectly signed message, or an IP address sending our mail that was not on the allow list.</p>
<p>DMARC defines what should happen if something is wrong, like still accepting it or rejecting it altogether.</p>
<p>DMARC also has <strong>alignment</strong>, which makes things a little bit more complex. Email can have a different email address as the sender and what is presented in the &lsquo;From:&rsquo; header of the email. This may happen when forwarding email or solutions like running mailing groups. So if you use this functionality, have a good look at the alignment specific setting of DMARC.</p>
<p>A few tips:</p>
<ul>
<li>Read up on DMARC, it has several options</li>
<li>Receive messages about DMARC issues with <strong>rua</strong></li>
<li>Start with <strong>p=none</strong> for an existing domain, until you got everything configured correctly, then switch to a more strict configuration. Add this to you To Do list. For a new domain, go for strict from the very first day.</li>
</ul>
<h3 id="mta-sts">MTA-STS</h3>
<p>Besides SPF, DKIM, and DMARC, is there also <strong>MTA-STS</strong>. It is short for Mail Transfer Agent Strict Transport Security and can be compared with HTST, where it forces HTTPS usage. Now with email that translated to we only want to accept encrypted email.</p>
<p>To leverage MTA-STS, a web server is needed. Fortunately, OpenBSD provides the <strong>httpd</strong> service for this.</p>
<blockquote>
<p>URL: <a href="https://mta-sts.example.org/.well-known/mta-sts.txt">https://mta-sts.example.org/.well-known/mta-sts.txt</a><img class="m-l-sm" src="/images/icons/up-right-from-square.svg" height="16" width="16" loading="lazy" alt="External link" title="This link points to an external source">

   (replace example.org)</p>
</blockquote>
<p>Create the directory structure with the help of <code>mkdir</code> using &lsquo;-p&rsquo; to create the full path.</p>
<p><code>mkdir -p /var/www/htdocs/mta-sts.example.org/.well-known</code></p>
<p>Change the ownership, including any subdirectories and files.</p>
<p><code>chown -R www:www /var/www/htdocs/mta-sts.example.org</code></p>
<p>Create the file <strong>mta-sts.txt</strong>:</p>
<p><code>vi /var/www/htdocs/mta-sts.example.org/.well-known/mta-sts.txt</code></p>
<p>Add enforcement instructions. Example:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">version: STSv1
</span></span><span class="line"><span class="cl">mode: enforce
</span></span><span class="line"><span class="cl">mx: mail.example.org
</span></span><span class="line"><span class="cl">mx: mail2.example.org
</span></span><span class="line"><span class="cl">max_age: 604800
</span></span></code></pre></div><p>What does this all mean? Let&rsquo;s have a look:</p>
<ul>
<li>version: version of STS, which is currently STSv1</li>
<li>mode: enforce, only allow encrypted connections</li>
<li>mx: for each mail server a single line (this example has two)</li>
<li>max_age: how long the content of this file can be cached in seconds (604800 is a week)</li>
</ul>
<p>Create the file and then later we complete it with a virtual host configuration.</p>
<h2 id="configure-dns">Configure DNS</h2>
<p>With all this knowledge about DNS, time to set things up. When the DNS configuration is complete, the last parts for SPF/DKIM/DMARC/MTA-STS can be completed.</p>
<h3 id="aaaaa-records">A/AAAA records</h3>
<ul>
<li>Name: mail</li>
<li>Type: A / AAAA</li>
<li>Points to: IP address of server</li>
<li>Related protocols: HTTP/HTTPS/SMTP</li>
</ul>
<p>Set up a record for IPv4 (A) and one for IPv6 (AAAA).</p>
<ul>
<li>Name: mta-sts</li>
<li>Type: A / AAAA</li>
<li>Points to: IP address of server</li>
<li>Related protocols: HTTPS</li>
</ul>
<p>Again one for IPv4 and one for IPV6.</p>
<h3 id="mx-records">MX records</h3>
<p>The MX records define our server(s) that can receive the incoming email. If you are serious about your email, you can create a second server that accepts the email when the primary is down.</p>
<ul>
<li>Name: @</li>
<li>Type: MX</li>
<li>Value: 10</li>
<li>Points to: mail.example.org</li>
</ul>
<p>If you use a backup mail server, it gets a higher number (e.g. 20). The MX record with the lowest number is considered to be tried first. If you have multiple MX records with the same value, then both can be used. Please note that spammers may try to use the backup server, as they may have less spam training or weaker configurations.</p>
<h3 id="caa-record">CAA record</h3>
<p>With the help of <a href="https://letsencrypt.org/">Let&rsquo;s Encrypt</a><img class="m-l-sm" src="/images/icons/up-right-from-square.svg" height="16" width="16" loading="lazy" alt="External link" title="This link points to an external source">

   we can create some certificated. To define that only this entity may provide certificates, we can lock this down.</p>
<ul>
<li>Name: @</li>
<li>Type: CAA</li>
<li>Points to: 0 issue &ldquo;letsencrypt.org&rdquo;</li>
</ul>
<p>This specific value consists of a flag, tag and text value:</p>
<ul>
<li>Flag: <strong>0</strong> means &rsquo;non-critical&rsquo; and that no special flags needs to be set</li>
<li>Tag: <strong>issue</strong> normal certificate (instead of wildcard)</li>
<li>CA: define the entities that may create a certificate for our domain</li>
</ul>
<h3 id="spf-configuration">SPF configuration</h3>
<p>SPF defines who can send emails that originate from our domain. You have to decide the best match:</p>
<ul>
<li>&ldquo;v=spf1 -all&rdquo;: no system may send email originating from my domain (perfect for domains that have no email)</li>
<li>&ldquo;v=spf1 a:mail.example.org -all&rdquo;: only &lsquo;mail&rsquo; can send</li>
<li>&ldquo;v=spf1 a:mailrelay.example.org mx -all&rdquo;: the mailrelay host can send, so can the systems linked to our MX records (more overhead, as it requires multiple DNS lookups!)</li>
<li>&ldquo;v=spf1 ip4:46.1.2.4 ip6:2a03:6000:abcd:efab::4 -all&rdquo;: only the mentioned IPv4 and IPv6 addresses mentioned may send out email</li>
</ul>
<p>I prefer the last one. Explicit and the least amount of DNS lookups required.</p>
<h3 id="dmarc-1">DMARC</h3>
<p>DMARC helps to define what should happen when something is wrong, such non-matching SPF or DKIM values.</p>
<ul>
<li>Name: _dmarc</li>
<li>Type: TXT</li>
<li>Point to: v=DMARC1;p=reject;sp=reject;adkim=s;aspf=s;rua=mailto:dmarc@example.org</li>
</ul>
<p>This sets everything to reject emails that are not properly signed according to DKIM or when the sender system is not listed in the SPF records. Also the alignment is set to strict.</p>
<p>DMARC can also be defined very loose, which might be a good one for existing domains. Just do some initial monitoring, then set it to a stricter policy.</p>
<ul>
<li>Points to: v=DMARC1;p=none;rua=mailto:dmarc@example.org</li>
</ul>
<blockquote>
<p>DMARC has many specific settings and you are advised to read a bit more about it, to better understand what it does.</p>
</blockquote>
<h4 id="primary-domain">Primary domain</h4>
<p>When using multiple domains, you can have the other domains use the primary domain in the configuration. For the primary domain itself we do a &ldquo;normal&rdquo; configuration.</p>
<ul>
<li>Name: @</li>
<li>Type: TXT</li>
<li>Points to: v=spf1 ip4:1.2.3.4 ip6:2a03:abc:abc:abc -all</li>
</ul>
<p>Want other domains to use this primary domain for their configuration? Then define an additional record on the primary domain that can be used as an &lsquo;include&rsquo; within the other domains: _spf ➞ v=spf1 ip4:1.2.3.4 ip6:2a03:abc:abc:abc -all</p>
<p>Example when using it within the other domains and pointing to the primary domain: @ ➞ v=spf1 include:_spf.example.org -all</p>
<h3 id="ptr-records-reverse-dns">PTR records (reverse DNS)</h3>
<p>Another important step is setting up reverse DNS, the so-called PTR records. These are to be configured with your (web) hoster, the one owning the IP block. We link the IP address back to the name, that is why we call this <em>reverse DNS</em>. How to configure this is different for each hoster. So look for &lsquo;PTR record&rsquo; in the web interface of your hoster or frequently asked questions.</p>
<p>To check if all is set up correctly:</p>
<p><code>host -t ptr 1.2.3.4</code></p>
<p>When checking the output of your IP addresses (check both IPv4 and IPv6!), confirm that it points back to the hostname of your mail server.</p>
<h2 id="web-server-configuration">Web server configuration</h2>
<p>Open <strong>/etc/httpd.conf</strong> and add the virtual hosts. We can add a generic section that will cover the challenges on port 80 by using a wildcard (*). This particular configuration snippet will help proving Let&rsquo;s Encrypt that we have ownership over this server. Only then it will issue a certificate.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">server &#34;*&#34; {
</span></span><span class="line"><span class="cl">    listen on * port 80
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    # ACME http-01 challenge
</span></span><span class="line"><span class="cl">    location &#34;/.well-known/acme-challenge/*&#34; {
</span></span><span class="line"><span class="cl">        # /var/www/acme
</span></span><span class="line"><span class="cl">        root &#34;/acme&#34;
</span></span><span class="line"><span class="cl">        request strip 2
</span></span><span class="line"><span class="cl">        directory no auto index
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">server &#34;example.org&#34; {
</span></span><span class="line"><span class="cl">    listen on * tls port 443
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    tls {
</span></span><span class="line"><span class="cl">        certificate &#34;/etc/ssl/example.org.fullchain.pem&#34;
</span></span><span class="line"><span class="cl">        key         &#34;/etc/ssl/private/example.org.key&#34;
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    gzip-static
</span></span><span class="line"><span class="cl">    hsts
</span></span><span class="line"><span class="cl">    root &#34;/htdocs/example.org&#34;
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">server &#34;mta-sts.example.org&#34; {
</span></span><span class="line"><span class="cl">    listen on * tls port 443
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    tls {
</span></span><span class="line"><span class="cl">        certificate &#34;/etc/ssl/mta-sts.example.org.fullchain.pem&#34;
</span></span><span class="line"><span class="cl">        key         &#34;/etc/ssl/private/mta-sts.example.org.key&#34;
</span></span><span class="line"><span class="cl">    }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    hsts
</span></span><span class="line"><span class="cl">    root &#34;/htdocs/mta-sts.example.org&#34;
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><p>Start <strong>httpd</strong> and make it active for the next reboot of the system.</p>
<p><code>rcctl start httpd &amp;&amp; rcctl enable httpd</code></p>
<blockquote>
<p>Optional: feel free to create a generic page (/var/www/htdocs/index.html) with some text.</p>
</blockquote>
<p>This configuration now can be used by <strong>acme-client</strong>, which will request the certificates at Let&rsquo;s Encrypt.</p>
<h2 id="certificates-for-https">Certificates for HTTPS</h2>
<p>OpenBSD uses the <strong>acme-client</strong> to request Let&rsquo;s Encrypt certificates. This way we can provide an HTTPS connection, which is also required when using MTA-STS.</p>
<p>Open <strong>/etc/acme-client.conf</strong> and define below the <strong>authority</strong> section the individual hostnames that require a certificate.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">authority letsencrypt {
</span></span><span class="line"><span class="cl">    api url &#34;https://acme-v02.api.letsencrypt.org/directory&#34;
</span></span><span class="line"><span class="cl">    account key &#34;/etc/acme/letsencrypt-privkey.pem&#34;
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">domain &#34;example.org&#34; {
</span></span><span class="line"><span class="cl">    alternative names { &#34;www.example.org&#34; }
</span></span><span class="line"><span class="cl">    domain key &#34;/etc/ssl/private/example.org.key&#34;
</span></span><span class="line"><span class="cl">    domain full chain certificate &#34;/etc/ssl/example.org.fullchain.pem&#34;
</span></span><span class="line"><span class="cl">    sign with letsencrypt
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">domain &#34;mail.example.org&#34; {
</span></span><span class="line"><span class="cl">    domain key &#34;/etc/ssl/private/mail.example.org.key&#34;
</span></span><span class="line"><span class="cl">    domain full chain certificate &#34;/etc/ssl/mail.example.org.fullchain.pem&#34;
</span></span><span class="line"><span class="cl">    sign with letsencrypt
</span></span><span class="line"><span class="cl">}
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">domain &#34;mta-sts.example.org&#34; {
</span></span><span class="line"><span class="cl">    domain key &#34;/etc/ssl/private/mta-sts.example.org.key&#34;
</span></span><span class="line"><span class="cl">    domain full chain certificate &#34;/etc/ssl/mta-sts.example.org.fullchain.pem&#34;
</span></span><span class="line"><span class="cl">    sign with letsencrypt
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><p>If all is set, request a certificate, for example for our &lsquo;mail&rsquo; hostname:</p>
<p><code>acme-client -v mail.example.org</code></p>
<p>With <code>crontab -e</code> we can edit the scheduled tasks and tell it to run a daily/weekly task for the renewals.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">30 	4	*	*	1	/usr/local/scripts/renew-certificates
</span></span></code></pre></div><p>Example script (<strong>/usr/local/scripts/renew-certificates</strong>):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="cp">#!/bin/sh
</span></span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">set</span> -o nounset
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">NEEDS_HTTPD_RELOADED</span><span class="o">=</span><span class="m">0</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">for</span> D in <span class="k">$(</span>awk <span class="s1">&#39;/^domain/ {print $2}&#39;</span> /etc/acme-client.conf <span class="p">|</span> tr -d <span class="s1">&#39;&#34;&#39;</span><span class="k">)</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    /usr/sbin/acme-client <span class="s2">&#34;</span><span class="si">${</span><span class="nv">D</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="o">[</span> <span class="nv">$?</span> -eq <span class="m">0</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span> <span class="nv">NEEDS_HTTPD_RELOADED</span><span class="o">=</span>1<span class="p">;</span> <span class="k">fi</span> 
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Only reload http daemon when a certificate was renewed or updated</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[</span> <span class="si">${</span><span class="nv">NEEDS_HTTPD_RELOADED</span><span class="si">}</span> -eq <span class="m">1</span> <span class="o">]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    /usr/sbin/rcctl reload httpd
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">exit</span> <span class="m">0</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># EOF</span>
</span></span></code></pre></div><h2 id="opensmtpd">OpenSMTPD</h2>
<p>To use SQLite, install the package:</p>
<p><code>pkg_add opensmtpd-table-sqlite</code></p>
<h3 id="create-mailbox-location">Create mailbox location</h3>
<p>You can choose where you want to store the mailboxes. I prefer them in the home directory of the <em>vmail</em> user. So it has to be created first.</p>
<p><code>mkdir -p /home/vmail/mailboxes</code></p>
<p>Create the account:</p>
<p><code>useradd -c &quot;Mail for virtual users&quot; -d /home/vmail -s /sbin/nologin -u 2000 -g =uid -L staff vmail</code></p>
<p>Change ownership of the directory:</p>
<p><code>chown -R vmail:vmail /home/vmail</code></p>
<h3 id="define-database-structure-and-content">Define database structure and content</h3>
<p>Open with the help of the <code>sqlite3</code> the database:</p>
<p><code>sqlite3 /etc/mail/smtpd.sqlite</code></p>
<p>Create the database table <strong>domains</strong> for your domains. The table <strong>credentials</strong> contains the accounts and <strong>virtuals</strong> the virtual email addresses and aliases.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">domains</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">id</span><span class="w"> </span><span class="nb">INTEGER</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="w"> </span><span class="n">AUTOINCREMENT</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">domain</span><span class="w"> </span><span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">255</span><span class="p">)</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">credentials</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">id</span><span class="w"> </span><span class="nb">INTEGER</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="w"> </span><span class="n">AUTOINCREMENT</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">email</span><span class="w"> </span><span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">255</span><span class="p">)</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">password</span><span class="w"> </span><span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">255</span><span class="p">)</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">is_active</span><span class="w"> </span><span class="nb">INTEGER</span><span class="w"> </span><span class="k">DEFAULT</span><span class="w"> </span><span class="mi">1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">CREATE</span><span class="w"> </span><span class="k">TABLE</span><span class="w"> </span><span class="n">virtuals</span><span class="w"> </span><span class="p">(</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">id</span><span class="w"> </span><span class="nb">INTEGER</span><span class="w"> </span><span class="k">PRIMARY</span><span class="w"> </span><span class="k">KEY</span><span class="w"> </span><span class="n">AUTOINCREMENT</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">email</span><span class="w"> </span><span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">255</span><span class="p">)</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">destination</span><span class="w"> </span><span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">255</span><span class="p">)</span><span class="w"> </span><span class="k">NOT</span><span class="w"> </span><span class="k">NULL</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">comment</span><span class="w"> </span><span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">255</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">hyperlink</span><span class="w"> </span><span class="nb">VARCHAR</span><span class="p">(</span><span class="mi">255</span><span class="p">),</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">is_active</span><span class="w"> </span><span class="nb">INTEGER</span><span class="w"> </span><span class="k">DEFAULT</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">expires_after</span><span class="w"> </span><span class="nb">DATE</span><span class="w"> </span><span class="k">DEFAULT</span><span class="w"> </span><span class="s1">&#39;2099-12-31&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">);</span><span class="w">
</span></span></span></code></pre></div><p>Add a domain into the related <em>domains</em> table using the <strong>INSERT</strong> statement:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">domains</span><span class="w"> </span><span class="p">(</span><span class="k">domain</span><span class="p">)</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="s1">&#39;example.org&#39;</span><span class="p">);</span><span class="w">
</span></span></span></code></pre></div><p>Got more domains? Then repeat this for every domain.</p>
<p>Create one or more accounts. For this we use the email address (for authentication purposes, similar to username) and create a password. The command <code>smtpctl encrypt</code> will wait for you to insert some text, then return the hashed password string</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line hl"><span class="cl"># smtpctl encrypt 
</span></span><span class="line"><span class="cl">paste-your-password-here
</span></span><span class="line"><span class="cl">$2b$09$CVUigOvEqpik3GAOcUaZ4.tyYzU1bvOeIRexAlBZ38QLT12AZoxh6
</span></span></code></pre></div><p>So we use this last line in our SQL query:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">credentials</span><span class="w"> </span><span class="p">(</span><span class="n">email</span><span class="p">,</span><span class="w"> </span><span class="n">password</span><span class="p">,</span><span class="w"> </span><span class="n">is_active</span><span class="p">)</span><span class="w"> </span><span class="k">VALUES</span><span class="w"> </span><span class="p">(</span><span class="s2">&#34;account123@example.org&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;$2b$09$CVUigOvEqpik3GAOcUaZ4.tyYzU1bvOeIRexAlBZ38QLT12AZoxh6:&#34;</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">);</span><span class="w">
</span></span></span></code></pre></div><p>Just the account is not enough to receive email. Link the email address and any additional aliases.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="cl"><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">virtuals</span><span class="w"> </span><span class="p">(</span><span class="n">email</span><span class="p">,</span><span class="w"> </span><span class="n">destination</span><span class="p">,</span><span class="w"> </span><span class="k">comment</span><span class="p">,</span><span class="w"> </span><span class="n">is_active</span><span class="p">)</span><span class="w"> </span><span class="k">VALUES</span><span class="p">(</span><span class="s2">&#34;account123@example.org&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;vmail&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;Primary email address for Account 123&#34;</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">virtuals</span><span class="w"> </span><span class="p">(</span><span class="n">email</span><span class="p">,</span><span class="w"> </span><span class="n">destination</span><span class="p">,</span><span class="w"> </span><span class="k">comment</span><span class="p">,</span><span class="w"> </span><span class="n">is_active</span><span class="p">)</span><span class="w"> </span><span class="k">VALUES</span><span class="p">(</span><span class="s2">&#34;mailalias1@example.org&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;account123@example.org&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;First alias for Account 123&#34;</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="k">INSERT</span><span class="w"> </span><span class="k">INTO</span><span class="w"> </span><span class="n">virtuals</span><span class="w"> </span><span class="p">(</span><span class="n">email</span><span class="p">,</span><span class="w"> </span><span class="n">destination</span><span class="p">,</span><span class="w"> </span><span class="k">comment</span><span class="p">,</span><span class="w"> </span><span class="n">is_active</span><span class="p">)</span><span class="w"> </span><span class="k">VALUES</span><span class="p">(</span><span class="s2">&#34;mailalias2@example.org&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;account123@example.org&#34;</span><span class="p">,</span><span class="w"> </span><span class="s2">&#34;Another alias for Account 123&#34;</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">);</span><span class="w">
</span></span></span></code></pre></div><blockquote>
<p>The first line links the email address to the &lsquo;vmail&rsquo; system account. For every account, create such a related entry. Then define any additional aliases, by linking them to the primary email address of the account.</p>
</blockquote>
<p>Open <strong>cat /etc/mail/sqlite.conf</strong> and define the path of the database, including the related database queries.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl"># Database
</span></span><span class="line"><span class="cl">dbpath /etc/mail/smtpd.sqlite
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># Queries
</span></span><span class="line"><span class="cl">query_alias       SELECT destination FROM virtuals WHERE email=? AND is_active=1 AND current_date &lt; expires_after;
</span></span><span class="line"><span class="cl">query_credentials SELECT email, password FROM credentials WHERE email=? AND is_active=1;
</span></span><span class="line"><span class="cl">query_domain      SELECT domain FROM domains WHERE domain=?;
</span></span></code></pre></div><h3 id="opensmtpd-configuration">OpenSMTPD configuration</h3>
<p>Open <strong>/etc/mail/smtpd.conf</strong> to configure OpenSMTPD. Below is an example configuration. I suggest adding a reference to this website (with version), so that you can refer to it if any changes were made.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl"># OpenSMTPD configuration v2026-01
</span></span><span class="line"><span class="cl"># Based on the work of https://bsd-audit.com
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">### Tables
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># Primary tables for mailboxes
</span></span><span class="line"><span class="cl">table aliases_local file:/etc/mail/aliases
</span></span><span class="line"><span class="cl">table credentials sqlite:/etc/mail/sqlite.conf
</span></span><span class="line"><span class="cl">table domains sqlite:/etc/mail/sqlite.conf
</span></span><span class="line"><span class="cl">table virtuals sqlite:/etc/mail/sqlite.conf
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># Other tables
</span></span><span class="line"><span class="cl">table bad_rdns_regexes file:/etc/mail/block-rdns-regexes.list
</span></span><span class="line"><span class="cl">table bad_sender_from_regexes file:/etc/mail/block-sender-regexes.list
</span></span><span class="line"><span class="cl">table bad_sender_domains file:/etc/mail/block-sender-domains.list
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">### Filters
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># Filter to combat spam
</span></span><span class="line"><span class="cl">filter rspamd      proc-exec &#34;filter-rspamd&#34;
</span></span><span class="line"><span class="cl">#filter senderscore proc-exec &#34;filter-senderscore -junkBelow 70 -slowFactor 5000 -scoreHeader&#34;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">filter check_fcrdns                  phase connect match !fcrdns junk
</span></span><span class="line"><span class="cl">filter check_rdns                    phase connect match !rdns junk
</span></span><span class="line"><span class="cl">filter check_rdns_for_dynamic_ranges phase connect match rdns regex &lt;bad_rdns_regexes&gt; junk
</span></span><span class="line"><span class="cl">filter blocklist_sender_from         phase mail-from match mail-from regex &lt;bad_sender_from_regexes&gt; reject &#34;550 bad sender?&#34;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">### Special options
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">smtp sub-addr-delim &#39;_&#39; # use underscore (_) instead of plus sign (+) as a delimiter, as a + is not always accepted and an underscore is typically not seen as a way to create multiple aliases referring to the same e-mail address 
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">### Certificates
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">pki mail.example.org cert &#34;/etc/ssl/mail.example.org.fullchain.pem&#34;
</span></span><span class="line"><span class="cl">pki mail.example.org key &#34;/etc/ssl/private/mail.example.org.key&#34;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">### Mail routing
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># Listen on localhost
</span></span><span class="line"><span class="cl">listen on lo0
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># Listen on our external interface to receive mail from other systems
</span></span><span class="line"><span class="cl">listen on egress port  25 tls         pki mail.example.org protocols &#34;secure&#34; ciphers &#34;secure&#34; filter { blocklist_sender_from, check_fcrdns, check_rdns, check_rdns_for_dynamic_ranges, rspamd, senderscore }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># Listen on our external interface to receive mail from trusted users for relaying purposes
</span></span><span class="line"><span class="cl"># (auth requires authentication and mask-src removes the originator &#39;from&#39; field)
</span></span><span class="line"><span class="cl">listen on egress port 587 tls-require pki mail.example.org protocols &#34;secure&#34; ciphers &#34;secure&#34; auth &lt;credentials&gt; mask-src filter { rspamd }
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">### Actions
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">action &#34;local_mail&#34; mbox alias &lt;aliases_local&gt;
</span></span><span class="line"><span class="cl">action &#34;outbound&#34; relay helo mail.example.org
</span></span><span class="line"><span class="cl">action &#34;store_mail&#34; maildir &#34;/home/vmail/mailboxes/%{dest.domain}/%{dest.user}&#34; virtual &lt;virtuals&gt;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">### Matches
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># Store mail for valid user aliases
</span></span><span class="line"><span class="cl">match from any for domain &lt;domains&gt; action &#34;store_mail&#34;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># Local mail can be delivered to local aliases
</span></span><span class="line"><span class="cl">match from local for local action &#34;local_mail&#34;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># Allow authenticated users or from the local system itself to send out e-mail
</span></span><span class="line"><span class="cl">match from any auth for any action &#34;outbound&#34;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># The local system may send mail out if needed
</span></span><span class="line"><span class="cl">match from local for any action &#34;outbound&#34;
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"># EOF
</span></span></code></pre></div><p>Notes:</p>
<ul>
<li>SenderScore is a method to check an incoming IP address on a rated trust score. The service that does this lookup is rate-limited and very easily returns code <strong>127</strong> meaning you hit the limit. As I did not get it properly to work, it has been disabled in this configuration and article.</li>
</ul>
<h3 id="spam-filtering">Spam filtering</h3>
<p>Besides the basic filters in OpenSMTPD to block spammers, the more powerful Rspamd software can be added. To make them work together, a filter is required. Also the installation of Rspamd and Redis is needed. Redis is used to store dynamic data, such as learning from discovered spam and valid messages (ham), including statistics.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">pkg_add opensmtpd-filter-rspamd rspamd redis
</span></span></code></pre></div><p>Let&rsquo;s already enable the services for the next reboot.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">rcctl enable rspamd
</span></span><span class="line"><span class="cl">rcctl enable redis
</span></span></code></pre></div><p>Rspamd will most likely work without any configuration. Unfortunately, it will not be as efficient. So tuning is definitely needed.</p>
<p>Want to better understand Rspamd? An in-depth article about Rspamd is planned. Stay tuned!</p>
<h3 id="start-the-services">Start the services</h3>
<p>Before starting or restart OpenSMTPD, perform a configuration test by using the &lsquo;-n&rsquo; option.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl"># smtpd -n
</span></span><span class="line"><span class="cl">configuration OK
</span></span></code></pre></div><p>If all is fine, like above, then restart OpenSMTPD.</p>
<p><code>rcctl restart smtpd</code></p>
<h2 id="dkim-1">DKIM</h2>
<p>Previously it is was common to create RSA keys with 1024 bits. The recommended size is 2048 now, but not every DNS provider allows the length of this longer key size. You may need to split the key over multiple DNS entries.</p>
<blockquote>
<p>A key size of 4096 bits is not recommended, due to compatibility and that there is a more modern replace available that allows stronger keys with a smaller footprint (ed25519)</p>
</blockquote>
<p>Another option is ed25519, modern and shorter in size. Not all mail servers may support this yet, but most will support it now or in the upcoming years. This support for older and the modern key type ed25519 provides a challenge: what to use? Use both! That provides compatibility for both old and new systems.</p>
<h3 id="option-1---rspamd">Option 1 - Rspamd</h3>
<p>If you want to leverage the spam filtering capabilities of Rspamd, then you can let it also do DKIM signing. Rspamd has the option to define multiple selectors for a given domain.</p>
<h4 id="directory-creation">Directory creation</h4>
<p>Create the related directory and adjust ownership and file permissions. This way only the rspamd user (_rspamd) can access the DKIM key.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">install -d -o root -g \_rspamd -m 770 /etc/mail/dkim
</span></span></code></pre></div><h4 id="key-generation">Key generation</h4>
<p>Start by creating a key with type of ed25519. Since we will be using multiple keys, concise naming convention will help and avoid confusion later on. For example &lsquo;dkim-YEAR-XY-TYPE&rsquo;, where year is the full year, a follow number of two numbers, and finally the type. This makes rotation easier, while still clearly defining what the key.</p>
<p><code>rspamadm dkim_keygen --domain example.org --selector dkim-2026-01-ed25519 --privkey /etc/mail/dkim/example.org-ed25519.key --type ed25519</code></p>
<p>The default option for the &lsquo;dkim_keygen&rsquo; subcommand is to show a DNS output. This information we will use after we created also the RSA key.</p>
<p>Now the RSA can be created.</p>
<p><code>rspamadm dkim_keygen --domain example.org --selector dkim-2026-01-rsa2048 --privkey /etc/mail/dkim/example.org-rsa2048.key --type rsa --bits 2048</code></p>
<h4 id="change-dns-records">Change DNS records</h4>
<p>After the keys have been created, we need to add them to the DNS configuration. One entry will be created for the ed25519 key and another separate one for RSA.</p>
<p>Tips:</p>
<ul>
<li>The name of the DNS record is listed on the left side of the output of <code>rspamadm dkim_keygen</code></li>
<li>Record type is <strong>TXT</strong></li>
<li>Value is v=DKIM1;k=ed25519;p=ABC</li>
<li>Or for RSA: v=DKIM1;k=rsa;p=XYZ</li>
<li>When using the ed25519 key, the text (ABC) will fit directly after &lsquo;p=&rsquo; (without the quotes). When using the RSA (2048 bits) key, copy the two text strings as one. That is without quotes and if your provider allows you to insert a long string of text.</li>
</ul>
<h4 id="check-permissions-and-dkim-configuration">Check permissions and DKIM configuration</h4>
<p>Ensure that user <strong>_rspamd</strong> can access the generated keys.</p>
<p>Open <strong>/etc/rspamd/local.d/dkim_signing.conf</strong> to insert the keys. Here is an example for a domain (example.com) with just one selector and another one with the set-up of two selectors.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">domain {
</span></span><span class="line"><span class="cl">  example.com {
</span></span><span class="line"><span class="cl">    selector = &#34;dkim-2026-01&#34;;
</span></span><span class="line"><span class="cl">    path = &#34;/etc/mail/dkim/dkim-example.com.key&#34;;
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">  example.org {
</span></span><span class="line"><span class="cl">    selectors [
</span></span><span class="line"><span class="cl">      {
</span></span><span class="line"><span class="cl">        path: &#34;/etc/mail/dkim/example.org-ed25519.key&#34;;
</span></span><span class="line"><span class="cl">        selector: &#34;dkim-2026-01-ed25519&#34;;
</span></span><span class="line"><span class="cl">      },
</span></span><span class="line"><span class="cl">      {
</span></span><span class="line"><span class="cl">        path: &#34;/etc/mail/dkim/example.org-rsa2048.key&#34;
</span></span><span class="line"><span class="cl">        selector: &#34;dkim-2026-01-rsa2048&#34;;
</span></span><span class="line"><span class="cl">      }
</span></span><span class="line"><span class="cl">    ]
</span></span><span class="line"><span class="cl">  }
</span></span><span class="line"><span class="cl">}
</span></span></code></pre></div><p>Another more extensive article about Rspamd will cover the usage of DKIM keys.</p>
<h3 id="option-2---manual">Option 2 - manual</h3>
<p>Another option is do everything by hand.</p>
<p>Create a RSA key of 2048 bits with the help of the <code>openssl</code> command.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">openssl genrsa -out /etc/mail/dkim/dkim-example.org-rsa2048.key 2048
</span></span><span class="line"><span class="cl">openssl rsa -in /etc/mail/dkim/dkim-example.org-rsa2048.key -pubout -out /etc/mail/dkim-example.org-rsa2048.pub
</span></span></code></pre></div><p>Check the permissions so that that Rspamd can access the key. Then we need to publish the public key via DNS. As the public key part consists of multiple lines, it needs to be combined to one string of text. Also the key type and version is needed. Here is a one-liner to do so:</p>
<p><code>printf &quot;v=DKIM1;k=rsa;p=%s\n&quot; $(grep -v '\-\-' /etc/mail/dkim/dkim-example.org-rsa2048.pub | tr -d '\n')</code></p>
<ul>
<li>Name: dkim-2026-01-rsa2048._domainkey</li>
<li>Type: TXT</li>
<li>Value: v=DKIM1;k=rsa;p=random-string</li>
</ul>
<h2 id="dovecot">Dovecot</h2>
<p>After setting up OpenSMTPD and our DNS, we can receive email. It would be great if we can also see those emails, for example via Thunderbird or another IMAP capable client. To provide this IMAP service, we can use the open source Dovecot.</p>
<p>Install Dovecot:</p>
<p><code>pkg_add dovecot</code></p>
<p>To grant users the option to automate things using filters, we need another package named <strong>dovecot-pigeonhole</strong>. It enables the use of services like <strong>Sieve</strong> and <strong>ManageSieve</strong>, for example to place spam automatically in the folder &lsquo;Junk&rsquo;.</p>
<p><code>pkg_add dovecot-pigeonhole</code></p>
<h3 id="open-files">Open files</h3>
<p>With the usage of the Maildir format of mailboxes, the number of open file may increase quickly. Open <strong>/etc/login.conf</strong> and allow it more open files.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">dovecot:\
</span></span><span class="line"><span class="cl">	:openfiles-cur=1024:\
</span></span><span class="line"><span class="cl">	:openfiles-max=2048:\
</span></span><span class="line"><span class="cl">        :tc=daemon:
</span></span></code></pre></div><h3 id="customizing-dovecot">Customizing Dovecot</h3>
<p>Dovecot has a lot of options, so investing time in reading the documentation might be worth it. Knowing about what options there are will help in better customizing the system to your needs, such as additional authentication methods, Sieve support, and generic support for several protocols.</p>
<p>This article focuses on a few things:</p>
<ul>
<li>Use only IMAP for pulling in the email</li>
<li>Usage of PLAIN authentication method, but with encryption</li>
<li>Describe our default mailbox structure</li>
<li>Use of Dovecot and Sieve scripts to automate spam handling</li>
</ul>
<p>Open <strong>/etc/dovecot/local.conf</strong> to customize your installation. Example configuration:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># Authentication type, user, and SSL certificates</span>
</span></span><span class="line"><span class="cl"><span class="n">auth_mechanisms</span> <span class="o">=</span> <span class="n">plain</span>
</span></span><span class="line"><span class="cl"><span class="n">first_valid_uid</span> <span class="o">=</span> <span class="mi">2000</span>
</span></span><span class="line"><span class="cl"><span class="n">first_valid_gid</span> <span class="o">=</span> <span class="mi">2000</span>
</span></span><span class="line"><span class="cl"><span class="n">ssl_cert</span> <span class="o">=</span> <span class="o">&lt;/</span><span class="n">etc</span><span class="o">/</span><span class="n">ssl</span><span class="o">/</span><span class="n">mail</span><span class="o">.</span><span class="n">example</span><span class="o">.</span><span class="n">org</span><span class="o">.</span><span class="n">fullchain</span><span class="o">.</span><span class="n">pem</span>
</span></span><span class="line"><span class="cl"><span class="n">ssl_key</span> <span class="o">=</span> <span class="o">&lt;/</span><span class="n">etc</span><span class="o">/</span><span class="n">ssl</span><span class="o">/</span><span class="n">private</span><span class="o">/</span><span class="n">mail</span><span class="o">.</span><span class="n">example</span><span class="o">.</span><span class="n">org</span><span class="o">.</span><span class="n">key</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Location of mail boxes</span>
</span></span><span class="line"><span class="cl"><span class="n">mail_location</span> <span class="o">=</span> <span class="n">maildir</span><span class="p">:</span><span class="o">/</span><span class="n">home</span><span class="o">/</span><span class="n">vmail</span><span class="o">/</span><span class="n">mailboxes</span><span class="o">/%</span><span class="n">d</span><span class="o">/%</span><span class="n">n</span>
</span></span><span class="line"><span class="cl"><span class="c1"># Location of plugins</span>
</span></span><span class="line"><span class="cl"><span class="n">mail_plugin_dir</span> <span class="o">=</span> <span class="o">/</span><span class="n">usr</span><span class="o">/</span><span class="n">local</span><span class="o">/</span><span class="n">lib</span><span class="o">/</span><span class="n">dovecot</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Sieve</span>
</span></span><span class="line"><span class="cl"><span class="n">managesieve_notify_capability</span> <span class="o">=</span> <span class="n">mailto</span>
</span></span><span class="line"><span class="cl"><span class="n">managesieve_sieve_capability</span> <span class="o">=</span> <span class="n">body</span> <span class="n">copy</span> <span class="n">environment</span> <span class="n">fileinto</span> <span class="n">envelope</span> <span class="n">include</span> <span class="n">mailbox</span> <span class="n">stop</span> <span class="n">variables</span> <span class="n">vnd</span><span class="o">.</span><span class="n">dovecot</span><span class="o">.</span><span class="n">pipe</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Using NFS? Then disable the usage of mmap(), otherwise skip this option</span>
</span></span><span class="line"><span class="cl"><span class="c1"># mmap_disable = yes</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Define how a mailbox should look like, including some folders</span>
</span></span><span class="line"><span class="cl"><span class="n">namespace</span> <span class="n">inbox</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">inbox</span> <span class="o">=</span> <span class="n">yes</span>
</span></span><span class="line"><span class="cl">  <span class="n">location</span> <span class="o">=</span>
</span></span><span class="line"><span class="cl">  <span class="n">mailbox</span> <span class="n">Archive</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">auto</span> <span class="o">=</span> <span class="n">subscribe</span>
</span></span><span class="line"><span class="cl">    <span class="n">special_use</span> <span class="o">=</span> \<span class="n">Archive</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="n">mailbox</span> <span class="n">Drafts</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">auto</span> <span class="o">=</span> <span class="n">no</span>
</span></span><span class="line"><span class="cl">    <span class="n">special_use</span> <span class="o">=</span> \<span class="n">Drafts</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="n">mailbox</span> <span class="n">Junk</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">auto</span> <span class="o">=</span> <span class="n">create</span> <span class="c1"># only create, don&#39;t subscribe to it</span>
</span></span><span class="line"><span class="cl">    <span class="n">special_use</span> <span class="o">=</span> \<span class="n">Junk</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="n">mailbox</span> <span class="n">Sent</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">auto</span> <span class="o">=</span> <span class="n">subscribe</span>
</span></span><span class="line"><span class="cl">    <span class="n">special_use</span> <span class="o">=</span> \<span class="n">Sent</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="n">mailbox</span> <span class="n">Trash</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">auto</span> <span class="o">=</span> <span class="n">no</span>
</span></span><span class="line"><span class="cl">    <span class="n">special_use</span> <span class="o">=</span> \<span class="n">Trash</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="n">prefix</span> <span class="o">=</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Plugins for Sieve</span>
</span></span><span class="line"><span class="cl"><span class="n">plugin</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Message marked as SPAM? Learn as SPAM</span>
</span></span><span class="line"><span class="cl">  <span class="n">imapsieve_mailbox1_before</span> <span class="o">=</span> <span class="n">file</span><span class="p">:</span><span class="o">/</span><span class="n">usr</span><span class="o">/</span><span class="n">local</span><span class="o">/</span><span class="n">lib</span><span class="o">/</span><span class="n">dovecot</span><span class="o">/</span><span class="n">sieve</span><span class="o">/</span><span class="n">learn</span><span class="o">-</span><span class="k">as</span><span class="o">-</span><span class="n">spam</span><span class="o">.</span><span class="n">sieve</span>
</span></span><span class="line"><span class="cl">  <span class="n">imapsieve_mailbox1_causes</span> <span class="o">=</span> <span class="n">COPY</span>
</span></span><span class="line"><span class="cl">  <span class="n">imapsieve_mailbox1_name</span> <span class="o">=</span> <span class="n">Junk</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Message marked as SPAM but moved? Learn as HAM</span>
</span></span><span class="line"><span class="cl">  <span class="n">imapsieve_mailbox2_before</span> <span class="o">=</span> <span class="n">file</span><span class="p">:</span><span class="o">/</span><span class="n">usr</span><span class="o">/</span><span class="n">local</span><span class="o">/</span><span class="n">lib</span><span class="o">/</span><span class="n">dovecot</span><span class="o">/</span><span class="n">sieve</span><span class="o">/</span><span class="n">learn</span><span class="o">-</span><span class="k">as</span><span class="o">-</span><span class="n">ham</span><span class="o">.</span><span class="n">sieve</span>
</span></span><span class="line"><span class="cl">  <span class="n">imapsieve_mailbox2_causes</span> <span class="o">=</span> <span class="n">COPY</span>
</span></span><span class="line"><span class="cl">  <span class="n">imapsieve_mailbox2_from</span> <span class="o">=</span> <span class="n">Junk</span>
</span></span><span class="line"><span class="cl">  <span class="n">imapsieve_mailbox2_name</span> <span class="o">=</span> <span class="o">*</span>
</span></span><span class="line"><span class="cl">  <span class="n">sieve</span> <span class="o">=</span> <span class="n">file</span><span class="p">:</span><span class="o">~/</span><span class="n">sieve</span><span class="p">;</span><span class="n">active</span><span class="o">=~/.</span><span class="n">dovecot</span><span class="o">.</span><span class="n">sieve</span>
</span></span><span class="line"><span class="cl">  <span class="n">sieve_global_extensions</span> <span class="o">=</span> <span class="o">+</span><span class="n">vnd</span><span class="o">.</span><span class="n">dovecot</span><span class="o">.</span><span class="n">pipe</span> <span class="o">+</span><span class="n">vnd</span><span class="o">.</span><span class="n">dovecot</span><span class="o">.</span><span class="n">environment</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Script location</span>
</span></span><span class="line"><span class="cl">  <span class="n">sieve_pipe_bin_dir</span> <span class="o">=</span> <span class="o">/</span><span class="n">usr</span><span class="o">/</span><span class="n">local</span><span class="o">/</span><span class="n">lib</span><span class="o">/</span><span class="n">dovecot</span><span class="o">/</span><span class="n">sieve</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Activate plugins for Sieve</span>
</span></span><span class="line"><span class="cl">  <span class="n">sieve_plugins</span> <span class="o">=</span> <span class="n">sieve_imapsieve</span> <span class="n">sieve_extprograms</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Extra script to mark SPAM marked email and move it to &#39;Junk&#39; folder</span>
</span></span><span class="line"><span class="cl">  <span class="n">sieve_after</span> <span class="o">=</span> <span class="n">file</span><span class="p">:</span><span class="o">/</span><span class="n">usr</span><span class="o">/</span><span class="n">local</span><span class="o">/</span><span class="n">lib</span><span class="o">/</span><span class="n">dovecot</span><span class="o">/</span><span class="n">sieve</span><span class="o">/</span><span class="n">spam</span><span class="o">-</span><span class="n">to</span><span class="o">-</span><span class="n">folder</span><span class="o">.</span><span class="n">sieve</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Used protocols (only IMAP and Sieve)</span>
</span></span><span class="line"><span class="cl"><span class="n">protocols</span> <span class="o">=</span> <span class="n">imap</span> <span class="n">sieve</span>
</span></span><span class="line"><span class="cl"><span class="n">protocol</span> <span class="n">imap</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">mail_plugins</span> <span class="o">=</span> <span class="err">$</span><span class="n">mail_plugins</span> <span class="n">imap_sieve</span>
</span></span><span class="line"><span class="cl">  <span class="n">mail_max_userip_connections</span> <span class="o">=</span> <span class="mi">10</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="n">protocol</span> <span class="n">sieve</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">log_path</span> <span class="o">=</span> <span class="o">/</span><span class="n">var</span><span class="o">/</span><span class="n">log</span><span class="o">/</span><span class="n">dovecot</span><span class="o">-</span><span class="n">sieve</span><span class="o">-</span><span class="n">errors</span><span class="o">.</span><span class="n">log</span>
</span></span><span class="line"><span class="cl">  <span class="n">info_log_path</span> <span class="o">=</span> <span class="o">/</span><span class="n">var</span><span class="o">/</span><span class="n">log</span><span class="o">/</span><span class="n">dovecot</span><span class="o">-</span><span class="n">sieve</span><span class="o">.</span><span class="n">log</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="n">service</span> <span class="n">imap</span><span class="o">-</span><span class="n">login</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="c1"># Disable IMAP by setting it to 0, this will disable port 143 but still enable IMAPS (port 993)</span>
</span></span><span class="line"><span class="cl">  <span class="n">inet_listener</span> <span class="n">imap</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">port</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="n">service</span> <span class="n">managesieve</span><span class="o">-</span><span class="n">login</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">inet_listener</span> <span class="n">sieve</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">port</span> <span class="o">=</span> <span class="mi">4190</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h3 id="dovecot-and-using-the-sqlite-database">Dovecot and using the SQLite database</h3>
<p>Open <strong>/etc/dovecot/dovecot-sql.conf.ext</strong> to define the usage of SQLite and the related database queries.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># Use SQLite</span>
</span></span><span class="line"><span class="cl"><span class="n">driver</span> <span class="o">=</span> <span class="n">sqlite</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Path to database</span>
</span></span><span class="line"><span class="cl"><span class="n">connect</span> <span class="o">=</span> <span class="o">/</span><span class="n">etc</span><span class="o">/</span><span class="n">mail</span><span class="o">/</span><span class="n">smtpd</span><span class="o">.</span><span class="n">sqlite</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Define how the password is stored</span>
</span></span><span class="line"><span class="cl"><span class="n">default_pass_scheme</span> <span class="o">=</span> <span class="n">BLF</span><span class="o">-</span><span class="n">CRYPT</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Authentication query to validate user accounts</span>
</span></span><span class="line"><span class="cl"><span class="n">password_query</span> <span class="o">=</span> \
</span></span><span class="line"><span class="cl">  <span class="n">SELECT</span> <span class="n">email</span> <span class="n">AS</span> <span class="n">user</span><span class="p">,</span> <span class="n">password</span> \
</span></span><span class="line"><span class="cl">  <span class="n">FROM</span> <span class="n">credentials</span> <span class="n">WHERE</span> <span class="n">email</span> <span class="o">=</span> <span class="s1">&#39;</span><span class="si">%u</span><span class="s1">&#39;</span> \
</span></span><span class="line"><span class="cl">  <span class="n">AND</span> <span class="n">password</span> <span class="n">NOT</span> <span class="n">NULL</span> <span class="n">AND</span> <span class="n">is_active</span> <span class="o">=</span> <span class="mi">1</span> \
</span></span><span class="line"><span class="cl">  <span class="n">LIMIT</span> <span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Define the user and where the mail box can be found</span>
</span></span><span class="line"><span class="cl"><span class="n">user_query</span> <span class="o">=</span> \
</span></span><span class="line"><span class="cl">  <span class="n">SELECT</span> <span class="s1">&#39;/home/vmail/</span><span class="si">%d</span><span class="s1">/%n&#39;</span> <span class="n">AS</span> <span class="n">home</span><span class="p">,</span> <span class="mi">2000</span> <span class="n">AS</span> <span class="n">uid</span><span class="p">,</span> <span class="mi">2000</span> <span class="n">AS</span> <span class="n">gid</span> \
</span></span><span class="line"><span class="cl">  <span class="n">FROM</span> <span class="n">credentials</span> <span class="n">WHERE</span> <span class="n">email</span> <span class="o">=</span> <span class="s1">&#39;</span><span class="si">%u</span><span class="s1">&#39;</span> \
</span></span><span class="line"><span class="cl">  <span class="n">AND</span> <span class="n">password</span> <span class="n">NOT</span> <span class="n">NULL</span> <span class="n">AND</span> <span class="n">is_active</span> <span class="o">=</span> <span class="mi">1</span> \
</span></span><span class="line"><span class="cl">  <span class="n">LIMIT</span> <span class="mi">1</span><span class="p">;</span>
</span></span></code></pre></div><h3 id="dovecot-configuration">Dovecot configuration</h3>
<p>Fully configured Dovecot? Great! As Dovecot combines default settings with your customized settings, it is wise to confirm that all is properly set up. This can be done using <code>doveconf | less</code> and look at the full configuration.</p>
<h3 id="sieve-configuration">Sieve configuration</h3>
<p>Create <strong>/usr/local/lib/dovecot/learn-as-spam.sieve</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sieve" data-lang="sieve"><span class="line"><span class="cl"><span class="kn">require</span> <span class="p">[</span><span class="s">&#34;vnd.dovecot.pipe&#34;</span><span class="p">,</span> <span class="s">&#34;copy&#34;</span><span class="p">,</span> <span class="s">&#34;imapsieve&#34;</span><span class="p">,</span> <span class="s">&#34;environment&#34;</span><span class="p">];</span>
</span></span><span class="line"><span class="cl"><span class="nb">if</span> <span class="err">environment</span> <span class="nt">:is</span> <span class="s">&#34;imap.cause&#34;</span> <span class="s">&#34;COPY&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="err">pipe</span> <span class="nt">:copy</span> <span class="s">&#34;rspamc&#34;</span> <span class="p">[</span><span class="s">&#34;learn_spam&#34;</span><span class="p">];</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>To learn normal mail (HAM) create <strong>/usr/local/lib/dovecot/learn-as-ham.sieve</strong>. When a message goes to the bin, then we take no action. However if we move it to another folder, then it most likely is good email. Then we learn that message being HAM.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sieve" data-lang="sieve"><span class="line"><span class="cl"><span class="kn">require</span> <span class="p">[</span><span class="s">&#34;vnd.dovecot.pipe&#34;</span><span class="p">,</span> <span class="s">&#34;copy&#34;</span><span class="p">,</span> <span class="s">&#34;imapsieve&#34;</span><span class="p">,</span> <span class="s">&#34;environment&#34;</span><span class="p">,</span> <span class="s">&#34;variables&#34;</span><span class="p">];</span>
</span></span><span class="line"><span class="cl"><span class="nb">if</span> <span class="err">environment</span> <span class="nt">:matches</span> <span class="s">&#34;imap.mailbox&#34;</span> <span class="s">&#34;*&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kd">set</span> <span class="s">&#34;mailbox&#34;</span> <span class="s">&#34;${1}&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="nb">if</span> <span class="nb">string</span> <span class="s">&#34;${mailbox}&#34;</span> <span class="p">[</span><span class="s">&#34;Trash&#34;</span><span class="p">]</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">stop</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="err">pipe</span> <span class="nt">:copy</span> <span class="s">&#34;rspamc&#34;</span> <span class="p">[</span><span class="s">&#34;learn_ham&#34;</span><span class="p">];</span>
</span></span></code></pre></div><p>We can also have the spam mails automatically be moved. A great function to keep the inbox clean, especially when a big spam run is active. Create the file <strong>/usr/local/lib/dovecot/move-spam-to-junk-folder.sieve</strong>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sieve" data-lang="sieve"><span class="line"><span class="cl"><span class="kn">require</span> <span class="p">[</span><span class="s">&#34;fileinto&#34;</span><span class="p">,</span><span class="s">&#34;mailbox&#34;</span><span class="p">];</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">if</span> <span class="nb">header</span> <span class="nt">:contains</span> <span class="s">&#34;X-Spam&#34;</span> <span class="s">&#34;Yes&#34;</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">fileinto</span> <span class="nt">:create</span> <span class="s">&#34;Junk&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">stop</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Scripts need to be compiled first, also after they are changed.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sievec /usr/local/lib/dovecot/learn-as-spam.sieve
</span></span><span class="line"><span class="cl">sievec /usr/local/lib/dovecot/learn-as-ham.sieve
</span></span><span class="line"><span class="cl">sievec /usr/local/lib/dovecot/move-spam-to-junk-folder.sieve
</span></span></code></pre></div><p>After running these commands, you should have three additional files with the file extension <strong>.svbin</strong>.</p>
<p>By default our rspamc binary is not available in the path where Sieve configuration is looking. This may prevent the &lsquo;pipe&rsquo; action to run it. After looking where rspamc is located (<code>whereis rspamc</code>), we can link it.</p>
<p><code>ln -s /usr/local/bin/rspamc /usr/local/lib/dovecot/sieve/rspamc</code></p>
<h3 id="connection-with-sqlite-database">Connection with SQLite database</h3>
<p>Open <strong>/etc/dovecot/conf.d/auth-sql.conf.ext</strong> for the configuration with our SQLite database and how authentication should occur.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># Define how passwords are looked up</span>
</span></span><span class="line"><span class="cl"><span class="n">passdb</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">driver</span> <span class="o">=</span> <span class="n">sql</span>
</span></span><span class="line"><span class="cl">  <span class="n">args</span> <span class="o">=</span> <span class="o">/</span><span class="n">etc</span><span class="o">/</span><span class="n">dovecot</span><span class="o">/</span><span class="n">dovecot</span><span class="o">-</span><span class="n">sql</span><span class="o">.</span><span class="n">conf</span><span class="o">.</span><span class="n">ext</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Define how users are looked up</span>
</span></span><span class="line"><span class="cl"><span class="n">userdb</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">driver</span> <span class="o">=</span> <span class="n">sql</span>
</span></span><span class="line"><span class="cl">  <span class="n">args</span> <span class="o">=</span> <span class="o">/</span><span class="n">etc</span><span class="o">/</span><span class="n">dovecot</span><span class="o">/</span><span class="n">dovecot</span><span class="o">-</span><span class="n">sql</span><span class="o">.</span><span class="n">conf</span><span class="o">.</span><span class="n">ext</span>
</span></span><span class="line"><span class="cl">  <span class="n">override_fields</span> <span class="o">=</span> <span class="n">uid</span><span class="o">=</span><span class="n">vmail</span> <span class="n">gid</span><span class="o">=</span><span class="n">vmail</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Open <strong>/etc/dovecot/dovecot-sql.conf.ext</strong> for defining the database settings.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># Define that we use SQLite</span>
</span></span><span class="line"><span class="cl"><span class="n">driver</span> <span class="o">=</span> <span class="n">sqlite</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Point to our SQLite database</span>
</span></span><span class="line"><span class="cl"><span class="n">connect</span> <span class="o">=</span> <span class="o">/</span><span class="n">etc</span><span class="o">/</span><span class="n">mail</span><span class="o">/</span><span class="n">smtpd</span><span class="o">.</span><span class="n">sqlite</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Define how passwords are saved</span>
</span></span><span class="line"><span class="cl"><span class="n">default_pass_scheme</span> <span class="o">=</span> <span class="n">BLF</span><span class="o">-</span><span class="n">CRYPT</span>
</span></span></code></pre></div><h3 id="ssl-configuration">SSL configuration</h3>
<p>Run the command <code>dovecot-mkcert.sh</code> to create a self-signed certificate. We then can replace this for our own certificate.</p>
<h3 id="sieve-support">Sieve support</h3>
<p>Most likely <strong>/etc/dovecot/conf.d/20-sieve.conf</strong> already exists on your system. It enables Sieve support.</p>
<blockquote>
<p>Note: The reference to port 2000 (name: managesieve_deprecated) can be removed, as it is an older implementation.</p>
</blockquote>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">protocols</span> <span class="o">=</span> <span class="err">$</span><span class="n">protocols</span> <span class="n">sieve</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">service</span> <span class="n">managesieve</span><span class="o">-</span><span class="n">login</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">inet_listener</span> <span class="n">sieve</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">port</span> <span class="o">=</span> <span class="mi">4190</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">plugin</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="n">sieve</span> <span class="o">=</span> <span class="o">~/.</span><span class="n">dovecot</span><span class="o">.</span><span class="n">sieve</span>
</span></span><span class="line"><span class="cl">  <span class="n">sieve_dir</span> <span class="o">=</span> <span class="o">~/</span><span class="n">sieve</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h3 id="start-dovecot">Start Dovecot</h3>
<p>If the configuration has been completed, finish it by enabling the service and then start it.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">rcctl enable dovecot
</span></span><span class="line"><span class="cl">rcctl start dovecot
</span></span></code></pre></div><h2 id="accounts">Accounts</h2>
<p>Password generation can be done using the <code>smtpctl</code> command with the <strong>encrypt</strong> subcommand.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line hl"><span class="cl"># smtpctl encrypt 
</span></span><span class="line"><span class="cl">paste-your-password-here
</span></span><span class="line"><span class="cl">$2b$09$CVUigOvEqpik3GAOcUaZ4.tyYzU1bvOeIRexMlNZ38QLT12AZoxh6
</span></span></code></pre></div><p>To test if it works:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line hl"><span class="cl"># doveadm auth test account@example.org 
</span></span><span class="line"><span class="cl">Password: 
</span></span><span class="line"><span class="cl">passdb: account@example.org auth succeeded
</span></span><span class="line"><span class="cl">extra fields:
</span></span><span class="line"><span class="cl">  user=account@example.org
</span></span></code></pre></div><h2 id="additional-tips">Additional tips</h2>
<h3 id="testing-mails">Testing mails</h3>
<p>Test the configuration by sending an email with the <code>mail</code> command. Include the sender, receiver, and a subject. Copy-paste some text and on a new line add just a dot to close the conversation.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plain" data-lang="plain"><span class="line hl"><span class="cl"># mail -s test -r sender@example.org receiver@example.org
</span></span><span class="line"><span class="cl">This is a wonderful message, or so I believe.
</span></span><span class="line"><span class="cl">.
</span></span></code></pre></div><blockquote>
<p>Note: Yes, the &lsquo;-r&rsquo; is confusing. It refers to the <strong>sender</strong> not the receiver</p>
</blockquote>
<h3 id="useful-commands">Useful commands</h3>
<h4 id="dovecot-1">Dovecot</h4>
<ul>
<li>doveadm auth test (test authentication)</li>
<li>doveadm kick (disconnect users)</li>
<li>doveadm mailbox list (show mailboxes)</li>
<li>doveadm log errors (show errors)</li>
<li>doveadm reload (reload the configuration)</li>
<li>doveadm stats (show statistics)</li>
<li>doveadm user (show available users)</li>
</ul>
<h4 id="opensmtpd-1">OpenSMTPD</h4>
<ul>
<li>smtpctl encrypt (generate a password for new accounts)</li>
</ul>
<h3 id="system-management">System management</h3>
<p>Keep your system up-to-date with the following commands. Consult the man page if you are not familiar with these commands yet.</p>
<ul>
<li>syspatch</li>
<li>fw_update</li>
<li>pkg_add -u</li>
<li>sysupgrade</li>
</ul>
<h4 id="useful-resources">Useful resources</h4>
<ul>
<li>internet.nl (check your mail configuration and DNS configuration externally)</li>
<li>Check your IP at <a href="https://check.spamhaus.org/">Spamhaus</a><img class="m-l-sm" src="/images/icons/up-right-from-square.svg" height="16" width="16" loading="lazy" alt="External link" title="This link points to an external source">

  </li>
</ul>
<h2 id="overview-of-the-dns-records">Overview of the DNS records</h2>
<p>These are the TXT records:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">_dmarc		TXT		1 hour	v=DMARC1;p=reject;sp=reject;adkim=s;aspf=s;rua=mailto:dmarc@example.org
</span></span><span class="line"><span class="cl">_mta-sts	TXT		1 hour	v=STSv1; id=20260605
</span></span><span class="line"><span class="cl">_smtp._tls	TXT		1 hour	v=TLSRPTv1; rua=mailto:tlsrpt@example.org;
</span></span><span class="line"><span class="cl">_spf		TXT		1 hour	v=spf1 ip4:1.2.3.4 ip6:2a03:6000:aaaa:bbbb:::123 -all
</span></span></code></pre></div><p>Most likely you will have the same ones. Something missing? Let me know.</p>
<h2 id="errors-and-troubleshooting-tips">Errors and troubleshooting tips</h2>
<p>Errors happen, so here are a few that came up during the configuration.</p>
<h3 id="lookup-table-proc-en-lka-socket-closed-opensmtpd">lookup: table-proc en lka socket closed (OpenSMTPD)</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">Jun  1 10:43:35 mail smtpd[60534]: lookup: table-proc: unexpected EOF during handshake
</span></span><span class="line"><span class="cl">Jun  1 10:43:35 mail smtpd[67030]: smtpd: process lka socket closed
</span></span></code></pre></div><p>This is caused by an incorrect SQL query. The wrong database table was used. Unfortunately nothing useful shows up in OpenSMTPD itself, so you also have to look in any external components linked to OpenSMTPD, such as Rspamd.</p>
<h3 id="dovecot-2">Dovecot</h3>
<p>Also in Dovecot it is fairly easy to end up with some error message. Especially during the configuration of Dovecot it is useful to check the log file now and then.</p>
<h4 id="password-query-failed-no-such-table">Password query failed: No such table</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">Jun  1 21:18:28 mail dovecot: auth-worker(5269): Error: conn unix:auth-worker (pid=64967,uid=518): auth-worker&lt;2&gt;: sql(myuser@mydomain.eu): Password query failed: no such table: credentials
</span></span></code></pre></div><blockquote>
<p>The database defined in dovecot-sql.conf.ext was incorrect</p>
</blockquote>
<h4 id="password-query-failed-no-such-column">Password query failed: no such column</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">Jun  1 21:21:56 mail dovecot: auth-worker(98895): Error: conn unix:auth-worker (pid=63146,uid=518): auth-worker&lt;1&gt;: sql(auth-michael@example.org): Password query failed: no such column: active
</span></span></code></pre></div><blockquote>
<p>Incorrect database field was used, so SQL query did not work. In dit case a missing field &lsquo;active&rsquo; that prevented the SQL query to work correctly.</p>
</blockquote>
<h4 id="failed-to-pipe-message-to-program-rspamc">Failed to pipe message to program rspamc</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">dovecot: imap(account@example.org)&lt;96356&gt;&lt;vIZjqddUouktjpAw&gt;: Error: sieve: failed to pipe message to program `rspamc&#39;: refer to server log for more information. [2026-06-22 15:31:43]
</span></span><span class="line"><span class="cl">dovecot: imap(account@example.org)&lt;96356&gt;&lt;vIZjqddUouktjpAw&gt;: Error: sieve: Execution of script /usr/local/lib/dovecot/sieve/learn-as-ham.sieve failed
</span></span></code></pre></div><p>rspamc was not accessible in the defined path within the Dovecot configuration (/etc/dovecot/local.conf).</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">sieve_pipe_bin_dir = /usr/local/lib/dovecot/sieve
</span></span></code></pre></div><p>To resolve the issue, create a symlink between rspamc and the defined file path.</p>
<p><code>ln -s /usr/local/bin/rspamc /usr/local/lib/dovecot/sieve/rspamc</code></p>
<h4 id="permission-denied-of-dkim-key">Permission denied of DKIM key</h4>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">2026-06-27 23:56:06 #85922(normal) &lt;004d42&gt;; task; dkim_module_load_key_format: cannot load dkim key /etc/mail/dkim/private/dkim-example.org.key: cannot stat key file: &#39;/etc/mail/dkim/private/dkim-example.org.key&#39; Permission denied
</span></span></code></pre></div><p>The file permissions might be too tight.</p>
<h2 id="sources">Sources</h2>
<p>Many sources cover OpenBSD, so it would be silly to reinvent the wheel. While I got inspired by some of the configuration snippets, I found none that fully covered this topic in-depth, hence the reason to create this extensive article. Below are some of the sources that I found, that might be worth also reading.</p>
<ul>
<li><a href="https://jmmr.dev/posts/dynamic-ip-ssh-allow-list-with-pf/">https://jmmr.dev/posts/dynamic-ip-ssh-allow-list-with-pf/</a><img class="m-l-sm" src="/images/icons/up-right-from-square.svg" height="16" width="16" loading="lazy" alt="External link" title="This link points to an external source">

  </li>
<li><a href="https://jkossen.nl/obsd-server-setup/">https://jkossen.nl/obsd-server-setup/</a><img class="m-l-sm" src="/images/icons/up-right-from-square.svg" height="16" width="16" loading="lazy" alt="External link" title="This link points to an external source">

  </li>
<li><a href="https://blog.thechases.com/posts/bsd/aggressive-pf-config-for-ssh-protection/">https://blog.thechases.com/posts/bsd/aggressive-pf-config-for-ssh-protection/</a><img class="m-l-sm" src="/images/icons/up-right-from-square.svg" height="16" width="16" loading="lazy" alt="External link" title="This link points to an external source">

  </li>
<li><a href="https://dataswamp.org/">https://dataswamp.org/</a><img class="m-l-sm" src="/images/icons/up-right-from-square.svg" height="16" width="16" loading="lazy" alt="External link" title="This link points to an external source">

  </li>
<li><a href="https://www.openbsdhandbook.com/">https://www.openbsdhandbook.com/</a><img class="m-l-sm" src="/images/icons/up-right-from-square.svg" height="16" width="16" loading="lazy" alt="External link" title="This link points to an external source">

  </li>
<li><a href="https://www.protectstar.com/download/blog/pf.conf_noDMZ.txt">https://www.protectstar.com/download/blog/pf.conf_noDMZ.txt</a><img class="m-l-sm" src="/images/icons/up-right-from-square.svg" height="16" width="16" loading="lazy" alt="External link" title="This link points to an external source">

  </li>
<li><a href="https://brycev.com/blog/openbsd-smtpd/">https://brycev.com/blog/openbsd-smtpd/</a><img class="m-l-sm" src="/images/icons/up-right-from-square.svg" height="16" width="16" loading="lazy" alt="External link" title="This link points to an external source">

  </li>
<li><a href="https://corrupted.io/2014/06/09/OpenSMTPD-virtual-users-with-sqlite.html">https://corrupted.io/2014/06/09/OpenSMTPD-virtual-users-with-sqlite.html</a><img class="m-l-sm" src="/images/icons/up-right-from-square.svg" height="16" width="16" loading="lazy" alt="External link" title="This link points to an external source">

  </li>
<li><a href="https://poolp.org/posts/2019-09-14/setting-up-a-mail-server-with-opensmtpd-dovecot-and-rspamd/">https://poolp.org/posts/2019-09-14/setting-up-a-mail-server-with-opensmtpd-dovecot-and-rspamd/</a><img class="m-l-sm" src="/images/icons/up-right-from-square.svg" height="16" width="16" loading="lazy" alt="External link" title="This link points to an external source">

  </li>
</ul>
<h2 id="definitions">Definitions</h2>
<table>
	<thead>
			<tr>
					<th>Definition</th>
					<th>Description</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>DNS</td>
					<td>Contains information related to the domain name with focus on translating between hostnames and IP addresses</td>
			</tr>
			<tr>
					<td>Domain name</td>
					<td>Unique name on the internet to make services available, such as a website</td>
			</tr>
			<tr>
					<td>Hostname</td>
					<td>The name of a computer or defined name within DNS entry, such as when creating A records</td>
			</tr>
			<tr>
					<td>HTTPS</td>
					<td>Encrypted communication channel between a web server and the web browser</td>
			</tr>
			<tr>
					<td>SQL</td>
					<td>Query language to manage a database, request, alter or remove data</td>
			</tr>
	</tbody>
</table>
<p>Some other definition missing that you had to look up? Feedback on this article is appreciated!</p>
<h2 id="special-thanks">Special thanks</h2>
<p>The following persons took the time to carefully read this document and provide feedback</p>
<ul>
<li>Misha Peters</li>
<li>Jeroen Janssen</li>
</ul>
<h2 id="work-in-progress">Work in progress</h2>
<ul>
<li>Monitoring of domain name and DNS records</li>
<li>Check and monitor DMARC/MTA-STS</li>
<li>Flush pf tables until pf supports automatic expiry</li>
<li>Only allow authenticated users to send with their own &lsquo;From:&rsquo;</li>
</ul>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">           senders &lt;users&gt; [masquerade]
</span></span><span class="line"><span class="cl">                 Look up the authenticated user in the users mapping table
</span></span><span class="line"><span class="cl">                 to find the email addresses that user is allowed to
</span></span><span class="line"><span class="cl">                 submit mail as.  In addition, if the masquerade option is
</span></span><span class="line"><span class="cl">                 provided, the From header is rewritten to match the
</span></span><span class="line"><span class="cl">                 sender provided in the SMTP session.
</span></span></code></pre></div>]]></content></entry><entry><title>Contact details</title><link href="https://bsd-audit.com/contact/"/><id>https://bsd-audit.com/contact/</id><author><name>Michael Boelen</name></author><published>2026-07-15T20:35:30+00:00</published><updated>2026-08-05T08:57:53+00:00</updated><content type="html"><![CDATA[<p>Want to share feedback or just want to know more about a particular article or topic? There are a few ways to get in contact.</p>
<ul>
<li>Mastodon: <a href="https://mastodon.social/@mboelen">mboelen</a><img class="m-l-sm" src="/images/icons/up-right-from-square.svg" height="16" width="16" loading="lazy" alt="External link" title="This link points to an external source">

  </li>
<li>E-mail: <a href="mailto:bsdauditfeedback@lessbigtech.eu">bsdauditfeedback@lessbigtech.eu</a></li>
</ul>
<h2 id="guest-blogs">Guest blogs</h2>
<p>In all the years, no single proposal was received with the intention to create a dedicated article without any commercial incentive. For that reason, guest blog posts are not possible.</p>
]]></content></entry></feed>