Host a server with OpenBSD

Disclaimer §

ℹ️ The english translation of this doc is quite new. Please be kind as I read again and fix missed mistakes. Feel free to suggest improvements.

This document is published under CC-BY license

It was written by an OpenBSD enthusiast (and contributors) who figured "If I can do it, anyone can".

Please tell me if you share this document somewhere else, just to know. Thanks. 😉 If you find any mistakes, wish to contribute or need help, feel free to contact me.

There are no ads or tracking scripts on this site.

Only your contributions help me to pay hosting and time given to this project.

If you want and can, you can donate to help me. Thank you! ♥

Do not hesitate to print this documentation on paper or on PDF 🌳. CSS is ready for a nice print rendering 😉.


A good start §

You are about to dive into the world of self-hosting. This document is written to help you host at home or on a dedicated (rented) server some services unfortunately too often entrusted to third parties. The main goal is to keep things as simple as possible while learning gradually.

Of course, compromises were made. If you feel you want to learn more after reading this, that's great! 😊

Why OpenBSD? §

In order to keep things simple, but secure, we'll describe the use of OpenBSD OS in its last stable version.

It is known to be safe.

It is also, in my opinion, easy to configure because the same syntax is shared by different included tools.

See Why OpenBSD rocks.

People say the OpenBSD documentation is great. How can this documentation be useful to me?

It is for sure! The manpages are amazing, use them as a reference. I see this documentation as an entry point, not a replacement, explaining the fundamentals along with a few tips. Anyway, manpages are great and you should read them too.

You'll see, hosting your server isn't that difficult and is mostly text-editing. Everyone should be able to do it.

Let's go!

Self-hosting: What is it? §

Most website you're used to reading -- emails, social networks... -- are hosted on computers somewhere in the world. They are only used to serve content to other computers, so we call them "servers". The biggest difference from most people's point of view is that "they don't have a screen".

When you want to read your mail, a client (a webmail, Thunderbird...) asks the server to retrieve your messages. A copy of them is then downloaded on your computer. In "real life", that would look like this:

Hey, mailman, do you have anything for me?

Yes, a postcard from your mom. I'll give it to you as soon as the copier has finished printing it.

Of course, you can ask the post office to delete the message. But how can you be sure ALL copies have been deleted?

Better become your own post office, don't you think? 😀

At first, everyone was supposed to make a part of the web. Now, most of us depend on private companies that don't respect our privacy.

Pros of self-hosting §

Arguing that you don't care about the right to privacy because you have nothing to hide is no different than saying you don't care about free speech because you have nothing to say. -- E. Snowden

Read about the argument "Nothing to hide"

Cons of self-hosting §

About this document §

In this document, we assume that:

What about the official FAQ? §

This document is not a duplicate of the official OpenBSD FAQ. You should ALWAYS refer to official documentation and manpages when available.

For those who need to read the FAQ offline, I keep an up-to-date archive downloadable from my server.

Official OpenBSD's FAQ

openbsd-faq.tgz: download and extract to read it offline.

What hardware should I use ? §

You don't need phenomenal power to self-host. Start with a machine you have retired because it is too weak for office use.

If you want to buy new hardware, check if it's suported by OpenBSD first.

ARM architectures don't require much power.

If you don't know where to start, APU are quite amazing: not too expensive, small, silent, they require less than 10W and are well supported. Actually, this documentation is hosted on an apu2d0.

APU2 board constructor.

Take a look at bsd-hardware too and do not hesitate to contribute.

What about OpenBSD on a Raspberry Pi ? §

RPi are supported from 3rd version.

Read the instructions carefully as you will need some files too boot correctly.

OpenBSD install §

Make sure you read the official documentation on installing OpenBSD.

There really isn't much to say here because the installer explains a lot.

When in doubt, just use the defaults 😉. This is especially true for disk slicing.

Read the commands for the disklabel editor if necessary.

Oh, you may want to set up Full Disk Encryption during installation by the way.

You also should install all the sets or make sure you understand what you don't want to install.

Can someone else host OpenBSD for me? §

If you don't have reliable internet access, or if you want to experiment before setting it up at home, you can rent a server or a virtual host. I've had good experience with:

They offer virtual machines. The team help develop OpenBSD. Mischa is very nice.


Works very well. It's fast and reliable. A bit more expensive though. (The above link is a refferer link).

Similar setups can be found at ARP Networks:

ARP Networks

Survival guide: which commands do I have to know? §

When you turn on your server, whether it is connected to a monitor or through SSH, you will see a command prompt:

acdc $ █

Enter commands to manage your server.

There are a lot of them, however we usually use a few of them daily. You'll find more over time.

For now, let's see a few of them. Don't try to memorize everything all at once -- come back and get what you need when the time comes.

Tip #1: Tab §

By using the "tabulation" key ↹, you can complete a command or a path to a file. Start writing the beginning, then press ↹.

Tab is a-ma-zing! 😉

Tip #2: ctrl-c §

To erase what you are writing, press "ctrl" and "c" simultaneously. This will also stop most process.

Tip #3: "\" §

Although this is very rare, some filenames sometimes contain spaces " " or even strange symbols. However, a space is interpreted as a separator between files, so the shell will assume that you are referring to several files instead of just one.

In this case use "\" to "escape" the weird symbol. That way the shell will ignore it. For example:

/path/to/some/file\ with\ spaces.txt

In any case, avoid creating files with strange names.

If you need to treat a large number of them, check out the "detox" tool (port of the same name).

Tip #4: order history §

To find the history and quickly relaunch an old command, use the shortcut ctrl-R.

You must first enable it by adding this line to the file "~/.profile":

$ echo "export HISTFILE=~/.history" >> ~/.profile

The next time you log in, the history will be active.

Tip #5: send a process to the background §

You can send a process to the background with "ctrl-z".

You can now run new processes.

To resume the previous process when ready, run 'fg'.

If you have multiple processes in the background, run 'jobs -l'. Example:

> jobs -l
[2] + 65085 Suspended            systat vm
[1] - 99389 Suspended            top -s .5

'fg' will restore the previous process sent to background, in this case systat (+). You may also specify the desired job by its id:

$ fg 99389 # resume top

See the ksh(1) manpage for more :)

su and doas: How to get superuser privileges (root) §

Enter the command 'su -l', followed by the password for the root user.

WARNING: For this to work, your user must belong to the wheel group. This is the case for the first user created on a system.

You can also configure doas to run a command with superuser privileges as follows:

doas command

Edit/create the /etc/doas.conf file and add:

user-name permit

Replace "user-name" with your, well, username.

See also 'man doas'

ls: list the contents of a directory §

Enter 'ls' on its own to list the current folder, or follow it with a path to some other folder.

The "-l" option also allows you to display permissions, owners, sizes and modification dates.


$ ls -l /etc
drwxr-xr-x  7 root  wheel        512 Apr 19 19:12 X11
drwx------  2 root  wheel        512 Apr 19 18:16 acme
-rw-r--r--  1 root  wheel       1542 Apr 13 15:39 acme-client.conf
-rw-r--r--  1 root  wheel       1764 Nov 28 13:56 adduser.conf
drwxr-xr-x  2 root  wheel        512 Apr 19 18:16 amd
drwxr-xr-x  2 root  wheel        512 Apr 19 18:16 authpf
-rw-r--r--  1 root  wheel         30 Aug  2  2020 boot.conf

We get one line per file/folder. Each line has these fields:

<permissions> <inode> <owner> <group> <size> <date of last access> <file name>

A simple and yet efficient method to secure your website -- and the server it lives on -- is to adjust the permissions and the owner of its files.

Read the sections on 'chmod' and 'chown' to learn more.

chmod: change permissions §

Let's take a closer look at what the return of the 'ls -l' command from earlier tells us.

The letters at the beginning of the line describe the permissions granted to the file. We can remember two things:

1. If the first character is "d", then it is a directory. Otherwise, it is a file (with exceptions).

2. The remaining characters are read by set of 3. Each "triplet" describes the permissions for the owner, for the group, and for everyone else, respectively.

For example, for this line:

drwxr-xr-x 2 www daemon 512 May 5 17:10 bin

We see that it is a directory. Then we see three triplets of letters:

rwx: The owner www can:

"r-x": Those belonging to the "daemon" group can

"r-x": All others can:

As a general rule, you should avoid giving write and execute rights to people other than the owner whereever you can. Sometimes, reading permission is also withdrawn on certain files (passwords, etc.).

To change permissions, there are several methods.

"symbolic" chmod §

Some use a set of numbers, like 'chmod 700'. I find this way not very explicit when you are not used to it yet. You may prefer to use chmod <identity>±<permission> where:

Would you like a few examples?

These changes can be applied recursively (to everything below that folder) with the -R flag.

Tip: to allow moving in folders, without making the files executable, use X (uppercase) instead of x.

"absolute" chmod §

If you want to understand the numerical notation of a chmod:

There is no distinction between folders or files, so proceed with caution.

In the triplet passed to "chmod", the first digit represents the owner, the second the group, and the last the others.

For each digit of this triplet, we add the values that "r", "w" and "x" stand for. This means that "chmod 700" grants "rwx" permissions to the owner, and none to the group and others (7 = 4 + 2 + 1).

Finally, in order to define the permissions by distinguishing between the folder and the files, and not to make a file executable with a "chmod -R" (recursive), the "find" command is your friend:

As always, the "man chmod" command will tell you more.

chown: Owner and group §

Each file has an owner and is part of a group. This will allow us to give certain permissions to the owners, which will not necessarily be the same as those given to the group member.

To modify the owner and the group, we use the chown command.

# chown <owner>:<group> filename

File management §

Before we talk about how to handle files, let's look at some key shorthand.

pwd: display the current folder §

With pwd you ask "where am I" 😄

mkdir: create a directory §

$ mkdir name_of_new_folder

Use the "-p" option to create a whole structure at once:

$ mkdir -p ~/folder/with/some/subfolders

cd: change directory §

To move to the "/var/www" folder:

$ cd /var/www

The "cd" command without argument moves you to your "$HOME".

cp: copy §

To copy a file:

$ cp source_file copy_file

To copy a folder and its contents:

$ cp -R source_folder copy_folder

rm: delete §

$ rm path_to_the_folder
$ rm -R path_to_the_folder

mv: move §

$ mv source destination

This works like cut and paste.

less: reading (and searching through) a file §

To only view a file, use the "less" command.

Then you can search for any string of characters by pressing "/" and entering your search. Press "n" to go to the next occurrence, or "N" to go back.

To exit less, press "q".

This command will come in handy if you want to search through the content of your logs 😉.

man: to read manuals §

Here is the real reason why there aren't many support forums for OpenBSD but only a mailing list: the man pages are very comprehensive, complete with examples, and most of the time are sufficient to answer questions/problems encountered.

The "man" command allows you to display a man page.

Note that there are different sections for categorizing man pages:

Also, it sometimes happens that a man page exists in several different sections: its content is not the same. In order to differentiate them, one refers to a manpage as follows: "page_name(section)".

For example: "apm(8)", or "apm(4)". Or "man(1)" and "man(7)". Yes, "man" has a "man" page.

We use this command as follows, without parenthesis:

$ man (section) page

The section is optional.

To practice, run "man hier". Use arrows to scroll. As with "less", you can search with "/". Notice the "SEE ALSO" part which invites you to read other manpages that may be of interest. Exit with "q".

If you don't know what the name of the man page is, you can search for it with the "apropos" command:

$ apropos search_term

vi: to edit a file §

Knowing how to edit a file is crucial.

There are a lot of text editors (vim, nano ...).

vi The default editor on OpenBSD is vi. (There is ed too...)

It may be confusing to use at first, so some people may want to install another editor instead. However, vi is handy once you get it. If, on the contrary, you are already used to the emacs editor, you will find what you are looking for with the mg editor, also available by default.

Here are some tips for using vi through an example. To edit the /etc/iloverocknroll file, you would enter this:

$ vi /etc/iloverocknroll

The contents of this file will then appear in the terminal.

Most of the time, you will only do this:

Are you still here ? 😁

So let's go a little further (but not too much, we promise 😁). Take note that there are three modes:

To save the changes, press ":" then "w". Confirm with enter. We can now quit by writing ":q". Note that you can go faster by typing directly ":wq".

To cancel a modification press "u". To remove multiple changes, press "u" then "." as many times as necessary. "." allows you to repeat the last action. To redo an action you undid previously, use ctrl-R

In order to search for a string, which is very useful in large files, press the "/" key and enter a search term.

If you want to exit without saving your changes then type: "q!".

Other very handy tips:

rcctl: Managing daemons §

In order to activate/deactivate daemons, the "rcctl" command is provided for this purpose. All available services are in the "/etc/rc.d" folder. Here is a short overview:

If you prefer the manual method, then you can directly edit the "/etc/rc.conf.local" file which manages the services launched at startup.

Let's practice with commandline §

Let's practice with a little exercise. Follow the instructions below, then check if you got same thing as in the "Answer". Try to do it from memory first. If you get stuck, read the page again and look for what you are missing.

The answer :

$ cd /tmp
$ mkdir ah
$ cd ah
$ vi dw.txt
$ cp dw.txt DrWho.txt
$ chmod 600 DrWho.txt
$ rm dw.txt
$ cd ~
$ pwd
$ ls -l /tmp/ah
total 2
-rw------- 1 prx wheel 22 May 5 21:10 DrWho.txt
$ cat /tmp/DrWho.txt

Note that we can replace "cd ~" by "cd $HOME", or even by "cd".

What sould you do after first boot ? §

First, read "afterboot(8)" and "intro(8)" manpages. Believe it or not, OpenBSD's developpers wrote a few words for you after install ✌

$ man afterboot
$ man intro

You'll learn how to read manpages actually.

man afterboot(8) online

man intro(8) online

Look configuration examples §

See examples in "/etc/examples".

If you add ports, read included instructions un "/usr/local/share/doc/pkg-readmes".

Set admin mail §

Edit "/etc/mail/aliases" and set root mail to get important reports:

root: batman@athome.tld

Shut down daemons §

You may want to disable non used daemons, such as sndiod in charge of audio (unless you need it of course).

By the way, let's talk about "rcctl" to deal with daemons.

List enabled daemons :

# rcctl ls on

To stop daemon, i.e audio server :

# rcctl stop sndiod

And to disable daemon for next boot :

# rcctl disable sndiod

You probably want to enable power management. As an example, below lines will enable apmd, add "-A" flag to daemon startup and actually start apmd :

# rcctl enable apmd
# rcctl set apmd flags -A
# rcctl start apmd

Read intro(8) to learn about default daemons availables on OpenBSD :

man 8 intro

Network addresses §

What is a network address ? §

IP means Internet Protocol. But first, let's talk about Santa 🎅.

To send a letter asking for all the presents you want, you write to the famous address "Santa Claus, North Pole, Rainbow road.". But how does Santa knows how where to answer? Of course you wrote your own address on the back of the envelope.

That's how works computers and servers : you know where is the video you want to watch, and the server hosting the video knows where to send data.

Every device has its own IP on the internet. There are numbers like, not as good looking as Santa's pretty address isn't it?

How devices knows what is the IP of domain name like ? It there a directory?

Indeed there is something like that : DNS. We'll talk about it later.

There are differences between local (or private) IP ans public IP. Most of the time, to reach the internet, your ISP lend you a router so your network look like this :

|                       |
| Local Network (home)  |
|     PRIVATE IP        |
|                       |
|                       |
|      laptop       <---+------+
|      |      |
|                       |      |
|                       |      |
|     Phone         <---+--+  ++---------------+
|      |  |  |                |
|                       |  +--+  Router  (box) |    +----------+
|                       |     |                |<---+ INTERNET |
|     Computer      <---+-----+  PUBLIC IP:    |    +----------+
|      |     |    |
|                       |  +--+                |
|   * * * * * * * * *   |  |  ++---------------+
|   * Your server   <---+--+   |
|   *  *   |      |
|   * * * * * * * * *   |      |
|                       |      |
|     Game console   <--+------+
|      |
|                       |

To host a server, it is useful to have a satic IP delivered by your ISP. Some don't, so you may want to look for a VPN or dynamic DNS or change ISP :)

Reminder about network §

To run your own server, you must know what are its IP, public, local, and its gateway (router). At first, one may want to configure its network using dhcp then configure a static network when more confident.

Let's see a few words to do so.

Public or local/private address §

First, understand your server is probably behind a router given by your ISP. The router received a public IP. People "on the internet" can reach this IP.

The router allocate private addresses to devices connected in your local network. Each device has its own private IP : one for your phone, another for a printer, a computer, a console... They all share one public address.

Private addresses can be in three ranges :

Your private or local IP is the one your computer use, the one it knows.

To find your local IP, run "ifconfig" and filter for "inet". You'll see something like that :

# ifconfig |grep inet
inet6 ::1 prefixlen 128
inet6 fe80::1%lo0 prefixlen 64 scopeid 0x3
inet netmask 0xff000000
inet netmask 0xffffff00 broadcast
inet6 fe80::feaa:14ff:fe65:5f86%re0 prefixlen 64 scopeid 0x1

inet means IPV4, inet6 means IPv6.

Your public address is the one "used" when you browse the web. Websites you're used to read see this IP. If you don't know it, you can check one of these links (amongst many others):




Router §

The router is the box your ISP gave you. We also call it a gateway because on a side it is the "public" network, on the other your "private network". The router deals with redirections : if someone wants to reach your website, the router will lead him to your server's private address.

To find your gateway IP, use "route" command:

# route -n show
Routing tables

Destination        Gateway            Flags   Refs      Use   Mtu  Prio Iface
default        UGS       35     4827     -     8 re0
224/4              URS        0       13 32768     8 lo0
127/8              UGRS       0        0 32768     8 lo0          UHhl       9      969 32768     1 lo0

The first line is what you're looking for : in this example.

Hostname §

Every device in your network has a name to identify it. It is the hostname.

On a computer, you can change this name. To do so on OpenBSD, edit the /etc/myname file and write the complete hostname:



To configure a network interface, you can use DHCP to autmatically get a local address. Be careful though, the "D" means "Dynamic", so this addres might change in the future depending on your router.

To do so, edit /etc/hostname.if file, and replace ".if" by the name of your inteface (run "ifconfig" to find it) and just write "autoconf" inside.


Static configuration §

A bit more complicated, static configuration gives you more control.

In /etc/hostname.if (replace "if" with your interface name) :

inet   alias
# and as many as you want
# inet 	(alias) local_ip       network_mask      broadcast_add

If you're not sure, at first configure using dhcp and look at the output of "ifconfig".

You must specify the route to your gateway. To do so, you can edit "/etc/mygate" file or add a "!route" command to "/etc/hostname.if". I prefer the latter as it avoid to split configuration in multiple places.

The gateway is you router's IP : here.

inet   alias
!route add -inet default

See also :

man hostname.if

IPv6 §

IPv6 is the new protocol to use facing the lack of ipv4 available. The main advantage is that you don't need a router doing NAT : your machine is directly reachable.

To configure your interface, magic happens in /etc/hostname.if with the "inet6" lines:


inet6   2001:db8:1:1::2    64 # adresse
inet6   autoconf              # SLAAC, if you want
!route add -inet6 default 2001:db8::1

You can mix inet and inet6 instructions.

Configure your router and redirect ports §

Most of your devices access the Internet through a router, probably given by your ISP. The router has a public IP address, making it reachable from the outside. We must learn how to tell the router to forward requests from the outide to your server (not anything else) depending which port number is used.

You can imagine your router as a big wall when seen from the outside. In this wall, there are doors : we call them "ports". When someone knocks at the door 80, he want to see a web serveur. Your router has a configuration line for this cas and will redirect the visitor to your server internal IP.

Most of the doors will remain closed, because you didn't configure a redirection for them, and that's fine to keep your other equipments safe.

                       |               |
                       | Router  (box) |
                       |  ***********  |
                       |               |
              +--------+-- door 443    |
+---------+   |        |               |
|         |<--+        |               |
| Server  |         ?<-+---door 143  <-+--- [Evil bot 👿]
|         |<--+        |               |
+---------+   |        |               |
         ^    +--------+-- door 80  <--+--- [Guest 👼]
         |             |               |
         |             |               |
         +-------------+-- door 22     |
                       |               |

Your router is a GATEWAY with a janitor.

Understand that if your server is reachable with IPV6, no redirection is required : its IPV6 is routable and reachable. That's why it is important to always configure the firewall on your server too.

To configure your router, it might have a web interface reachable using a browser from a computer connected on your local network. Some ISP provide a configuration tool on their web panel. It depends, you'll have to find out or ask your ISP how their router work.

Most of the time, you can try these URI in a browser :

Probably, a username/password will be required. Ask your ISP, or try "admin/admin" or "admin/password", or look what's written on the router.

Here are more clues :


If you do not want to bother with redirectionc, configure a DMZ to your server on your router : all traffic will be redirected to your server. If so, remember to configure your firewall accordingly 😉.

Get a domain name §

Instead of writing your public IP, someone might want to reach your server using something more human friendly : a domain name.

What is a domain name ?

One can register a domain, i.e. "something-cool.tld" as a sign pointing to your IP address. When an human enter "something-cool.tld", the computer will ask the registrar what IP is behind this sign to reach it. In this case, the domain is resolved.

It is easier to remember, it gives identity to your project, and it let you organise it using subdomains (webmail.athome.tld, cloud.athome.tld...).

Below is a little comic to explain a DNS query :


You can find free registrar, or rent a domain name. There are many registrars out there. As examples:




OVH (free)

DNS records §

When a device try to reach "athome.tld", it ask a DNS resolver what IP is behind. DNS is like road signs.

To create this sign, you must link the domain name to your IP in a zone. It can be done in registrar panel if you don't host your own domain name server (see nsd later 😉). As example :

athome.tld    A

Different records exists. You must at least know:

blog.athome.tld CNAME athome.tld
wiki.athome.tld CNAME athome.tld
webmail.athome.tld CNAME athome.tld

Learn more with these links :

Manage your server §

Once your server running, you must make sure everything works fine. Do not read any further if you don't plan to maintain it. Luckily, OpenBSD developpers made things easier.

Daily reports §

Every day (infact every night) a report from "Charlie Root" is generated and mailed to user "root". Inside, you can find :

I look like this:

Running security(8):

Checking mailbox ownership.
user mailbox is owned by root
user mailbox is -rw-r--r--, group wheel

/etc/rc.local diffs (-OLD  +NEW)
--- /var/backups/etc_rc.local.current   Sat Jun  4 03:46:48 2016
+++ /etc/rc.local       Thu Oct 20 09:46:54 2016
@@ -1 +1 @@
-su pi -c "tmux new -s rtorrent -d rtorrent"
+/usr/bin/su pi -c "/usr/bin/tmux new -s rtorrent -d /usr/local/bin/rtorrent"

/etc/spwd.db SHA-256 checksums
OLD: 075455a721ef[...]b942f590fb8e3edfd88c5dd4
NEW: 37eba7537dd7[...]42468cc4cbe768fcf496b230

You can read warning about file owners and changes in a file. Lines starting with "+" were added, those starting with a "-" removed.

In this example, at last, a checksum on the password database has changed : did you add a new user?

Well, that's nice, but how do I get it?

As you will see, it's very easy :

Of course, replace with a mail address you read daily.

Then, run "# newaliases".

That's it! 😄

Just to make sure, send yourself a test message :

echo "Hello sweetie!" | mail -s "test" root

You should get this mail at the address written in /etc/mail/aliases.

Now, it's up to you to read these report every day.

If you need to, you can change the domain name of the sender if you edit /etc/mail/mailname. As example :


File system check §

Add this line to /etc/daily.local to check filesystems every day :


Read man 8 daily to learn more

The Firewall (pf) §

The firewall? Already ?

Absolutely. You want to keep your fresh system safe, don't you?

We'll start with simple examples and suggest a few more for thoses interested.

On OpenBSD, the firewall is pf as in "packet filter". It is configured in "/etc/pf.conf". You will see, it's syntax is easy to understand.

First, you have to ask yourself what do you need. A web server (http)? Mail server? Depending on your answers, you won't open the same port number. We actually will close every port except the ones you really need.

To find the ports on which services listen, look at the contents of the "/etc/services" file. Thus, writing "www" or "80" is the same for pf, but is more readable for humans.

The rules you will define will be applied in the order they are written. This means next rules can override previous ones. To keep a rule state, use "match" instead of "pass" or "block". To apply directly a rule without looking forward, use "quick" keyword. You'll see, it's quite easy actually 😉.

Read the official PF's user guide to go further.

A first example for pf §

Ready? Let's go for a detailed but simple example. We are going to configure the firewall for a website with access on ports 80 (www) and 443 (https).

We start by blocking everything, and recording in the log the prohibited connections with the word "log" (obvious 😀).

block log

To make our life easier, we are going to put the ports to be opened in a variable. This will be particularly practical when there are more ports to manage.

tcp_pass = "{ 80 443 }"

Note that this is identical to 'tcp_pass = "{ www https }"'

Then, we allow potential visitors to access your server. These will be so-called "incoming" connections, so we will use the "in" keyword. Instead of looking for the name of your network interface (which depends on your hardware), we will specify the name of the group that these interfaces belongs to so that it works without worry: "egress".

pass in on egress proto tcp to port $tcp_pass

You're cute eh, but that's all gibberish!

Okay, we'll explain what this syntax means. We can translate the previous configuration line as follows: "let pass inward (pass in) through the egress interface (on egress) for the tcp protocol (proto tcp) to the ports in $tcp_pass (to port $tcp_pass).

Fiew! 😋

Finally, we allow all the output traffic (keyword "out"):

pass out on egress

Finally :

tcp_pass = "{80,443}"
block log

pass in on egress proto tcp to port $tcp_pass
pass out on egress all

Easy, right? 😁

I suggest that you replace the last line with something a little more restrictive:

pass out on egress proto {tcp udp icmp ipv6-icmp}

We simply specify the protocols authorized on exit.

You will understand, pf allows us a first order control on our server. At first, you'll just want to choose which ports should be opened.

This makes a lot of new concepts, take the time to reread what has been written so far if necessary.

When you are more comfortable, know that we can filter incoming and outgoing traffic, and even redirect part of it to internal services (eg anti-spam). I invite you to read the part "going-further-with-pf". You will see how to ignore certain lists of IPs known as harmful that will be recorded in tables, rules against bruteforce attacks or even a proposed tool that adds potential hackers to the blacklist in real time.

I let you build your /etc/pf.conf file according to your needs. Start from the example seen above then add the ports to open. Do not hesitate to consult the example provided at the end of this document.

To be sure, can we have an example to open the SSH port?

If the configured SSH port is 22, then you will have in the pf configuration:

pass in on egress proto tcp to port 22

Of course, you can also just change the tcp_pass variable:

tcp_pass = "{80 443 22}"
pass in ...

Convenient commands for pf §

Find below suggestions of useful commands to manage pf:

pf-badhost: IP blacklist §

Some IPs are known to be harmful. Between those that scan for an open port or those that attempt bruteforce attacks, the list goes on. Fortunately, we can configure the firewall so that it completely ignores these IPs and thus protect and lighten the load on our server.

This is what allows the pf-badhost project.

Author's blacklists §

I keep up to date lists of IP who recentrly tried bad stuff on my server.

feel free to use them! 😉

Bruteforce: what is it? §

One method hackers use to attack a server is called "bruteforce". Such attacks consist of trying all possible username / password combinations until they find the correct one.

If you choosed strong passphrases, you should be safe. However, these attacks abuse resources on your server and can potentially succeed one day (admit that it would be bad luck).

In order to thwart these attacks, you should take the time to analyze the logs of the services you host (in "/var/log" and "/var/www/logs") for identification failures and notice where these attacks are coming from to banish the source. But let's be honest, it's both painful and time consuming.

Ideally, a tool that monitors logs for you and takes care of blacklisting "attacker" IPs on your firewall. Good thing, you can use vilain or sshguard which are made for that.

In the meantime, pf already integrates a protection which limits the number of connections on a port during a given time. It is very effective and remains light. Read the next paragraph 😉.

Anti-Bruteforce integrated into pf §

pf has a very interesting feature: "tables". This will allow us to remember certain IP addresses of hackers who would try to compromise the server. For example, to protect the SSH service, we will proceed as follows:

So that gives us this:

ssh_port = "22"
table <ssh_abuse> persist
block in log quick proto tcp from <ssh_abuse> to any port $ssh_port
pass in on egress proto tcp to any port $ssh_port modulate state \
	(source-track rule, \
	max-src-conn-rate 3/60, \
	overload <ssh_abuse> flush global)

Note that we have recorded the port number used for the SSH server. It is useless, because one can put in the place of "$ssh_port" simply "ssh", the name of the service. However, you may want to change the default port of the SSH server, as described in the chapter on this service.

With the keyword "quick", further rules won't be read so abuser is blocked and that's it.

Here is another example for a website (http and https ports):

http_ports = "{www https}" # ports http (s)
# If more than 40 connections every 5 seconds on the http (s) ports
# or if she tries to connect more than 100 times
# we add the ip to block it.
pass in on egress proto tcp to any port $http_ports modulate state \
	(source-track rule, \
	max-src-conn 100, max-src-conn-rate 40/5,\
	overload <http_abuse> flush global)

Anti-bruteforce in real time with vilain §

I would like to offer you a tool that I wrote with the help of Vincent Delft in order to blacklist possible hackers when the attack takes place.

The script in question is called vilain.

Anti-bruteforce in real time with sshguard §

Contrary to what its name suggests, sshguard is able to check connection attempts on multiple services, not just SSH.

Its installation in OpenBSD is not very complicated to set up. You will need to create a table in pf that will contain all the blacklisted IPs. Thus, we add in the /etc/pf.conf file a new table containing the IPs of the attackers who are immediately blocked:

table <sshguard> persist
block in from <sshguard>

Reload the firewall with pfctl -f /etc/pf.conf.

Now, we install sshguard as we usually do:

# pkg_add sshguard

Copy the configuration file given as an example:

# cp /usr/local/share/examples/sshguard/sshguard.conf.sample /etc/sshguard.conf

Edit this file if you want to make it more or less harsh. The default options are quite reasonable.

We can now enable and run sshguard:

# rcctl enable sshguard
# rcctl start sshguard

And there you go, your server is now safer.

You may want to whitelist some IP addresses. Edit "/etc/sshguard.friends" and write, one per line, IP or ranges to ignore:

Trap with pf and iblock §

solene@ wrote a tool named iblock.

iblock ban every IP trying to access your server on ports that are actually unused.

This is definitely a must-have.

SSH: administer remotely §

Configuring SSH §

SSH is a great tool 🙋. You can access to your server from another computer. You can then manage it remotely without having to connect a keyboard and screen. Truth be told, with some exceptions, all servers are administered this way, but you are at home after all, so you can do whatever you want.

SSH is a protocol useful for much more: encrypted tunnels, SFTP file storage space...

Anyway, if you didn't enable SSH when installing OpenBSD, you can enable it with the following command:

# rcctl enable sshd

Although this protocol is reliable, it does not cost anything to take some safety precautions. We will edit the SSH configuration in the /etc/ssh/sshd_config file.

PermitRootLogin no

To connect to the machine, you will need to create a simple user with the "adduser" command.

Once the changes have been made, restart the SSH daemon:

# rcctl reload sshd

Later, to connect to the server, you will use the following command from your computer (in a terminal, or with a client like putty under windows):

$ ssh toto@chezmoi.tld

And if you changed the default port:

$ ssh -p 222 toto@chezmoi.tld

It will ask for the password of the user toto, then you can administer the server remotely.

Login with keys (without password) §

It is quite possible to connect in SSH without using a password. Instead, we use a pair of keys. This is a good idea at least for the following reasons:

However, this requires you to have the private key on the device used to connect, which is not always the case.

Here's the procedure to follow :

On the server, edit the /etc/ssh/sshd_config file so it contains this line:

PubkeyAuthentication yes

Now, on the computer that is used to access the server, we will generate a key pair with the following command:

ssh-keygen -t ed25519 -f ~/.ssh/sshkey -a 100

You will find two new files:

The ed25519 algorithm is suggested because it is very reliable at the moment.

Do not enter a password and then wait for the keys to be generated.

On an OpenBSD or Linux system, you will take care to edit your user's ~/.ssh/config file to fill it out as follows:

Host your_server
HostName verylong.reallylong.too.long
User myusername
Port 222
PasswordAuthentication no
IdentityFile ~/.ssh/sshkey

Of course, you will change the domain name of your server as well as the name of the user who must connect. Then run the following command to copy the public key to the server.

ssh-copy-id -p xxx -i ~/.ssh/ "myusername@verylong.reallylong.too.long"

Here, replace "xxx" with the port used by SSH if it's not 22.

If the ssh-copy-id tool is not available, you must copy the public key manually. To display it, type

$ cat ~/.ssh/

Connect to the server in SSH, then add in the file ~/.ssh/authorized_keys the contents of the file displayed previously.

Once this is done, you can log in without having to enter a password with just the command:

$ ssh your_server

Convenient, isn't it?

If that works as expected, you can disable password identification for good so that only the keyset is left as possibilities. Edit the /etc/ssh/sshd_config file to contain this line:

PasswordAuthentication no

BE CAREFUL not to lose your set of keys!

Upload a file §

You will definitely need to upload files to your server from time to time. Once SSH is configured, you will be able to use the scp command from any computer to copy a file to your server. For example :

$ scp -P <ssh port> user@athome.tld:/location/from/destination file-to-copy

It's very useful.

However, if you want to transfer many files, backup or store documents, consider SFTP instead, which has more features such as resuming a partial transfer.

If the files are too large, you can split them for multiple transfers.

SSHFP DNS records §

The first time you connect to your server using SSH, the client get the fingerprint of the server. Next time, it compares the previous checkprint and display a warning if it has changed because there might be a problem if you didn't touch your server since. It's called Trust On First Use (TOFU -- not the food, sorry).

To avoid TOFU, you can publish data necessary to check the fingerprint the first time. To do so, we use a DNS SSHFP record.

Enter "ssh-keygen -r athome.tld" and copy the result in your DNS zone. Don't forget to adjust the entries with a final dot "." after domain name or replacing it with a "@" if necessary.

As example :

@ IN SSHFP 1 1 97f5a9479fd16fbee1381c425297f7b2773a67a4
@ IN SSHFP 1 2 bd50a3c271e83dc769ea7c249a5c07aea1a817c62910129de56eba2763f93607
@ IN SSHFP 2 1 05fb8516842fc270ea5abda38d1c32b41e75da8a
@ IN SSHFP 2 2 491489437e137d7d27959a4de3ba7660185ef98cbd3abc2287c82f82e8f032ab
@ IN SSHFP 3 1 b7305417b0e981c4513642020a1f57ee03915af5
@ IN SSHFP 3 2 f15d33852df5f4043c77b6bf1c6134a5e11de0a513c1ec127fcbb8fae733b178
@ IN SSHFP 4 1 a1120fe90e69c0f534fece3dc837db6a262c3371
@ IN SSHFP 4 2 a36e418b517d95a74f1e0be1228d46e7aeaa8ed310359e91162d8c40b373eb55

We'll talk about DNS a bit further 😉.

In order the client check these records with the server fingerprint at first connection, add option "VerifyHostKeyDNS" to ssh. In "~/.ssh/config" :

Host *
    VerifyHostKeyDNS yes

Keep the system up to date §

To ensure your system security, it is essential to keep it up to date. In order to apply the security patches, you must update:

Short 😉:

# syspatch
# pkg_add -u

Update ports (packages) §

Updating packages is as easy as :

# pkg_add -u

Yes that's all. 😁

Thanks to solene who made this possible.

Since this paragraph is a bit short, I take this opportunity to give you two tips 😊. I advise you to add to the /etc/daily.local file the command

pkg_add -nu

This way, in the daily mail from Charlie Root, you will see what might happen if upgrades for packages are availables. Nothing is done for real, because if an updated program needs to be restarted, it may not work until it is. Thus, you are just warned of any updates to apply, and with the -n option, the packages are kept in the cache in order to save time without risking to put a service down if it has to be restarted after the update. You will enter "pkg_add -u" when you get the chance.

If you still want to apply the updates automatically, I advise you to first install the "checkrestart" port which will tell you if a service must be restarted after the update. This will therefore look like this in /etc/daily.local:

pkg_add -u
echo "Service to restart with rcctl restart:"

You will still have to reload the services manually with rcctl.

Update the system §

First of all, a little reminder: OpenBSD is available in 3 "flavors":

It can indeed happen that bugs are discovered. Each time, fixes are quickly proposed. It is then recommended to apply the security patches.

Since version 6.1, this operation is very simple with the following command:

# syspatch

The binary patches are then downloaded and installed. That's it! 😁 It looked like this in OpenBSD 6.1 (a long time ago):

Get/Verify syspatch61-002_vmmfpu.tgz 100% | ******************************* | 9377 KB 00:49
Installing patch 002_vmmfpu
Get/Verify syspatch61-003_libress ... 100% | ******************************* | 11391 KB 00:22
Installing patch 003_libressl

This tool is only available for i386, amd64, and more recently arm64 architectures.

One may want to use the old method of source recovery and manual installation described in the official FAQ (for reasons).

Be notified of updates §

To find out if updates need to be applied, you can consult the errata page which contains the list of available security patches. It is located at

"" where "XX" is the version number of your release, i.e "70" for OpenBSD 7.0.

You can also be notified by email (and that's great 😊). To receive important update messages available on the system, subscribe to the announce and security-announce lists. For that, send a first email to simply containing:

subscribe announce

Then send a second message with:

subscribe security-announce

I also advise you to subscribe to the list indicating that there is a new version of ports by subscribing to the ports-security list by always sending to the same address a message containing:

subscribe ports-security

Upgrade from a version to another §

When a new major version of OpenBSD is available, the update procedure is always detailed on the official website. You must read the release notes when upgrading at "" where "XX" must be replaced by the version number you want to upgrade to.

Since version 6.5, it only takes a simple command to update to the latest publication release or to -current:

# sysupgrade

ALWAYS check upgrade notes anyway 😄.

Clean up after multiple updates §

If your installation is a bit outdated, you can check which files you may have forgotten to delete with the help of the "sysclean" port (command of the same name).

The files which are not supposed to be present in a base system will then be listed. Read the output carefully, most of them are configuration files you created and want to keep 😉.

Backup §

One can use its server to store important files.

In any case, you also must think to backup the server itself. You never know when the hard drive will crash, when a storm will generate a power overload or when your cat will rush trought the wires 😼.

Automatic backup of / §

By default, you can backup / daily with OpenBSD

See The official FAQ about /altroot

It is relevant if you backup on a separate disk.

If one day the main hard drive has a failure, you can boot on the /altroot. When you see the prompt "boot>" :

Find the /altroot partition

boot> machine diskinfo

Then boot on the appropriate slice :

boot> boot -s hd1a:/bsd

⚠ WARNING : only / is saved, with /etc. If you need to backup /var or /usr/local or else, you must plan this. Read the next part to do so 😉.

Read also the official FAQ to duplicate filesystems.

Backup with rsync §

You may dedicate a partition to your backup, wether it's an external drive or an extra slice kept at OpenBSD install. Below, we use as example the mount point /mnt/bckp created for our needs.

We will use the "rsync" tool. You can install it throught ports, but take note there is "openrsync" already available in base install. It is a rewrite of rsync less powerful but at least avaiable in base. If the target device doesn't have rsync installed, add the option "--rsync-path=openrsync". The rsync protocol will detect modified or new files to copy and don't bother with the others.

As example, you can add those lines in /etc/weekly.local file to backup /var and /usr/local well, weekly :

/usr/local/bin/rsync -a --delete /var/ /mnt/bckp/var.bak/
/usr/local/bin/rsync -a --delete /usr/local/ /mnt/bckp/usrlocal.bak/

This is a local backup, but you can use rsync thought a SSH tunnel to send data from one computer to your server:

$ rsync -e "ssh" -avz --delete /directory/to/backup \

Backup with tar §

Tar let you create archives of a whole directory structure, and is available in base of course.

Below is a suggestion to create a daily archive named with the current date.


for i in $BACKLIST; do
    backupfile="${BACKDIR}/$(basename ${i})-$(date +%F).tar.gz"
	tar -czf "${backupfile}" "${i}"
chmod 700 "${BACKDIR}"

# remove olds
#find "${BACKDIR}" -type f -mtime +90 -delete

Uncomment the last line to delete achives older than 90 days.

SFTP : Secure file transfer §

One can see SFTP like FTP over SSH. Of course, it's a bit more than that 😉. It can continue partial transfer as example.

If you have an SSH access to your server, you can sftp 😄.

If you want to provide an SFTP server for multiple users, using a chroot might be helpful to keep everyone's data safe from others. We'll talk about it later.

sftp usage §

To copy files with the user account "batman", start a new sftp session :

$ sftp batman@athome.tld

If you configured key authentication (good idea!), use -i flag :

$ sftp -i ~/.ssh/sshkey batman@athome.tld

Now you're in front of a prompt. You can enter commands such as :

And also cd, mkdir, quit and help to get a list of availables commands.

One can prefer to use a graphical tool to deal with sftp. Look at Filezilla on WinSCP for SFTP clients.

Also, most file manager can handle sftp protocol. Try to open a path like :


At least Gnome, KDE and XFCE default file browser can do this.

However, if your file manager don't support SFTP, you still can use "sshfs" to mount a remote SFTP location as any other mountpoints.

$ sshfs -d batman@athome.tld:/remote/dir /home/batman/sftp \
    -o IdentityFile=/home/batman/.ssh/sshkey

chrooted SFTP §

To share a storage space between users, you might want to lock them is a specific directory. Doing so, they can't reach other files above their own dedicated repertory. It is interesting to keep everyone's data out of sight of the others, but also avoir sftp users to reach system files. This is called "chroot".

In other words, if an user is chrooted in "/var/sftp/batman", he can't read "/var/sftp" nor "/var" and even less "/". for this user, "/var/sftp/batman" is the new root "/".

For the example, we'll chroot every sftp users in "/var/sftp/user_name". You'll need to make the appropriates directories. They will all belong to a group named "sftpusers" in order to automatically chroot them when they sftp.

Create this groupe :

# groupadd sftpusers

Then, edit "/etc/ssh/sshd_config" to chroot every user belonging to group sftpusers in "/var/sftp"

Match Group sftpusers
      ChrootDirectory /var/sftp/
      ForceCommand internal-sftp
      AllowTcpForwarding no

Note you can do the same for a specific user : you just use a section "Match User" instead.

If you wish users to authenticate using keys, then add to the above section :

PasswordAuthentication no

Reload ssh with "# rcctl reload sshd".

Now make appropriate directories with required permissions for the chroot (this is critical):

# mkdir -p /var/sftp
# chown root:wheel /var/sftp
# chmod 700 /var/sftp

Read the next part to see how to create sftp users and their own directories.

Add chrooted sftp users §

Any regular account belonging to sftpusers group will match the above rule. You might consider to restrict even more the access to sftp users :

# install -d -o new_user -g new_user -m 700 "/var/sftp/home/new_user"

Now, new_user is chrooted in "/var/sftp/home/new_user" when he log in using sftp.

If you enable key authentication, then you'll have to fill the file "/home/user/.ssh/authorized_keys" with user's ssh public key. ⚠ I'm reffering to the file "/home/user/.ssh/authorized_keys", NOT "/var/sftp/home/user/.ssh/authorized_keys".

Keep your server on time (ntpd) §

It is crucial to set your server's date and time correctly. Even if the hardware is supposed to keep track of time, it sometimes isn't reliable. However, one must avoid time shift or fear database corruption and unexpected behaviour.

OpenNTPD in OpenBSD's base installation will ensure your server is on time.

Nothing to do 😃, it's already there.

Just enable ntpd daemon and you're good to go :

# rcctl enable ntpd

Edit "/etc/ntpd.conf" file. You may copy the example at "/etc/examples/ntpd.conf"

# use a random selection of NTP Pool Time Servers           
# see                                                            

# time server with excellent global adjacency               

# use all detected timedelta sensors                        
sensor *                     

# get the time constraint from a well-known HTTPS site      
constraint from ""               # quad9 v4 without DNS 
constraint from "2620:fe::fe"           # quad9 v6 without DNS 
constraints from ""      # intentionally not 

Actually, default configuration is fine 😉.

You just don't want to be a NTP server telling others what time it is, so let "listen on *" commented.

You choose a few public ntp server you "trust", then use every sensors availables. Finally, add some contraints from well known IP and end with a https website using it's domain name (a domain name resolution is required).

Finally, restart ntpd :

# rcctl restart ntpd

It's all good 😊.

Host a website (httpd) §

Host you own website is a great project ! 😄 It's the best way to have your personal space to do whatever you want without restrictions.

OpenBSD is shipped with "httpd", a http server. It is light and easy to configure. Most of the time, it is more than enough, but if you need even more features, look for "nginx" or "apache" in ports.

In this part, we'll see how to host a simple static website, then how to add PHP support to finally show some web applications examples.

Before going any further, take note that http is chrooted in "/var/www" by default.

What is that supposed to mean ?

To httpd, every file "above" "/var/www" can't be read. In other words, httpd see "/var/www" as the new root "/". For obvious security reasons, if httpd can't even reach system files, it reduce the risk that someone find sensitive data through a weak website.

Simple static website §

A static website is just a few html files delivered by httpd. It's safe and fast. That's a good starting point. Let's see how to host you own website.

First, create a directory to store the website :

# mkdir /var/www/htdocs/my_website

Now, copy your website html pages and files in that directory (index.html...). Just make sure httpd is able to read those files by adjusting permissions with "chmod a+r /var/www/htdocs/my_website" or setting owner :

# chown -R www:daemon /var/www/htdocs/my_website

You might change permissions later.

Now you must configure httpd to add a new section for your website. If it is served on "http://athome.tld", you'll edit "/etc/httpd.conf" file so it looks like this :

types { include "/usr/share/misc/mime.types" }

server "athome.tld" {
	listen on * port 80
	root "/htdocs/my_website"

A few notes about the previous lines :

You can now enable and start httpd :

# rcctl enable httpd
# rcctl start httpd

Check http port (80) is open in your router and firewall, then check your website is reachable.

Every file you add in "/var/www/htdocs/my_website" will be served.

You can use SFTP previously described to upload your website 😉.

Get a SSL certificate §

SSL certificates allow to make TLS encryption and thus, among other things, serve your website through the more secure protocol HTTPS.

It is necessary when sensitive data is sent to/from your server.

Letsencrypt §

"Let's Encrypt" is a free certificate authority providing easy to use API to get and manage certificates.

There is a client included in OpenBSD names "acme-client" to deal with such tasks. It currently requires a http server to generate certificates. That's why we'll discuss acme-client here. However, you certainly can use those certificates for other needs that https such as IMAPS.

⚠ First of all, check you opened port 80 in the firewall and eventually in your router. Acme-client needs to check for some files you'll make available for letsencrypt with httpd.

Actually, acme-client will ensure you manage the domain you request a certificate for. To do so, it ask an unique file to Let's Encrypt, kinda fingerprint, stored in "/var/www/acme". Then, Let's Encrypt try to get this "secret" file on your server at ".well-known/acme-challenge", asking for "http://athome.tld/.well-known/acme-challenge/secret_file". If it succeeds, the request is accepted and you get a certificate. Finally the file is deleted.

Remember your website is probably located in "/var/www/htdocs/website" or something. You need to make "/var/www/acme" for letsencrypt to get the secret file. So, add a "location" instruction in "/etc/httpd.conf" :

server "athome.tld" {
    listen on * port 80

    location "/.well-known/acme-challenge/*" {
        root "/acme"
        request strip 2
    root "/htdocs/website"

A few explanations :

⚠ This section will have to be added for every domain served by httpd, even alternative names specified in the acme-client configuration file below. You might find easier to use "include" instruction in "httpd.conf" later as it is described in the "Tips" part a bit further 😉.

Before calling acme-client, you must configure it by editing "/etc/acme-client.conf". You can use "/etc/examples/acme-client.conf" as template.

For the example, the file might look like this :

authority letsencrypt {
  api url ""
  account key "/etc/acme/letsencrypt-privkey.pem"

authority letsencrypt-staging {
  api url ""
  account key "/etc/acme/letsencrypt-staging-privkey.pem"

domain athome.tld {
    alternative names { webmail.athome.tld www.athome.tld }
    domain key "/etc/ssl/private/athome.tld.key"
    domain full chain certificate "/etc/ssl/athome.tld.crt"
    sign with letsencrypt

Of course, replace "athome.tld", "alternative names { ..." with the other domains and subdomains you need and eventually the location of key and full chain certificate.

Make sure necessary directories are here (they might already exist) :

# mkdir -p -m 700 /etc/ssl/private
# mkdir -p -m 755 /var/www/acme

Check acme-client configuration with "# acme-client -n", it must return nothing.

Now you can get your certificates :

# acme-client -v athome.tld

At first, you might want to make sure everything works as expected using "sign with letsencrypt-staging" in "/etc/acme-client.conf" and switch back to "sign with letsencrypt" later. You can force getting certificates using "acme-client -F athome.tld".

Consider renewing certificates automatically using "/etc/weekly.local" file. It is a script executed every week.

/usr/sbin/acme-client -v athome.tld && /usr/sbin/rcctl reload httpd

Using "&&" make httpd to reload certificates only if there are renewed certificates. If at some point other daemons than httpd uses your certificates, you'll have to reload them too (relayd, dovecot...)

Now you've got a certificate, let's enable https in "/etc/httpd.conf" 😄. A new "tls" section appears. :

server "athome.tld" {
	listen on * port 80
	# http version
	# [...snip...]
# snippet of /etc/httpd.conf
server "athome.tld" {
    listen on * tls port 443
    root "/htdocs/website"

    tls {
        certificate "/etc/ssl/athome.tld.crt"
        key "/etc/ssl/private/athome.tld.key"

    # add your website specific configuration here

As you can see, now httpd listens on port 443 (https) : remember to configure your firewall for this.

You might want to redirect clients accessing from http to https. To do so, add an instruction matching all request ("location *"). The clients will be redirected to the https version except if the request matched the previous one for acme-client.

# snippet of /etc/httpd.conf
server "athome.tld" {
    listen on * port 80

    location "/.well-known/acme-challenge/*" {
        root "/acme"
        request strip 2

    location * { block return 301 "https://$SERVER_NAME$REQUEST_URI" }
server "athome.tld" {
    listen on * tls port 443

Remember to add such section for every website you want to serve. You may want to use "include" instructions to ease your life. See next part 😉.

About "include" usage in httpd.conf §

You will certainly enable tls for multiple website. Instead of rewriting the same instructions again and again, you can include a file.

First, create a directory to keep multiple httpd configuration files :

# mkdir /etc/httpd.d

Then, we create "/etc/httpd.d/acme.conf" file to keep instructions related to acme seen earlier :

location "/.well-known/acme-challenge/*" {
    root "/acme"
    request strip 2

You even can go a bit further by creating a file "/etc/httpd.d/tls.conf" for multiples lines about certificates :

tls {
    certificate "/etc/ssl/athome.tld.crt"
    key "/etc/ssl/private/athome.tld.key"

Now, the file "/etc/httpd.conf" can be simplified to those lines below. The whole file let you serve 3 websites : athome.tld, www.athome.tld and webmail.athome.tld :

server "www.athome.tld" {
    listen on * tls port 443
    include "/etc/httpd.d/tls.conf"
    root "/htdocs/www.athome.tld"

server "www.athome.tld" {
    listen on * port 80
    include "/etc/httpd.d/acme.conf"
    location * { block return 301 "https://$SERVER_NAME$REQUEST_URI" }

server "athome.tld" {
    listen on * tls port 443
    include "/etc/httpd.d/tls.conf"
    block return 301 "https://www.$SERVER_NAME$REQUEST_URI"

server "athome.tld" {
    listen on * port 80
    include "/etc/httpd.d/acme.conf"
    location * { block return 301 "https://$SERVER_NAME$REQUEST_URI" }

server "webmail.athome.tld" {
    listen on * tls port 443
    include "/etc/httpd.d/tls.conf"
    root "/htdocs/webmail.athome.tld"

server "webmail.athome.tld" {
    listen on * port 80
    include "/etc/httpd.d/acme.conf"
    location * { block return 301 "https://$SERVER_NAME$REQUEST_URI" }

The above configuration redirect from athome.tld to www.athome.tld. Another website webmail.chez.tld is handled. For each domain, using acme is possible.

You may have noticed each section is quite a routine. You can imagine other usages to "include" instruction if you need it.

Thank you Grégory Marchal for your suggestions on this part. 😉

Facultative : CAA records §

You can add a CAA record in your DNS zone to show you own the domain and YOU asked a certificate to Let's Encrypt. This is just a proof of honesty helping someone to trust the certificate used.

@ 300 IN   CAA   0 issue ""

Generate a self-signed certificate §

You can generate your own self signed-certificate. At first connection, clients will see a warning asking if they trust or not the certificate.

To do so, see man 8 ssl in section "Generating RSA server certificates".

Check your server SSL configuration §

Find below a few tools to check how well you've configured your server (not only https) :


The very minimal PHP support §

You may want to add PHP support to your website if you use a CMS as example.

Use OpenBSD's ports to install php (adjust the version number)

# pkg_add php-7.4.7

To see every PHP version availables :

# pkg_info -Q php

Then, enable and start php :

# rcctl enable php74_fpm
# rcctl start php74_fpm

Here, "74" suggests you installed PHP version 7.4.

Now edit httpd configuration so it sends php files to the interpreter. In "/etc/httpd.conf":

server "athome.tld" {
        listen on * port 80
        root "/htdocs/website"
        directory index index.php

        location "*.php*" {
                fastcgi socket "/run/php-fpm.sock"

Notice the "directory index index.php" instruction. By default, when someone reach "http://athome.tld/", it's equivalent to "http://athome.tld/index.php".

You're good to use PHP for most use cases.

Add PHP modules §

If you read "/usr/local/share/doc/pkg-readmes" php file, you already know what to do 😉. You have to add symlinks from "/etc/php-7.4.sample" to "/etc/php-7.4" (edit php version).

# cd /etc/php-7.4.sample
# for i in *; do ln -sf ../php-7.4.sample/$i ../php-7.4/; done
# rcctl restart php74_fpm

With main php package, most extension are already installed. You may add the following as they are quite common and useful :

Edit PHP configuration §

Edit "/etc/php-7.4.ini". Below is an example of some useful changes :

; Increase the size of uploadable files
post_max_size = 10M
upload_max_filesize = 10M
; let php download remote content
allow_url_fopen = On
; Timezone
date.timezone = Europe/Paris
; Enable cache to avoid every page regeneration

Configuration to deal with httpd's chroot §

Your php pages may need to download remote content. Therefore, it must be able to do domain name resolution, check tls certificates and more. The necessary is stored in "/etc". However, if you remember, httpd is chrooted. Do you remember where ?

In /var/www !!!

Indeed Jean-Michel! Good to have you here. 😁

We'll have to copy a few files usually stored in "/etc" to "/var/www/etc":

# cd /var/www     # go in the chroot 
# mkdir etc/      # create etc directory
# cp /etc/resolv.conf etc/resolv.conf # for Domain resolution       
# cp /etc/hosts etc/hosts # DN too
# cp /etc/localtime etc/localtime
# mkdir etc/ssl   # Create another directory for tls certs
# install -m 444 -o root -g bin /etc/ssl/cert.pem /etc/ssl/openssl.cnf /var/www/etc/ssl

Those files must be read only :

chmod -R 444 /var/www/etc/*
chmod -R a+X /var/www/etc/

Les fichiers copiés servent notamment à :

Files in "/etc/ssl/*" must be updated periodically. Add in "/etc/monthly.local" :

install -m 444 -o root -g bin /etc/ssl/cert.pem /etc/ssl/openssl.cnf /var/www/etc/ssl

If you need PHP to send mails, you must copy "sh" in chroot (see "/usr/local/share/doc/pkg-readmes/femail-chroot*").

# cp /bin/sh /var/www/bin/

At last, reload php 😉.

A few tips for httpd §

man httpd §

You should read man "httpd.conf".


You'll find what's written here and more.

httpd logs §

Choose where to store logs.

By default, they are in "/var/www/logs"

log access "website-name.log"

Disable logs with "no log".

gzip compression §

Add "gzip-static" instruction in a domain configuration or a "location" section. Thus, httpd try to deliver the requested file with ".gz" suffix if it is present.

This is good for bandwidth, it can reduce 2x to 10x the weight to transfer. Compressoin ratio is good on text files (html, css, js, svg, ...)

To gzip a file before uploading on your server :

$ gzip -vk9 index.html
index.html:                49.5% -- replaced with index.html.gz
1395 bytes in, 733 bytes out

Custom error pages §

Add "errdocs" intruction to tell which directory contains custom error pages. A generic "err.html" or multiple pages with error code as name can be used.

As example :

errdocs "/htdocs/athome.tld/err"

In /var/www/htdocs/athome.tld/err", there is the following "err.html" :

<!DOCTYPE html>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="/favicon.png" type="image/png" />
<style type="text/css">body, html {height:100%; margin:0}
#bg {
	position: relative;
	background-position: center;
	background-repeat: no-repeat;
	background-size: cover;
	background-image: url("/img/errimg.jpg");
	height: 100%;
#content {
<div id="bg">
	<div id="content">
		<h1>Error page 😖</h1>

Improve disponibility §

To increase the number of server processes and thus serve simultaneously content to multiple clients, increase the default value of 3 :

prefork 10

Publish with utf-8 §

If you try to serve plain text, the web browser may have trouble to display some glyphs except if you specifically set it to utf-8.

To avoid clients to look for this setup, you can explicitly send the appropriate header. However, the semicolon has to be escaped making the configuration a bit weird. Here is an example for ".txt" and ".gmi" file extensions :

types {
	include "/usr/share/misc/mime.types"
        text/"plain;charset=UTF-8" gmi
        text/"plain;charset=UTF-8" txt
        text/"plain;charset=UTF-8" awk
        text/"plain;charset=UTF-8" sh
        text/"plain;charset=UTF-8" c

Notice how we add lines about UTF-8 after including /usr/share/misc/mime.types to overwrite previous declarations.

Password restricted area §

Create login credentials with "htpasswd":

# htpasswd /var/www/secret.htpw login

Replace "login" with any username and set a strong password.

Do it again to add more users.

Set appropriate permissions on this file :

# chown www /var/www/secret.htpw
# chmod 400 /var/www/secret.htpw

Finally, tell httpd to read this file for credentials and ask client to enter login + password when "/hidden_directory" is requested :

location "/hidden_directory/*" {
    authenticate "Restricted access" with "/secret.htpw"

"secret.htpw" location is relative to httpd's chroot.

For a whole website :

location "/*"

Or add "authenticate" instruction at the very beginning without any "location".

httpd automatic index §

To display a list of files availables in a directory if no "index.html" is found :

location "/dir/*" {
    directory auto index

Include configuration files §

If you have many websites, you can include files in "/etc/httpd.conf" instead of writing again and again the same thing :

include "/etc/httpd/site1.conf"
include "/etc/httpd/site2.conf"

httpd TLS §

Add a "ticket session lifetime" to speed up TLS.

hsts preload
tls {
  certificate "/etc/ssl/athome.tld.crt"
  key "/etc/ssl/private/athome.tld.key"
  ticket lifetime default

How to set permission for website files ? §

Setting appropriate permissions on files is mandatory to keep your data safe. You want to keep an eye on who can read some files or not, and even more carefully on files to execute or not.

For a website, there are no perfect solution, so make sure you understand the following advices and make your own choices.

You should set owner and group to "www" and "daemon" so httpd can read the files :

# chown -R www:daemon /var/www/htdocs/website

For a static website (no PHP), remove execution (-x) and writing (-w) permissions. You should let one change current path in these directories (+X).

# chmod -R a-xw /var/www/htdocs/website 
# chmod -R ug+X /var/www/htdocs/website

First, you remove x and w permissions to everyone, and then add to owner and group permission to change into this directory.

For a dynamic website, you need to write in the directory and execute files in them :

# chmod -R a-xw /var/www/htdocs/website # remove all x and w permissions
# chmod -R u+xwX /var/www/htdocs/website # owner can xwX
# chmod -R g+rX /var/www/htdocs/website  # group can read

WARNING : you really should edit permissions more carefully to suit your website case.

For information, most commercial web hosters use the following permissions (not saying that's the best) :

To apply the previous permissions :

# chmod -R a-rwx /var/www/htdocs/site    # remove all perms
# chmod -R a+rX /var/www/htdocs          # everyone can read and cd in directories
# chmod -R u+w /var/www/htdocs           # only owner can write

Notice the "X" (not "x").

Relayd and headers §

Because httpd can't manage headers itself, you can put relayd "before" httpd. It helps add headers or modify others.

As you can guess, relayd is included in base installation.

Visit this link to test your website headers.

Relayd configuration §

Edit /etc/relayd.conf to configure relayd. Inside, add the following lines as example :

http protocol "http" {
  match request header remove "Proxy"
  match response header set "X-Xss-Protection" value "1; mode=block"

  return error

relay "www" {
  listen on port 80
  protocol "http"
  forward to port 80

Those lines means :

That's why you must edit httpd configuration accordingly :

# httpd's configuration
listen on localhost port 80

To sum up, things goes like this now :

1. A client ask to see your website and knocks at port 80.

2. relayd modify a few headers and redirect to httpd which still serve the website.

Dont forget to reload relayd and httpd:

# rcctl enable relayd
# rcctl restart httpd
# rcctl start relayd

Notice logs will show incoming connections from (relayd local address). You may want to use "forwarded" log format for httpd :

log style forwarded

relayd and TLS / https §

Find below an example to add TLS support to relayd. There are a few things to take care of about certificates and keys.

http protocol "https" {
  match request header remove "Proxy"
  match response header set "X-Xss-Protection" value "1; mode=block"

  return error

  tls keypair athome.tld
  tls keypair here.tld

relay "tlsforward" {
  listen on port 443 tls
  protocol "https"
  forward with tls to port 443

Look at the lines starting with "tls keypair". They define the certificate and keys to use for TLS. In the above example, two certificates for two different domains are used, you can add as much as you have certificates to use.

However, thos certificates MUST be stored with the appropriate filename in the correct location :


Make sure /etc/ssl/athome.tld.crt is the "full chain".

This means you should have such configuration in acme-client configuration :

domain athome.tld {
    domain key "/etc/ssl/private/athome.tld.key"
    domain certificate "/etc/ssl/athome.tld-cert.crt"
    domain chain certificate "/etc/ssl/athome.tld-chain.crt"
    domain full chain certificate "/etc/ssl/athome.tld.crt"
    sign with letsencrypt

This way, relayd will automatically pick up the right certificate.

relayd and certificates renewal §

Remember to reload relayd after certificate renewal :

/usr/sbin/acme-client -v athome.tld && \
    /usr/sbin/rcctl reload relayd

Relayd and IPv6 §

If you want to add ipv6 support with relayd, it is obviously possible.

First, make sure you set the local address in "/etc/hosts" : localhost
::1 localhost

You can now use "localhost" to refer to both ipv4 or ipv6 local depending on context.

Now add two entries in relayd configuration:

relay "http" {
        listen on $ext_ip4 port 80
        protocol "http"
        forward to port 80

relay "http6" {
        listen on $ext_ip6 port 80
        protocol "http"
        forward to ::1 port 80

httpd's configuration will make use of "localhost" so it's simpler :

listen on localhost port 80

Relayd and security headers §

You can set a few headers to improve your website security. It's mostly useful if you host huge webapps, not really for static websites.

match request header remove "Proxy"
match response header set "X-Xss-Protection" value "1; mode=block"
match response header set "Frame-Options" value "SAMEORIGIN"
match response header set "X-Frame-Options" value "SAMEORIGIN"
match response header set "X-Robots-Tag" value "index,nofollow"
match response header set "X-Permitted-Cross-Domain-Policies" value "none"
match response header set "X-Download-Options" value "noopen"
match response header set "X-Content-Type-Options" value "nosniff"
match response header set "Permissions-Policy" value "interest-cohort=()"

If you only host one domain, add :

match response header set "Access-Control-Allow-Origin" value "athome.tld"

Learn more

Optimize client cache and bandwidth usage §

You should consider to tune up th amount of request a client should make each time he checks on your website. As example, you can specify to keep in cache files such as pictures, stylesheets or fonts for a few days before asking again.

In "protocol" section, just before "pass" keyword, add :

match request path "/*.css" tag "CACHE"
match request path "/*.js" tag "CACHE"
match request path "/*.atom" tag "CACHE"
match request path "/*.rss" tag "CACHE"
match request path "/*.xml" tag "CACHE"
match request path "/*.jpg" tag "CACHE"
match request path "/*.png" tag "CACHE"
match request path "/*.svg" tag "CACHE"
match request path "/*.gif" tag "CACHE"
match request path "/*.ico" tag "CACHE"

match response tagged "CACHE" header set "Cache-Control" value "max-age=1814400"

Everytime a client ask for a file ending with ".css" or ".js" or ".atom" (...), relayd tag the resquest with "CACHE". At last we add a header to increase cache to 21 days to requests with this tag.

Set default encoding §

Following the same scheme, you can specify the default encoding according to file extension :

match request path "/*.html" tag "UTF8"
match request path "*/" tag "UTF8"
match response tagged "UTF8" header set "Content-Type" value "text/html;charset=UTF-8"

About relayd tags §

Understand you can't set multiple tags at once. If you want to apply the two above headers (cache-control and content-type), you must do it separately since relayd configuration is processed in order :

match request path "/*.html" tag "CACHE"
match response tagged "CACHE" header set "Cache-Control" value "max-age=1814400"

match request path "/*.html" tag "UTF8"
match response tagged "UTF8" header set "Content-Type" value "text/html;charset=UTF-8"

Databases §

Databases are essentials to organize data linked to each others.

As example, a blog engine needs to know who commented which article, at which date and by whom. This comment has a link, the author may have left a mail address to get an alert when there is an answer... All those are tied together.

When you self host, you may not need a huge database engine. In this scenario, SQLite is a better choise : easy to backup and light even if there are less features.

SQlite §

SQlite is amazing.

It is easy and light. To backup, just copy a file. It is more than enough in mosts cases.

To install, just :

# pkg_add sqlite3.

To use with PHP, add "php-pdo_sqlite-*" and "php-sqlite3-*".

To backup the database, just copy the database file. That's it 😊.

MariaDB (MySQL) §

Very well known database engine, MySQL or MariaDB is often required in webapps. Make sure to understand how to secure your installation as it is a sensitive software.

Read "/usr/local/share/doc/pkg_readmes/*" related to mariadb install.

To use with PHP, install php-mysqli-* and php-pdo_mysql-* then enable extensions as explained in PHP's part.

Afin d'installer MariaDB, il faut lancer les commandes suivantes :

To install MariaDB :

# pkg_add mariadb-server
# /usr/local/bin/mysql_install_db

The second command install a default database.

To start mysql :

# rcctl enable mysqld
# rcctl start mysqld

Finally, use the following command to improve mysql safety :

# /usr/local/bin/mysql_secure_installation

To let httpd talk with MariaDB (it is chrooted), launch thos command to reproduce root structure with appropriate permissions :

# install -d -m 0711 -o _mysql -g _mysql /var/www/var/run/mysql

Add those lines to /etc/my.cnf to change MariaDB socket location so it is accessible to httpd :

    socket = /var/www/var/run/mysql/mysql.sock

    socket = /var/www/var/run/mysql/mysql.sock

At last, restart mysql :

# rcctl restart mysqld

Now you can add users and databases.

As example, we will show how to create a database for Wordpress.

Enter "mysql -u root -p" To get to the MariaDB shell. Below see a log of inputs and ouputs.

# mysql -u root -p
Enter password:
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 3
Server version: 10.0.23-MariaDB-log openBSD port: mariadb-server-10.0.23p0v1

Copyright (c) 2000, 2015, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]> CREATE DATABASE wordpress_base;
Query OK, 1 row affected (0.01 sec)

MariaDB [(none)]> CREATE USER 'wp'@'localhost' IDENTIFIED BY 'password';
Query OK, 0 rows affected (0.01 sec)

MariaDB [(none)]> GRANT ALL PRIVILEGES ON wordpress_base.* TO 'wp'@'localhost';
Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

MariaDB [(none)]> exit

That's it 😊.

To get backup a MariaDB base (a dump) to db-name :

# mysqldump -u root -p db-name > /var/backup/db_backup

Of course, edit user "root" and "db-name".

To restore the database :

Notice the "<" backwards.

PostgreSQL §

PostgreSQL is another database engine. Install postgresql-server to use it.

With PHP, you'll need php-pgsql-* and php-pdo_pgsql-*.

Read /usr/local/share/doc/pkg-readmes/postgresql* carefully.

Create a default database :

# su - _postgresql
$ mkdir /var/postgresql/data
$ initdb -D /var/postgresql/data -U postgres -A scram-sha-256 -E UTF8 -W
$ exit

Default user is postgres.

Edit /var/postgresql/data/postgresql.conf to suit your needs.

To let httpd access postgresql, you should have :

unix_socket_directories = '/var/www/tmp/postgresql, /tmp'

You have to edit permissions on this directory :

# mkdir -p /var/www/tmp/postgresql
# chown _postgresql:www /var/www/tmp/postgresql

To start postgresql, as usual :

# rcctl enable postgresql
# rcctl start postgresql

To get the postgresql shell :

# su _postgresql -c psql

Below a few examples to dael with postgresql :

Change admin password :

# psql -U postgres -c "ALTER USER postgres WITH PASSWORD 'new_password'";

Add "toto" user :

# psql -U postgres -c "CREATE USER toto WITH PASSWORD 'password';"

Créer une nouvelle base et donner à toto tous les droits dessus :

Create a new database and let toto do everything he wants with :

# psql -U postgres 
\connect template1

To save a database with postgresql, you actually save all the instructions to recreate the database :

# pg_dump db-name > /var/backup.db

To restore the base :

# psql -U postgres db-name < /var/backup.db

Webapps you can host §

There are numbers of webapps : Wikis, Blogs, CMS, Webmails...

Most of the time, you'll need PHP and sometimes a database engine to run them.

⚠ Make sure to check official documentation related to each webapps, and check release notes as long as you host these apps to keeps your install secure.

Most of the time, the steps to install a webapp are the same.

Nice webapp selection §

There are so many webapps you may ask yourselves which one you should consider. Below I suggest a few of them with a few requirements :

One can appreciate :

Where can I find other apps to host ? §

Look on alternativeto.

This github selection is well filled.

Webmail attachments or tools requiring file upload §

Attachments size are 35M by default with smtpd mail server, that's why you should edit php-*.ini configuration :

post_max_size = 35M
upload_max_filesize = 35M

You have to set this limit in httpd's configuration too :

connection max request body 36700160

About hosting emails §

To get in charge of your communication tools is a big deal to more freedom and independence. This part will describe how to host an email server at home. No database will be required because it has no benefit on a small server. Let's keep things simple !

Setting up a mail serer is often considered difficult. That's why we will split this tutorial in small easy steps : complex, but not complicated.

In order, you will :

Of course, you'll learn how to anew accounts and how to configure a mail client.

You first have to open port 25 (smtp) in your firewall and router.

Some ISP are very restrictive with this port. You can ask them to be more permissive, but make inquiries first to avoid bad surprises. At last resort, we'll discuss how to use external smtp serices.

Read also opensmtpd main developper article about this subject.

DNS configuration for an email server §

Emails requires specific DNS records. You have add at least two new fields.

First, make sure you have an "A" record (probably) :

athome.tld.  IN  A

Of course, use your real IP here.

You must add a MX field pointing to the previous record :

athome.tld.  IN  MX  1  athome.tld.

Notice ending dots.

"1" describe the weight of the field. This parameter will be used to prioritize mails servers because some of them will be backups if the main server is unavailable. We'll talk about this later.

Some may want to use a specific domain, another A field to organize their mail servers. That is not mandatory, but interesting :

mail1.athome.tld. IN  MX  1  mail1.athome.tld..
mail1.athome.tld. IN  A

Below is another example using a different syntax :

$ORIGIN athome.tld
@      IN  MX  10  mail1.athome.tld.
@      IN  A
mail1  IN  A

Here, you can see the mail server for this zone is hosted on a different IP.

That's all for now.

Get certificates §

To encrypt traffic with your email server, you'll need TLS certificates.

It is explained in the chapter about httpd.

However, you can use self-signed certificates if you prefer, that is absolutely not an issue. (seel man ssl(8))

In the next parts, we'll consider using letsencrypt certificates to keep consistent.

Just remember to hae a certificate for the domain in the MX field of your server.

A mail server in 10 minutes (almost ^^) §

I you're in a hurry, here is a quick setup to have a working email server. It will also be a good starting point to build on later. With the following :

Later, we'll discuss virtual users to fix these minor issues. But here, we're in a rush so let's go! 😊

smtpd.conf file §

Edit /etc/mail/smtpd.conf :

# Generic config
# Tables 
table aliases "/etc/mail/aliases"

# Certificates
pki athome.tld key "/etc/ssl/private/athome.tld.key"
pki athome.tld cert "/etc/ssl/athome.tld.crt"

# Get messages
listen on all tls pki athome.tld 
# To send with mail client
listen on all port submission tls-require pki athome.tld auth 

action "sendthismail" relay 

# System aliases
action in_maildir maildir alias <aliases>

# Get
match for local action in_maildir
match from any for domain athome.tld action in_maildir

# Sending
match for any action "sendthismail"
match auth from any for any action "sendthismail"

That's it 😊

There a almost more comments than reals instructions.

Dovecot configuration (IMAP) §

Install dovecot : "# pkg_add dovecot".

In "/etc/dovecot/local.conf" add :

# listen on IPV4 and IPV6.
listen = *, [::]

# We use Maildir
mail_location = maildir:~/Maildir

# only username, not user@domain.tld to auth
auth_username_format = %n

# imap > pop
protocols = imap

# Safety. Edit those lines !
ssl = yes
ssl_cert = </etc/ssl/athome.tld.crt
ssl_key = </etc/ssl/private/athome.tld.key
disable_plaintext_auth = yes

# auth methods
passdb {
        driver = bsdauth

userdb {
        driver = passwd

Reload dovecot : "# rcctl restart dovecot".

Here you go, it's done, you can configure your mail client.

Full mail server with virtual users §

"Virtual users" means they are not UNIX system users with a shell. However, they are obviously using your mail server.

New accounts won't be created with "adduser" but you will edit a file with the username and it's password hash.

Here we'll discuss how to install a SMTP (Opensmtpd) and IMAP (Dovecot) server

One user to rule them all : _vmail §

We will create an user with the sweet name "_vmail" to manage every emails access. It won't have any shell, it's safer 😊.

# useradd -m -g =uid -c "Virtual Mail" -d /var/vmail -s /sbin/nologin _vmail

A new directory is created : /var/vmail. Emails will be stored inside, but following a structure with subdirectories for each virtual users. As example :


/etc/mail/virtuals §

In this file, we write user's emails, one on each line. Actually, it works like the file /etc/mail/aliases. (read man aliases(5) 😉)

heroes@athome.tld batman@athome.tld,superman@athome.tld
batman@athome.tld _vmail
superman@athome.tld _vmail
kiki@athome.tld _vmail

As you can see, every email addresses belongs to user _vmail.

Notice the first line : you see an example showing how to transfer emails sent to "heroes@athome.tld" to "batman@athome.tld" and "superman@athome.tld".

Notez que sur la première ligne, on a fait un alias à titre d'exemple pour transférer un mail d'une adresse à une autre.

/etc/mail/passwd §

Same as before, one line per passphrase :


Notice how ":" split email address and the hashed passphrase : <email>:<hash>.

To hash passphrases, use "encrypt" command :

encrypt -b a -p

Or "smtpctl encrypt passphrase".

(Facultative) Fine tuning files permissions §

Previous files shouldn't be readable by everyone. Let's adjust permissions so root and mail daemons -- dovecot and smtpd -- can read passwords. It is not mandatory, but I suggest this part as good habits that can hurt anyone 😉.

Dovecot and smtpd run as users _dovecot and _smtpd. If one day, one of this process is compromised, it will avoid privilege escalation and threaten the whole system.

Let's create a new group _maildaemons for _dovecot and _smtpd to ease permissions :

# groupadd _maildaemons
# usermod -G _maildaemons _smtpd
# usermod -G _maildaemons _dovecot

Of course, if you haven't installed dovecot yet since we discuss about it later : "# pkg_add dovecot".

Now we can set owner and group for files storing logins and passwords :

# chown root:_maildaemons /etc/mail/passwd /etc/mail/virtuals

Finally, only root can modify these files and _maildaemons can only read. Others can't do anything :

# chmod 640 /etc/mail/passwd /etc/mail/virtuals

To check everything is as expected :

# ls -l /etc/mail/passwd
-rw-r-----  1 root  _maildaemons  17226 Nov 12 08:40 /etc/mail/passwd

Opensmtpd (smtpd) config §

Opensmtpd or smtpd is the default mail server shipped with OpenBSD. You just have to configure it.

First of all, make sure you opened thos ports : 25 (smtp), 587 (submission) and 993 (imaps). The latter will be user with dovecot. Do not care about 465 (smtps) since it is deprecated.

To configure smtpd, edit "/etc/mail/smtpd.conf". Instructions will be applied in order.

We will split it in 3 parts :

Adjust the example below to your needs :

# Generic configuration
# Tables 
table aliases "/etc/mail/aliases"
table passwd "/etc/mail/passwd"
table virtuals "/etc/mail/virtuals"

# Certificates
pki athome.tld key "/etc/ssl/private/athome.tld.key"
pki athome.tld cert "/etc/ssl/athome.tld.crt"

# Listening ports
# Reception
listen on all tls pki athome.tld 
# Sending with a mail client
listen on all port submission tls-require pki athome.tld auth <passwd> 

action "sendthismail" relay 
action local_mail maildir alias <aliases>
action virtual_maildir maildir "/var/vmail/%{dest.domain:lowercase}/%{dest.user:lowercase}/Maildir" virtual <virtuals>

# In/Out
# Reception
# Message for virtual users
match from any for domain athome.tld action virtual_maildir
# Message for system users
match from any for local action local_mail

# Sending
match auth from any for any action "sendthismail"
match for any action "sendthismail"

There is almost nothing to change in this file, except domain name.

WAIIIIIT a minute! Tell me more!

Take a look at each lines :

First, there are generic instructions

Then, we define actions applied on envelopes.

At last, we apply actions according to envelope criterias.

If not mentioned, the rule is matching for a local mail. In other case, we add "from any".

Finally, in order to label your outgoing messages with the appropriate domain name, add in "/etc/mail/mailname" your email's domain name. It's the domain mentioned in the MX record.


Now enable and start smtpd :

# rcctl enable smtpd
# rcctl restart smtpd

That's all for smtpd 😊.

Dovecot §

We will use Dovecot as IMAP server so you can read mails from a client like Thunderbird.

As usual, install dovecot with pkg_add :

# pkg_add dovecot

Now configure dovecot. Edit "/etc/dovecot/local.conf" :

# listen IPv4 and IPv6
listen = *, [::]

# imap
protocols = imap

# Encryption. Edit those lines according to your certificates.
ssl = yes
ssl_cert = </etc/ssl/athome.tld.crt
ssl_key = </etc/ssl/private/athome.tld.key
disable_plaintext_auth = yes

# where mails are stored. %d is domain, %n is username
mail_location = maildir:/var/vmail/%d/%n/Maildir

# essential since we edited persmission on /etc/mail/passwd
service auth {
    user = $default_internal_user
    group = _maildaemons

# Auth methodes
passdb {
    args = scheme=blf-crypt /etc/mail/passwd
    driver = passwd-file

# Mails are /var/vmail , belongs to _vmail
userdb {
    driver = static
    args = uid=_vmail gid=_vmail home=/var/vmail/%d/%n/ 

I left comments above to help you understand what is done here.

Adjust ssl_cert and ssl_key according to your certificates.

By the way, ssl configuration is already available in "/etc/dovecot/conf.d/10-ssl.conf". It is supposed to help up with a script generating a self-signed certificate. However, you probably already have your own. You should comment lines in this file :

# /etc/dovecot/conf.d/10-ssl.conf
#ssl_cert = </etc/ssl/dovecotcert.pem
#ssl_key = </etc/ssl/private/dovecot.pem

Finally, reload mail daemons :

# rcctl enable dovecot
# rcctl start dovecot
# rcctl restart smtpd

Now you can use a mail client to read your messages.

Add a mail account §

Add a new line for the new account in "/etc/mail/virtuals" and "/etc/mail/passwd" then reload tables for smtpd (dovecot read them on the fly) :

smtpctl update table virtuals
smtpctl update table passwd

Or "rcctl restart smtpd"

Manage multiple domains §

You can host an email server and manage multiple domain names.

However, you should organize how you set this up.

Below are a few notes on how to achieve this.

smtpd changes for multi-domains §

I suggest to create a file containing every hosted domain, one per line. Let's call il "/etc/mail/domains" :


This, in "/etc/mail/smtpd.conf" you can write one line for multiple domains :

table domains "/etc/mail/domains"
match from any for domain <domains> action virtual_maildir

Take care of used TLS certificates. If you have a certificate for each domain, you can specify each of them in smtpd.conf. Make sure you have a default certificate in the end ("*").

pki athome.tld key "/etc/ssl/private/athome.tld.key"
pki athome.tld cert "/etc/ssl/athome.tld.crt"
pki domain.tld key "/etc/ssl/private/domain.tld.key"
pki domain.tld cert "/etc/ssl/domain.tld.crt"
pki key "/etc/ssl/private/"
pki cert "/etc/ssl/"
pki "*" key "/etc/ssl/private/athome.tld.key"
pki "*" cert "/etc/ssl/athome.tld.crt"
listen on all tls
listen on all port submission tls-require auth <passwd>

HOWEVER, you can use only one certificate matching multiple domains. To do so, use "alternative names" in acme-client configuration. It is absolutely valid and much easier to manage. If so, configure smtpd as if there was only one certificate.

dovecot changes to support multiple domains §

Dovecot will need some care to handle certificates for each domain. Add sections "local_name" in its configuration so it looks like this :

ssl = yes
ssl_cert = </etc/ssl/athome.tld.crt
ssl_key = </etc/ssl/private/athome.tld.key
# no plaintext
disable_plaintext_auth = yes

local_name domain.tld {
        ssl_cert = </etc/ssl/domain.tld.crt
        ssl_key = </etc/ssl/private/domain.tld.key
local_name {
        ssl_cert = </etc/ssl/
        ssl_key = </etc/ssl/private/

Here also, a single certificate for multiple domains is much more easier to set up.

Redirecting mails §

As explained in man aliases(5), you can redirect mails to various destination.

As example, you host jdoe@athome.tld and want to redirect every mails received by jdoe to

Edit /etc/mail/virtuals and add :


You can set redirections in /etc/mail/aliases too. For system users, you can use their login only :

root: jdoe
hostmaster: root
postmaster: root
webmaster:  bruce
jdoe: jdoe@domain.tld

Remember to restart smtpd so changes are considered :

# rcctl restart smtpd

Easy. 😎

How to configure you mail client ? §

To use your mail server with a client such as Thunderbird, use the parameters below :

DNS record for clients configuration (facultative) §

To help clients finding automatically how to configure parameters to use your mail server, you can add specific DNS records.

_submission._tcp.athome.tld  86400   IN  SRV 0 1 587 athome.tld
_imap._tcp.athome.tld        86400   IN  SRV 0 0 0   .
_imaps._tcp.athome.tld       86400   IN  SRV 0 1 993 athome.tld

Read rfc6186 about these records.

Do not loose mails : MX fields and backup §

If, for reasons, your server is offline for a few days or a week, mails could be lost. Normally, senders try again for a while if they couldn't deliver a message.

However, you can plan secondary mail servers to keep your messages if yours is unreachable. You only need :

It will keep messages for your server in queue and will deliver them as soon it gets back online.

On your side, add a new MX field in your DNS zone with a bigger weight. This field points to the secondary server :

@                     IN MX 10  athome.tld.
@                     IN MX 70

In the above example, your server is ligther (10) than the secondary (70). This means a sending server will try first to deliver on your server, then on the secondary if the first failed to answer.

On his side, your friend just has to add your domain as a backup. If he uses smtpd, then /etc/mail/smtpd.conf should look like this :

action relaybackup relay backup mx "athome.tld" 
match from any for domain athome.tld action relaybackup

Of course, you should return the favor 😉.

Do not be considered as a spam (SPF, DKIM...) §

Some servers could consider your mails as spams. There are a few proofs of good faith you can setup if possible. They are not mandatory. Keep in mind that most spams comes from most well known servers (gmail, I'm looking at you) so don't be too hard on yourselves.

Reverse DNS §

Your ISP might let you configure a reverse DNS. As it suggests, a reverse DNS links your IP to your domain name.

Look in your ISP panel for reverse DNS, or ask them directly. They are responsible for this 😉.

If you can't and reaaaallly want a rDNS, you could rent a VPN and get a dedicated IP with a rDNS configured by the VPN provider. But in most cases, following next steps should be sufficient.


SPF records show that only YOUR server is allowed to send mails for YOUR domain name. Since it's usually the server's admin who also deals with DNS record, it's a proof of good faith.

Add a DNS record of type SPF in your zone such as :

athome.tld.   SPF "v=spf1 a mx ~all"

Or use a TXT field if SPF is not available :

athome.tld. TXT "v=spf1 a mx ~all"

Above is a very simple example that works for most cases. Consider reading about SPF records if you want to fine-tune this record.

DKIM signing §

With a private key, your server will sign outgoing emails. In DNS records, you will publish a public key to let recipient check if it matches the signature from your server.

Ahem... Say that again?

Here we go. We will generate a private and a public key.

Private key is used to sign mails. It is "private" because you must be the only ont able to add a signature to outgoind mails.

Public key displayed in DNS record -- viewable by all -- let one check signature authenticity. You can see it as a unique puzzle piece, the only one that can fit the puzzle.

We'll see two ways to sing outgoing messages : one with an smtpd extension, the other with dkimproxy. You also could to the same with rspamd, it is described later, following the same method to generate keys. Choose the one you prefer 😉.

Following commands below, you will create a directory for keys, set permissions on this folder and go inside before generating keys with openssl and set permissions on the private part :

# mkdir -p /etc/dkim/
# chmod 770 /etc/dkim/
# cd /etc/dkim/
# openssl genrsa -out private.key 2048               
# openssl rsa -in private.key -pubout -out public.key
# chmod 400 private.key

Add a DKIM or TXT field so anyone can check signature on your messages match what is published in DNS records.

Mails will receive a flag "dkimpubkey" when signed, it is used to identify the signature in DNS record.

Replace "..." the content of the file "public.key" :

# cat /etc/dkim/public.key

The record will look like this :

dkimpubkey._domainkey    IN TXT    ( "v=DKIM1; k=rsa; t=s;p=v+Fb...vhP/oB")

Since smtpd has filters support, you can sign your messages with port opensmtpd-filter-dkimsign :

# pkg_add opensmtpd-filter-dkimsign

Make sure the script can read keys generated as seen before :

# chown -R _dkimsign:_dkimsign /etc/dkim/

In "/etc/mail/smtpd.conf", you add now a new filter with instructions on how to sign messages :

filter "dkimsign" proc-exec "filter-dkimsign \
    -d <domain> \
    -s <selector> \
    -k /etc/dkim/private.key" \
    user _dkimsign group _dkimsign

Replace "<domain>" with your domain name and "<selector>" by "dkimpubkey" : that's what we defined in the DNS field earlier.

Now, make sure outgoing mails are processed by this new filter. In "/etc/mail/smtpd.conf", the line for outgoing messages now look like this :

Enfin, il vous suffit de faire passer les mails sortants par ce filtre. On modifie la ligne correspondant à l'envoi des messages dans "/etc/mail/smtpd.conf":

listen on all port submission tls-require auth <passwd> filter "dkimsign"

As an alternative, you can use dkimproxy if you prefer to sign mails.

As usual :

# pkg_add dkimproxy

Make sure dkimproxy can read keys :

# chown -R _dkimproxy:_dkimproxy /etc/dkim/

Now configure dkimproxy to match you configuration (domain and DNS selector) :

In "/etc/dkimproxy_out.conf" :

domain    athome.tld
signature dkim(c=relaxed)
signature domainkeys(c=nofws)
keyfile   /etc/dkim/private.key
selector  dkimpubkey

Of course, edit domain and selector according to your DNS record.

Now, tell smtpd to listen for incoming signed and to-send messages on port 10028. They will be tagged "DKIM". Then we can send mails with this tag. On the contrary, they are forwarded to dkimproxy on port 10027.

In /etc/mail/smtpd.conf :

listen on lo0 port 10028 tag DKIM   
match tag DKIM for any action "sendthismail"
match auth tag DKIM from any for any action "sendthismail"

action dkimproxy relay host smtp:// 

match auth from any for any action dkimproxy
match for any action dkimproxy

Finally enable dkimproxy and restart smtpd :

# rcctl enable dkimproxy_out
# rcctl start dkimproxy_out
# rcctl restart smtpd

Check it works §

Follow instructions on You'll send a mail to a randomized recipient and get a score :


You may read about dmarc and other advices if you want to fin tune your configuration. Remember your score is already better than most "big" provider. Last time I checked with a gmail address, I got 6.1/10... 😜

Blocking ISP : use external SMTP §

If your ISP restrict the use of smtp port (25), you can't send any email from your server. To fix this, you may :

However, you need another smtp provider. Put the necessary credentials to access this mail account in "/etc/mail/secrets" :

# echo "secret_id user:passphrase" > /etc/mail/secrets

Make sure permissions are appropriate, you don't want everyone to know your password :

# chmod 640 /etc/mail/secrets
# chown root:_smtpd /etc/mail/secrets

Then, edit "/etc/mail/smtpd.conf" so outgoing messages go throught external mail server :

table secrets "/etc/mail/secrets"
listen on all...

action "relay" relay host smtp+tls:// \
      auth <secrets> mail-from "@athome.tld"

match from any for any action "relay"

Some details :

In the end, reload smtpd.

rcctl restart smtpd

Read the example in the end of smtpd manpage to learn more.

Avoid receiving spam : senderscore filter §

Senderscore keeps a database of mail server reputation.

Havin a moderate reputation doesn't mean much, but having a bad one means the server already has sent spams before.

You can use a filter in smtpd to recognize as spam incoming mails according to senderscore.

Install opensmtpd-filter-senderscore port.

Then, edit /etc/mail/smtpd.conf to add a filter :

filter senderscore \
         proc-exec "filter-senderscore -junkBelow 70 -slowFactor 2000"
listen on all tls pki athome.tld filter { senderscore }

Avoid receiving spams : spamassassin §

spamassassin is a well-known anti-spam. It is used by OpenBSD project with spamd for their mailing lists. Let's see how to use it with smtpd using tags since the setup is interesting if you want to understand how things works. You may instead want to use the filter opensmtpd-filter-spamassassin : install the port then read /usr/local/share/doc/pkg-readmes/opensmtpd-filter-spamassassin.

Install appropriate ports :

# pkg_add p5-Mail-SpamAssassin spampd

Run spamassassin daemons.

You may add a few flags to make sure it is running as we expect.

# rcctl enable spampd
# rcctl set spampd flags "--relayhost="
# rcctl start spampd
# rcctl enable spamassassin
# rcctl start spamassassin

Every incoming mails now must be checked by spammassassin, which is listening on port 10025. Once they've been scanned, they will be delivered on port 10026 and tagged "SPAMASSASSIN" so we don't check them twice.

That's how it looks in "/etc/mail/smtpd.conf" :

# mails checked by spammassassin
listen on lo0 port 10026 tag SPAMASSASSIN
action spamassassin relay host smtp:// 
# system mails
accept from local for local alias <aliases> deliver to maildir "~/Maildir"
# Mails scanned by SPAMASSASSIN
match tag SPAMASSASSIN from any for domain "athome.tld" action virtual_maildir
# not tagged mails, must be checked by spammassassin
match from any for domain "athome.tld" action spamassassin

Spams subject are modified by spammassassin.

You can automatically delete them, but it's a risk in case of false positive.

In order to save spams in a junk folder, modify smtpd's action so it checks for "X-spam" header :

# /etc/mail/smtpd.conf
action virtual_maildir maildir "/var/vmail/%{dest.domain:lowercase}/%{dest.user:lowercase}/Maildir" junk virtual <virtuals>

Avoid receiving spams : rspamd §

Rspamd is a very complete mail filter. It is not only an anti-spam, but can handle greylisting, DKIM...

It is also very fast and efficient. If you want to use it, you should read it's official documentation. For now, let's see how to use it with OpenBSD's smtpd as an antispam and for DKIM.

Install rspamd §

# pkg_add rspamd redis opensmtpd-filter-rspamd
# rcctl enable redis rspamd
# rcctl start redis rspamd

Add rspamd to /etc/mail/smtpd.conf §

Just add a new filter named "filter-rspamd" and use it for incoming messages.

filter rspamd proc-exec "filter-rspamd"

# filtre en reception
listen on all tls pki athome.tld \
    filter { rspamd }

DKIM with rspamd §

Since rspamd can handle DKIM signatures, you won't have to configure dkimproxy or another tool.

Create keys as described before and make sure they belong to _rspamd group.

# chown -R _rspamd:_rspamd /etc/dkim/

Remember to edit your DNS zone 😉.

Now create "/etc/rspamd/local.d/dkim_signing.conf" file :

# If true, username does not need to contain matching domain
allow_username_mismatch = true;

path = "/etc/dkim/private.key";
selector = "dkimpubkey";

Then add a few lines in "/etc/mail/smtpd.conf" to sign outgoing messages :

filter rspamd proc-exec "filter-rspamd"
# Send and DKIM sign with rspamd
listen on all port submission tls-require pki athome.tld auth \
    filter { rspamd }

Greylisting §

Rspamd does greylisting by default. If you want to still use spamd instead, disable rspamd greylisting in file "/etc/rspamd/local.d/actions.conf" :

greylist = none;

And file /"etc/rspamd/local.d/greylist.conf" :

enabled = false;

Spamtraps §

Contrairement à spamd, rspamd ne garde pas captif les spammeurs qui écrivent sur une spamtrap. Cela sert tout de même à reconnaître des spammeurs pour plus tard.

You can use a spamtrap (like what spamd does) using the following lines in "/etc/rspamd/local.d/spamtrap.conf" :

action = "no action";
learn_spam = true;
map = file://$LOCAL_CONFDIR/maps.d/;

enabled = true;

Then fill with regular expressions of fake trap mail addresses : "/etc/rspamd/maps.d/"


Blacklists §

To do so, use multimap module.

rspamd WebUI §

Configure admin following these instructions

Then, dig a SSH tunnel from your computer and open in a browser http://localhost:9999.

ssh -N -L 9999: sshuser@athome.tld

Enjoy wonderfull charts 😄

Manage spams with dovecot §

You might want to help the anti-spam to learn if it missed spams or got wrong for some messages.

If you use dovecot and a mail client with IMAP protocol, you can move spams in "Junk" folder so the anti-spam recognize them in the future. On the contrary, moving a mail from "Junk" to "INBOX" folder indicates it is legitimate.

No matter if you use rspamd or spammassassin, we will see both. 3 steps are necessary :

1. Install dovecot plugin.

2. Enable and configure the plugin.

3. Create scripts so the anti-spam can learn.

It is described in dovecot documentation (you should read 😉)

First, let's install dovecot pigeonhole plugin :

# pkg_add dovecot-pigeonhole

Add "imap_sieve" in plugins list to enable it at the end of "/etc/dovecot/local.conf" :

protocol imap {
	mail_plugins = $mail_plugins imap_sieve

Then, still in dovecot's local.conf, configure sieve plugin si it run scrits according to where messages are moved..

plugin {
	sieve_plugins = sieve_imapsieve sieve_extprograms

	# When a message is moved in Junk folder; report as spam
	imapsieve_mailbox1_name = Junk
	imapsieve_mailbox1_causes = COPY
	imapsieve_mailbox1_before = file:/usr/local/lib/dovecot/sieve/report-spam.sieve

	# When a mail is removed from Junk folder : it is nor a spam 
	imapsieve_mailbox2_name = *
	imapsieve_mailbox2_from = Junk
	imapsieve_mailbox2_causes = COPY
	imapsieve_mailbox2_before = file:/usr/local/lib/dovecot/sieve/report-ham.sieve

	sieve_pipe_bin_dir = /usr/local/lib/dovecot/sieve

	sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.execute

Now create two files in the directory "/usr/local/lib/dovecot/sieve/".

The first is "report-spam.sieve":

require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];

if environment :matches "imap.user" "*" {
  set "username" "${1}";

pipe :copy "" [ "${username}" ];

The second is "report-ham.sieve" :

require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];

if environment :matches "imap.mailbox" "*" {
  set "mailbox" "${1}";

if string "${mailbox}" "Trash" {

if environment :matches "imap.user" "*" {
  set "username" "${1}";

pipe :copy "" [ "${username}" ];

Now reload dovecot and compile the above scripts :

# rcctl restart dovecot
# sievec /usr/local/lib/dovecot/sieve/report-spam.sieve
# sievec /usr/local/lib/dovecot/sieve/report-ham.sieve

As you may have notices, the above files call "" and "". Thoses scripts will be different wether you use rspamd or spammassassin since they don't learn spams/hams with the same command. Read the next part 😉.

Learning scripts for spamassassin §

Scripts below are stored in "/usr/local/lib/dovecot/sieve".

/usr/local/bin/spamc -u ${1} -L spam -C report

/usr/loca/bin/spamc -u ${1} -L ham -C report

Learnin scripts for rspamd §

Scripts below are still in "/usr/local/lib/dovecot/sieve".

exec /usr/local/bin/rspamc -d "${1}" learn_spam

exec /usr/local/bin/rspamc -d "${1}" learn_ham

Scripts for both §

Scripts must be executables :

# chmod +x /usr/local/lib/dovecot/sieve/learn-*.sh

At last, reload dovecot:

# rcctl reload dovecot

Here you go, now your anti-spam will learn when you'll move messages in approriate folders.

Learn more with dovecot official documentation.

Fight against spams before receiving one : OpenBSD's spamd §

You can sort incoming spams into a Junk folder. But imagine annoying spammers 😁? That's what spamd can do, faking being a mail server and holding spammers so they can't deliver spams while it's talking to them veeery slowly.

How does spamd work ? §

You can let somewhere on the web a fake mail address : a spamtrap. Everyone sending a message to this address will be blacklisted and trapped.

In the meantime, new senders are greylisted. They're told to try again later. Every proper email server will try again. Spammers won't, so you don't get the spam. Others who try again correctly are put on a whitelist.

There is also a blacklist-only mode, trapping IPs from lists you already have.

However, this means senders do things correctly. That's is sadly not always true for commercial senders. This said, it is easy to put a server on a whitelist.

To keep track of good and bad senders, the command "spamd-setup" should be run periodically.

Everything is very well explained in spamd(8) manpage. You must read it since the following are just advices, not a complete howto.

Set spamd up §

# rcctl enable spamd
# rcctl set spamd flags "-v -G 25:4:864"
# rcctl start spamd

Also enable spamlogd to keep track of servers already whitelisted and to add to whitelist servers you write to.

# rcctl enable spamlogd
# rcctl start spamlogd

spamd is running in front of smtpd (like a shield, or a janitor 😉). To do so, edit packet-filter configuration : spamd should redirect mails to smtpd if it is not a spam.

Add in /etc/pf.conf :

table <spamd-white> persist
table <nospamd> persist file "/etc/mail/nospamd"
pass in on egress proto tcp to any port smtp divert-to port spamd
pass in on egress proto tcp from <nospamd> to any port smtp
pass in log on egress proto tcp from <spamd-white> to any port smtp

On line order :

Remember to reload pf :

# pfctl -ef /etc/pf.conf

It is necessary to reload spamd blacklist in pf. Edit root's crontab with "# crontab -e" then uncomment the line below :

~  *  *  *  *  /usr/libexec/spamd-setup

With "~", spamd-setup is run at a random schedule 😉.

Spamtrap with spamd §

Let somewhere on the web fake email addresses : spamtraps. Everyone writing to these addresses is a spammer and will be trapped. It is important thos addresses don't really exist!

To keep these spamtraps for bots only, you can add such html snippets on your website so they are invisible for human eyes :

<a href="mailto:trap@athome.tld"></a>

You can copy it on public pastebins.

To teach spamd what are spamtraps, use the command :

# spamdb -T -a 'trap@athome.tld'

Use pre-existing black/white lists §

Some nice people gather IP lists and let everyone download them. To use such lists, edit /etc/mail/spamd.conf.

As example, the default configuration uses :

nixspam's list

Add in /etc/mail/spamd.conf the following lines :


# Nixspam recent sources list.
# Mirrored from
        :msg="Your address %A is in the nixspam list\n\
        See for details":\

At the beginning, you can read "all". Then are written lists you want to use and configured below, separated with ":".

Use an URL to download the list with spamd-setup automatically or a file already on your server.

bsdly's blacklist §

Peter N.M.Hansteen publish a blacklist, gathered with its own spamtraps.

Avoid to download it too often or you'll be put on a blacklist. Be king with Peter's bandwidth 😉.

To get the list every hour past 20 minutes :

20 * * * *   /usr/local/sbin/bsdly-spamd

Here is bsdly-spamd script you should copy and make executable :

# update traplist into DST
# alternative URL

ftp -o "${DST}" "${URL}"

Then add the new list in /etc/mail/spamd.conf so spamd uses /var/db/bsdly.traplist file :


      :msg="Your address %A is in the nixspam list\n\
      See for details":\

      :msg="SPAM.  Your address %A has sent spam within the last 24 hours.  See for details.":\

uceprotect black and whitelists §

See official website.

Below we describe how to periodically download lists provided by uceprotect. We will use rsync to reduce bandwidth usage.

Add a new cronjob to call a script downloading lists. Not too often or you'll be blacklisted.

10 * * * *     /usr/local/sbin/uceprotect-spamd 

/usr/local/sbin/uceprotect-spamd is the script below :


RSYNC="/usr/bin/openrsync -a"
mkdir -p "${OUT}"

for URL in ${URLS}; do
      ${RSYNC} "${URL}" "${OUT}"

Load the lises in /etc/mail/spamd.conf. There are white and blacklists.



      :msg="Your address %A is listed on UCEPROTECT-Level 1\n \

      :msg="Your address %A is listed on UCEPROTECT-Level 2\n \


Do not hesitate do donate to for all their work 😄.

Common issues with greylisting §

Some big mail provider uses multiple servers to send mails. If you do greylisting, the IP trying to send an email from a provider might change between two tries. This means spamd can't recognize a legitimate server, never whitelist the IP and you never get the mail.

However, you can whitelists those domains if you want.

Create a file containing domains not to greylist : /etc/mail/nospamd_domains_list.txt :

Add more domains if you need to.

Now create a script to get sending IP of above domains : /usr/local/sbin/generate-nospamd :

# /usr/local/sbin/generate-nospamd
# Auteur :      prx <>
# licence :     MIT


echo "#$(date)" > "$WHITELIST"
smtpctl spf walk < "${DOMAINS}" >> "$WHITELIST"
exit 0

Call this script daily with /etc/daily.local and reload nospamd table :

# /etc/daily.local
pfctl -t nospamd -T replace -f /etc/mail/nospamd

See spamd activity §

Run "spamdb" command to see what's happening :


Read spamdb(8) manpage to learn what it means.

To translate times in human-readable format, run "date" with -r flag :

$ date -r 1462699174
    Sun May  8 11:19:34 CEST 2016

You can put on whitelist an IP with "spamdb -a" :

# spamdb -a ""

Share spamd data between backups §

Remember to keep spamd databases synchroniszd between your server and backups (secondary MX). Read about -Y and -y flags in spamd manpage.

Check everything works well §

To make sure your mail server works as expected, you can send an email to friends. however, this leads to issues :

Luckily there are robots taht can automatically answer. As example :

Take a look at mxtoolbox and mail-tester too, the last one is very nice.



What if smtp server doesn't work as expected? §

You should look at smtpctl command to get details about what's happening.

As example, to have generic status :

# smtpctl show stats

To see awaiting envelopes :

# smtpctl show queue
1101f6e60c169eac|inet4|mta|||||1491327053|1491672653|0|5|pending|3154|Network error on destination MXs
1a2123e1c2b3e462|inet4|mta|||||1491333449|1491679049|1491333849|1|inflight|50|Network error on destination MXs

To see in real time what's happening :

# smtpctl monitor

And of course; read "man smtpctl" 😉.

Domain name server §

DNS (Domain Name System), are road signs of the Internet. That's how a human can recognize server's names, like a big directory but for Internet.

It is also used to know whre a mail should be delivered, and many others things hidden to normal human beings but crucial to have a reliable network.

DNS is based on the idea of a "zone". First, you start at the root represented by a dot ".". The root list addresses of all authoritary name servers : tld (fr, de, com, net, org...). Each tld has a list of servers for the next levels.

That's how it looks like :

 |       |       |      |       |       |
 v       v       v      v       v       v
.fr    .com    .tld   .xyz    .org    .net
   |             |              |
   v             v              v
site.tld     athome.tld     other.tld
                 |                  |
                 v                  v
         webmail.athome.tld   wiki.athome.tld

If you start at the root, you can find the IP of any domain name. Searching through the zone is called "resolving". A server hosting data about a zone is "authoritary".

When a resolver retrieve a device IP, for a website or other purpose, it is kept in cache, in memory, as long as it is specified in the Time To Live (TTL) of the zone. So it is not reached too often.

This validity expiration time means there will be a delay before every resolver update a fresh zone. However, a bigger TTL means less network stress. It's about balance.


A few years ago, clever people started worrying since DNS is critical. It had to be secured.

It is actually quite easy to be redirected on fake addresses. We need a way to ensure DNS authenticity.

To do so, domain name owner wite a zone and sign it. Resolvers check if the signature is correct.

Thus, you are certain to have reached the correct IP behind a domain name, not hijacked on a fake website.

One can publish safe data through DNS. That's the idea of DANE/TLSA to to publish fingerprints of TLS certificates.

Zone example §

Zone files uses a standard format understood by every domain name servers.

Below is an example of "/var/nsd/zones/athome.tld" :


@           IN SOA    master.athome.tld. hostmaster.athome.tld. (
          ; domain of DNS server
          ; followed by admin email address
          ; Here : hostmaster@athome.tld
          ; "@" is replaced by a "."
                    2014110502      ; serial number to increment
                                    ; after each changes
                    86400           ; Refresh
                    7200            ; Retry
                    3600000         ; Expire
                    172800 )        ; Negative Cache TTL

$ORIGIN athome.tld.
@           IN NS       master
@           IN NS       secondary

@           IN MX       10 mail1
@           IN MX       20 mail2

master      IN A
master      IN AAAA     2001:db8:1:1::2

mail1       IN A
mail2       IN A

ipv4only    IN A
ipv6only    IN AAAA     2001:db8:1:1::400
dualstack   IN A
dualstack   IN AAAA     2001:db8:1:1::200

gate        IN AAAA    %%ipv6_gate

master      IN A       %%ip_pub_master
master      IN AAAA    %%ipv6_master

secondary  IN  A       %%ip_pub_second
secondary  IN  AAAA    %%ipv6_second

DNS records and how to use them §

DNS records are usually written like this :


NAME is what you're looking for when you ask a resolver.

TTL is Time To Live, delay while the data is considered as valid.

Class means internet (IN). At the beginning, other classe were used but not anymore.

Type is the kind of data in the record.

At last, RDATA is the data relative to the Name.

@, $ORIGIN, $TTL §

"$ORIGIN" is the complete zone name. As example, "athome.tld".

When a zone file don't have a "$ORIGIN" instruction, the domain name server will create one with the zone name.

"@" is replaced by "$ORIGIN".

Read this page if you want to learn more about this instruction.

"$TTL" is the validity duration of datas. It is recommended to set to 1 day (1D).

Each DNS record can have its own TTL if you want to reduce network stress for very stable fiels such as MX and NS.

"$ORIGIN" and "$TTL" must be written at the beginning of the zone file.


The first record with "$ORIGIN" and "$TTL" is called SOA as in Source Of Authority. It is crucial.

The first field after SOA indicate the origin name server, in example master.athome.tld. and the last field indicates the domain administrator mail address. "hostmaster.athome.tld." will turn into "hostmaster@athome.tld". The first dot is turned into a "@". You may have set an alias on hostmaster when you configured the mail server. If you can't and have to use a dot before the "@" in the admin mail (you like it rough... 😲), you have to write a "\" before :


Autoritary domain name server and administrator mail may be on different servers or domains.

The serial number can have multiple formats, but always must increase everytime the zone is updated. Thus, secondary servers notice there is an update to download. Some admins use a timestamp, others start from 1 and increment each time. It's up to you.

SOA values (refresh, retry, expire, negative) and TTL in the example are those recommended in standard (RFC). Of course you can use your own values, but in doubt you have a robust example.

By default, values are seconds. You can write them as hours (H), days (D) or weeks (W).

"Refresh" and "retry" describe when secondary autoritative servers should reload the zone. Nowadays, most servers send alerts to secondary servers so it's automatic.

"Expire" tells how many time we can still use data in the zone if servers are unreachable. It is not TTL.

"Negative" is the delay a NXDOMAIN answer (non existent data requested) is kept in cache.


It may be one of the most important record type. Like others, they follow this format : {NAME, TTL, CLASS, TYPE, RDATA}.

Addresses of device master.athome.tld are recorded like this :

master          IN A
master          IN AAAA     2001:db8:1:1::2

Remember the last dot in addresses is very important. It represent the root zone. If a data doesn't end with a dot, it is expanded or will be buggy.

As example :

@                         1W  IN NS     machine

Is a record to machine.athome.tld.


@                         1W  IN NS     machine.other.tld.

Is not completed.


CNAME, means Canonical NAME, it is an alias.

In the following example, the real name of www is master.athome.tld.

www IN CNAME master

It is used for virtual hosts such as blog.athome.tld, webmail.athome.tld, ...

NS §

A DNS zone should have at least 2 NS records since they describe where are the authoritative servers. Actually, the zone will work with only one as long as the domain name server doesn't encounter any problem.

There is no NS servers maximum limit in theory. If you got two, that's a good start : yours and a friend's server.

athome.tld.   IN  NS   master.athome.tld.

This record means : for zone athome.tld, authoritative name server (NS) is master.athome.tld. It also can be written this way :

@     IN NS   master

In this case, the "@" is replaced by "$ORIGIN", actually its full name to the root, and master hasn't an ending dot so it is expanded with $ORIGIN too : "master.athome.tld".

Name servers must be known to the rest of the world, you have to register then in the registrar, in the tld zone. This is actually one of the two only records you have to do in your registrar panel with DNSSEC keys records. Later, once authoritative server are published, everythings happens on your server.

When recording NS on the registrar, you generally give two fields : the full hostname of the server, master.athome.tld here, and its addresses. In this case, we talk about "Glue Record". How to know master.athome.tld even though it's it who knows addresses for zone athome.tld? In this specific case, the address is written in the registrar.

To record authoritative servers on the registrar, you have to log on your registrar panel

You'll find a panel "GLUE" in your domain setup.

Once the new GLUE recorded, you can modify the name server list for your domain.

Read more about registrars and self-hosting with this article.

MX §

MX, a little like NS, tells where is a kind of service for the zone. In this case, where mail should be delivered. The record is build the same way as before :

@     IN MX 10   mail1
@     IN MX 20   mail2

For the zone athome.tld, the mails server (MX) is mail1.athome.tld.

The only significant differences is the "10", indicating the priority or "weight". When you have multiple servers, which one has priority to receive the mail? The one with the lowest weight.

If you self-host, you can choose to use the main domain name instead of a subdomain with the same address in the A record, it is still valid:

@     IN MX 10   athome.tld.

MX and NS can't be redirections (CNAME), they must point to A or AAAA records, or in last resort to IP.


TXT records are used to publish various data about your server. It is useful to share public keys as example. It looks like this :

@    IN TXT    "v=spf1 a mx ~all"

Ending words §

That's it for this long part about DNS. Do not hesitate to read again later, there are many things to understand here.

Of course, you can do so much more with DNS (SRV, SSHFP, ...), we'll describe eache case later if necessary.

Validating DNS resolver : unwind §

Unwind is available dy default on OpenBSD. It resolve domains (DNS) on your own device instead of asking your ISP or external provider. Results are kept in cache. This increase your server's performances and I strongly suggest to enable it 😎.

Notice unwind only work locally and can't do resolution for other devices. To provide such feature, look at unbound.

To use unwind, enable is as usual :

# rcctl enable unwind
# rcctl start unwind

Edit "/etc/resolv.conf" file so your server asks unwind to resolve domain names. This is not necessary if you use DHCP.


Here you go, your server now resolve domain names on its own.

You can try how well unwind works using dig command to see DNS requests results:

$ dig
;; Query time: 61 msec

61 ms were necessary to get an answer. Now try again :

$ dig
;; Query time: 0 msec

Yay, the address in in cache, speeding up future requests until TTL.

If you want to go further, read :

unwind(8) man

unwind.conf(5) man

unbound(8) man

Authoritative name server : nsd §

You absolutely can host the authoritative server for your own zone, one step further to independence 😏. That is exactly the purpose of nsd available in OpenBSD base.

Configuring nsd §

To configure nsd, edit "/var/nsd/etc/nsd.conf".

        hide-version: yes
        zonesdir: "/var/nsd/zones"
        ip-address: 2001:db8:1:1::2
        debug-mode: no
        verbosity: 1

# master zones
        name: "athome.tld"
        zonefile: "master/athome.tld"

We will follow this structure: zones are categorized wether the server is authoritative (master) or secondary (slave). Each zone is a file with the name of the corresponding domain. As example "/var/nsd/zones/master/athome.tld". Or course you can choose to do things differently if you prefer.

Do not forget to adjust the above example with your own IP.

Once setup, start nsd as usual :

rcctl enable nsd
rcctl start nsd

Remember to open (and redirect) port 53 (domain) UDP and TCP since it's used by nsd.

Once your nsd is up, you can set in the registrar your server's public ip in the list of authoritative servers for your zone.

An nsd example is available at the end of this documentation.

A few notes about DNS zone signing §

DNSSEC requires two kind of keys : ZSK (Zone Signing Key) light and short time living, and KSK (Key Signing Key) bigger with a long time usage.

Why such differences ?

It is recommended to renew KSK regularly -- about monthly, some do it weekly. However, it is quite annoying to update this monthly in the registrar.

That's why we created another kind of key to sign the firsts, but used only for this task. Those should be stronger, heavier since they are less used with a longer lifetime. Those will be recorded in the registrar.

Actually, we won't record KSK in the registrar but a checksum instead in the upper zone. It's the same idea with each zone publishing their NS in the upper zone.

Keep in mind changes won't propagate instantly. There is TTL, lifetime and validity of keys and signatures, so you can't expect to see things working right away.

Especially, you must publish in advance keys that will be used in the near future. That's why many administrators have 2 ZSK in the meantime: one currently used and another that will be used when the first has expired. That's what we'll do below. KSK are also published early, however rarely two in the same time.

Setup DNSSEC with ldnscripts §

Signing your DNS zone ensure its integrity.

You have to do it periodically since signature have an expiration date. You'll need a tool to sign and a bit of automation.

We will describe how to use ldnscripts designed for this task (thanks to 22decembre).

Note there are tools more complete/complicated such as OpenDNSSEC or KNOT.

With ldnscripts, you'll only need the package "ldns-utils" :

# pkg_add ldns-utils

Automation relay on tools already available on OpenBSD, I mean scripts /etc/weekly, /etc/monthly...

Let's follow ldnscripts' author's intructions :

$ cd /tmp
$ ftp
$ tar xvzf ldnscripts-master.tar.gz
$ cd ldnscripts-master* 
# make install

Configure ldnscripts in the file "/etc/ns/ldnscripts.conf". There is an example in the previous archive. Configuration should look like this :

# repository where to find unsigned zone file and specific conf

# serial : unixtime

# algorithm to use. They are listed : ldns-keygen -a list

# length of the zsk


# validity of signatures in days


# Verbose - set to 1 if needed

If you want, you can set a configuration file per domain: they should be named "/etc/ns/domain-name.tld.conf".

That's it for configuration 😊

To actually start using ldnscript, run with "init" parameter to create everything required: first keys, directories...

# ldnscript init athome.tld

You can read such output (below, VERBOSE is enabled):

This script will initialize your zone athome.tld with the general configuration or the
one set at /etc/ns/athome.tld.conf.
If you are not happy with it, modify your configuration (by copying the conf file to /etc/ns/athome.tld.conf and then editing it) and run this script again.
The script will create one KSK and two ZSK and finally sign the zone (which will triger a reload of your nsd server on the athome.tld zone).
The key Kathome.tld.+010+25115 has been generated with these arguments : -a RSASHA512 -b 1024 athome.tld
The key Kathome.tld.+010+34655 has been generated with these arguments : -a RSASHA512 -b 1024 athome.tld
The key Kathome.tld.+010+12321 has been generated with these arguments : -k -a RSASHA512 -b 2048 athome.tld
A new KSK key has been generated.
Make sure to update the DS record in your registrar quickly.
The key is Kathome.tld.+010+12321
DS content : 
athome.tld. IN      DS      12321 10 2 f6f91afd522680a3c459e1956e75f8eda078f99b8cf07114f0d299161bff0145
create a complete zone file for athome.tld with the current time as SOA
Signing zone
Zone is verified and complete

A directory "/var/ldnscripts/athome.tld/" is created, containing ZSK and KSK.

Signed zone file is in /var/nsd/zones/signed/athome.tld. Adjust nsd configuration to serve it :

	zonesdir: "/var/nsd/zones"

# master zones
    name: "athome.tld"
    zonefile: "signed/athome.tld"

Notice nsd is a static server, that's why ldnscript will reload it's configuration.

Warning, you must publish in your registrar public keys you can find in "/var/ldnscript/athome.tld/ksk" with ".key" extension. Depending on your registrar, you may have to publish DS records in ".ds" files (not at GANDI).

It's not over yet, we will set up a signing process with key renewal when necessary. Everything is ready, don't worry 😊.

ldnscript offers those actions :

Let's do this. First, edit "/etc/monthly.local" to add the rollover :

/usr/local/sbin/ldnscript rollover all

Then, make sure signatures are renewed before the end of VALIDITY parameter in configuration file. Above, we set 9 days so you can sign each week 2 day early. Edit "/etc/weekly.local" :

/usr/local/sbin/ldnscript sign all

Finally, every year, you will create a new KSK :

/usr/local/sbin/ldnscript ksk athome.tld

To remember, you can add a new crontab to send you a message every May the 2nd (someone's birthday 😜):

# crontab -e
0 0 2 5 * echo 'renew ksk' | mail -s "KSK ALERT" root

Remember to publish this new key in the registrar. The script rollover will delete the old key automatically. At this point, you may remove its record in the registrar.

That's it! Everything else is handled by ldnscript, it requires some efforts only the first time.

Notice in the example, you deal with multiples zones automatically.

Add or remove keys in the registrar §

When you renew KSKs, you must add in your registrar the public key. Make sure to do it early enough si it is correctly propagated before the old one is dismissed.

Ldnscript will display the number of the new key (keytag) and its checksum (DS) useful to record it in the registrar.

The public key is located at "/var/ldnscripts/zones/athome.tld/ksk/Kathome.tld*.key".

Once a key is dismissed, you can delete the previous DS record.

Add a secondary DNS server §

Sometimes called "slave", a secondary server (NS) can serve your zone in case the primary (master) is unreachable for reasons : network unreachable, nsd daemon crashed, meteor shower, frog invasion or worse : pancakes shortage.

You could ask a friend with a domain name server (not necessarily OpenBSD nor nsd either, even if it's what we describe here) to host your zone as a slave on your domain. You can even install a secondary server on your own -- on a VM rented at as example 😊. It's important that the secondary server is in another network, and even the same area : a few hundreds of kilometers are a good plan to mitigate power outage or network issues 😉.

Note you can absolutely set up secondary servers for your friend's domain, being mutually the slave of the other's domain name server.

We will describe both side of the picture. You could be administrator of master server, secondary server or both at the same time.

First, we prepare a little bit of authentication.

Yes, I know, it's hard work. But we want to do things right 😉? Here, we ensure our zone is updated by a legitimate server.

We will create a shared secret key, identical on both primary and secondary.

WIP \_o>

We use ldns-keygen command available in ldns-utils package you already have installed if you followed the previous part. It will create a keypair with a "secret" code inside :

$ cd /tmp
$ ldns-keygen -a hmac-sha256 -b 160 athome.tld

You have two new files, display the content of the private key :

$ cat Kathome.tld.+159+54791.private
Private-key-format: v1.2
Algorithm: 159 (HMAC_SHA256)
Key: H8D/Ka9RerEtmC0jN5hSQeATxNI=

Copy "Key" string in nsd configuration for master and slave servers in a "secret:" parameter. Remember to adjust with the proper algorithm:

        name: "transfer"
        algorithm: hmac-sha256
        secret: "H8D/Ka9RerEtmC0jN5hSQeATxNI="

The "name:" parameter -- "transfer" here -- doesn't matter. It's just a mark.

You can delete previously created files in /tmp now.

On the master server:

In the configuration of the domain name server, you add two lines to alert the slave server so it retrieve the zone. For this purpose, you add instructions "notify" and "provide-xfr" :

# master zone 
       name: "athome.tld"
       zonefile: "signed/athome.tld"  
       notify: transfer
       provide-xfr: transfer

        name: "transfer"
        algorithm: hmac-sha256
        secret: "H8D/Ka9RerEtmC0jN5hSQeATxNI="

Everytime you update the zone on your primary domain name server, it will alert the server on the address so the latter update the zone.

Many online services -- such as registrars -- offer to host your zone as slaves for free. In this case, it is not necessary to notify : servers get the zone on schedule. Keep the "provide" without authentication : "NOKEY" in place of the key name.

To see how to use secondary server of GANDI, follow this link.

You can find GANDI's slave server :

$ dig +short

Now add in your zone and in the registrar the secondary servers for your zone (NS) :

$ORIGIN athome.tld.
$TTL 86400

@           IN SOA    master.athome.tld. hostmaster.athome.tld. (
                        2014110502      ;
                        86400           ; refresh
                        7200            ; retry
                        3600000         ; expire
                        172800 )        ; negative

@               IN NS       master.athome.tld.
@               IN NS       secondairy.athome.tld.
@               IN NS       other.domain.tld.    ; a friend is
                                                 ; helping

maitre          IN A
maitre          IN AAAA     2001:db8:1:1::2

secondaire      IN A

On the secondary server now:

Just add a bit of configuration for nsd:

# slave zone 
       name: "athome.tld"
       zonefile: "slave/athome.tld"
       allow-notify: transfer
       request-xfr: transfer

        name: "transfer"
        algorithm: hmac-sha256
        secret: "H8D/Ka9RerEtmC0jN5hSQeATxNI="

Notice both transfer keys share exactly the same configuration on each server.

Zones files should stay untouched on the secondary server. They will be updated automatically when needed. Edit the nsd configuration if you need to change their location : zones will be fetched again.

You can check if zones are in sync with "dig":

$ dig -q @master.athome.tld athome.tld
$ dig -q @secondairy.athome.tld athome.tld

Make sure you added secondary servers in the name servers lists in your registrar.

Check your zone works as expected §

You may use the following links to check if your zone is correctly propagating over the world, has no errors and if DNSSEC is correct :

Check your zone

Zone check and various tests

Is it propagated ?

DNSSEC checks

Complete DNSSEC checks with diagrams

Complete and free example with § offer domain names ending with "".

We'll see how to set up a zone with this registrar.

First, check availables domains open for registration then choose one you like.

For the example, we'll use ""

Create the domain zone. Since we'll use "ldnscripts" later to enable DNSSEC, we write it in "/etc/ns/" :

@       IN SOA batman.athome.tld. (
                              2D )
@                             IN NS
@                             IN A
@                             IN AAAA   2001:db8:1:1::2
ns1                           IN A
ns1                           IN AAAA   2001:db8:1:1::2
ns2                           IN A

This is a pretty simple zone with two name servers, "ns1" and "ns2", the latter only available on IPV4.

We add a new section in nsd for "nsd1" :

# cat /var/nsd/etc/nsd.conf

    name: "transfert"
    algorithm: hmac-sha256
    secret: "Hsd/Ka9RerEtmC0jsd5d5eATxNI="

    name: ""
    zonefile: "signed/"
    provide-xca: transfert
    notify: transfert

Do the same on secondary server "ns2" :

# cat /var/nsd/etc/nsd.conf

  name: "transfert"
  algorithm: hmac-sha256
  secret: "Hsd/Ka9RerEtmC0jsd5d5eATxNI="

  name: ""
  zonefile: "slave/"
  allow-notify: transfert
  request-xca: transfert

reload nsd :

# rcctl reload nsd

Enable the zone with ldnscripts and prepare for DNSSEC :

# ldnscript init

That's ready. Now it is served, you can register the domain.

Create an annount on and connect.

Choose to create a New Domain.

Fill the fields with the full domain name you want to register and data about yourself.

example niceuorg 1

Then, fill "Name server" section. You must link the domain name and NS servers for the zone. The zone should already be managed by the servers. You can use IPV4 and IPV6 both. In other words, fill with the "NS" records in your zone.

example niceuorg 2

At last, after validating, you may read something like that if everything works as expected :

  ---- Servers and domain names check

  Accepted IP for NS1.ATHOME.TLD.CA.EU.ORG: 2001:db8:1:1::2
  Accepted IP for NS2.ATHOME.TLD.CA.EU.ORG:

  ---- Checking SOA records for

  SOA caom NS1.ATHOME.TLD.CA.EU.ORG at 2001:db8:1:1::2: serial
  2019100702 (21.005 ms)
  SOA caom NS1.ATHOME.TLD.CA.EU.ORG at serial 2019100702 (6.006 ms)
  SOA caom NS2.ATHOME.TLD.CA.EU.ORG at serial
  2019100702 (73.715 ms)

  ---- Checking NS records for

  NS caom NS1.ATHOME.TLD.CA.EU.ORG at 2001:db8:1:1::2: ok (20.674 ms)
  NS caom NS1.ATHOME.TLD.CA.EU.ORG at ok (5.953 ms)
  NS caom NS2.ATHOME.TLD.CA.EU.ORG at ok (65.559 ms)

  No error, storing for validation...
  Saved as request 20191007195509-arf-42318


Check your emails. You soon will get a configmation message about your registration.

Once it's done, you can enable DNSSEC in the dedicated panel.

Copy the DS record in field :

cat /var/ldnscript/

Now you can check dnssec is correctly enabled.

Virtualization §

It is realy easy to virtualize an operating system with OpenBSD thanks to vmd.

Well, that's nice, but what virtualizing means ?

Instead of installing different OS on different computers, you can install an OS inside your current OS. The virtualizer fake to be a full device and run an OS installed on a disk, which is in this case a simple file.

It is very handy since :

However, it require more resources.

Before going further, make sure we use the same vocabulary :

OpenBSD offer 3 tools for virtualization :

First of all, check if your hardware can virtualize :

$ dmesg | egrep '(VMX/EPT|SVM/RVI)'

If the result isn't empty, it's all good 😊.

Don't forget to upgrade firmwares if needed : "# fw_update".

To add a new virtual machine, you will always :

How to virtualize OpenBSD ? §

Actually, everything is already well documented on OpenBSD's website. This chapter will just focus on a few tips.

You should definitely read OpenBSD's FAQ on this topic 😉.

Use other installation media §

To use img disk (installXX.img or minirootXX.img) :

# vmctl start -c -m 1G -L -i 1 -d installXX.img -d /var/vm/obsdvm.qcow2 openbsdvm

Or if you don't want to download any file, you still can use the bsd.rd you probably already have :

# vmctl start -c -m 1G -L -i 1 -b /bsd.rd -d /var/vm/obsdvm.qcow2 openbsdvm 

However, such install requires to previously configure a network access for clients.

In /etc/pf.conf :

# using quad9 DNS
pass in quick proto { tcp udp } from to any port domain \
    rdr-to port domain
match out on egress from to any nat-to (egress)

Even better, you can use unwind if it is already configured on host, which is an excellent idea 😎 :

pass in proto { tcp udp } from to any port domain \
    rdr-to localhost port domain
match out on egress from to any nat-to (egress)

In "/etc/sysctl.conf" :


Virtualize debian §

Like most linux distro, you'll have to set the installer and bootloader to start the client with a serial console at speed 15200.

# download debian image
ftp ""
# create disk
vmctl create -s 50G /var/vm/debian.qcow2 
# start the VM
vmctl start -c -m 1G -L -i 1 -r debian*.iso -d /var/vm/debian.qcow2 debianvm

Choose install menu without validating ⚠.

Press TAB then edit the line to change vga and console parameters :

/install.amd/vmlinuz vga=off initrd=/install.amd/initrd.gz --- quiet console=ttyS0,115200n8 

Press Enter.

After installing and rebooting on the fresh debian install, edit "/etc/default/grub" so serial console is still used.

GRUB_CMDLINE_LINUX="console=tty1 console=ttyS0,115200"
GRUB_TERMINAL="console serial"
GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=0 --word=8 --parity=no --stop=1"

Then reload grub:

# update-grub

Virtualize Alpine Linux §

Alpine Linux is a very light distro.

Here again, you need to boot using serial console.

# vmctl create -s 50G /var/vm/linux.qcow2 
# vmctl start -c -m 1G -L -i 1 -r image.iso -d /var/vm/linux.qcow2 linux

When starting virual machine, press "TAB" to see available image (i.e. "lts" or "virt"). Add appropriate parameter to use serial console :

virt console=ttyS0,115200

Then press Enter and it's all good.

What is a VPN? §

VPN stands for "virtual private network". As the "private" suggests, the network created is "hidden" from the great Internet.

Most of the time, VPN are used to bypass ISP restrictions, secure a public WiFi access or hide from a government monitoring.

There are various VPN tools, each offering pros and cons.

Here, we'll discuss Wireguard, OpenIKED and a bit of SSH. The two lasts are already very well documented in OpenBSD documentation. They all are available in base install.

In the following pages, we'll use as designations :

We'll configure "roadwarriors".

roadwarrior... what does that mean ?

Roadwarrior describe a setup allowing a client from any origin to reach the Internet through the VPN. Thus, the VPN servers appears to be the actual identity of the client, hiding it in the process.

A bit further, we'll describe how to set up your server so you get a fixed IP provided by the VPN. At this point, no more roadwarrior is mentioned and VPN configuration is simpler, but we'll discuss firewall configuration to redirect requests. It becomes useful if your ISP doesn't provide static IP or if your server is travelling for some reasons.

Wireguard §

Wireguard is probably the easiest VPN to set up while one of the most secure since it requires only recent encryption methods. For clients, it is handy since it stay up even if the device's IP changes -- switching from wireless to wired as example. IPv4 and IPv6 are supported so you can get an IPV6 if your ISP doesn't offer one. Wireguard is supported natively since OpenBSD 6.8.

Wireguard official website

Many software for various platforms are availables (android, MacOS...) so it's even more handy.

It might be the best choice if you want to self-host a VPN.

If you want to learn how Wireguard works, look at the whitepaper.

Setting up an exit point ("roadwarrior") §

Below we describe the following structure:

Understand that route's changes are for OpenBSD's clients and won't be a question with other OS clients.

In this example, IPs inside the VPN are in subnet :

Here's how it will look like :

    |   server    | wg0: port 4545
    |             |---------------+
    +-------------+               |
           | Public IP            |
           |            |
           |                      |
           |                      |
    /##########\                |WireGuard
    |  Internet  |                |VPN
    \############/                |
           |                      |
           |                      |
           |rdomain 1             |
    +-------------+               |
    |   client    |---------------+
    +-------------+ wg0:
                    rdomain 0 (default)

By default, traffic goes through unless you explicitly ask to use another route (ie : "route -T1 exec ping")

VPN is set up by creating wgN interfaces, where "N" is a number from 0 to 9 as example. Such interface is created by filling a file "/etc/hostname.wgN".

Server will listen on port 4545 UDP. Any other port can be used, just check it isn't already reserved in "/etc/services".

Create keys for Wireguard §

Use the following to create a key :

openssl rand -base64 32

It returns as example : "uA1ggvRv66QslZ0ygorWvcLVTxzFauffqMigXTrmLVY="

Once an interface receive a private key, you can retrieve the associated public key with ifconfig :

# ifconfig wgN

Create a key on the server:

# openssl rand -base64 32

Create wg0 interface with the previous private key :

# ifconfig wg0 create wgkey r8uSGD6vyycE5n5/atU9/NX9JQPo4SJryNGpjbQG+rA= wgport 4545

Now get the matching public key :

# ifconfig wg0
wg0: flags=8082<BROADCAST,NOARP,MULTICAST> mtu 1420
        index 5 priority 0 llprio 3
        wgport 4545
        wgpubkey x9VXlh4AMa2YRjTMRVE39pQRsFHRJHUYrATL6vkqFmU=
        groups: wg

The line starting with "wgpubkey" indicates the interface's public key. You'll need it for clients so they can authentify the server, so write it down.

Of course, take note of server's private key: we'll write it later in a file to automate interface creation.

Now on a client, we do pretty much the same: create a private key and set up a wg interface without specifying a port:

# openssl rand -base64 32
# ifconfig wg0 create wgkey q/7uIx6wBIRUIdxOi5D6OWEQRVUt2AXhMj7j29W/s3s=
# ifconfig wg0 |grep wgpubkey
    wgpubkey V3pCAhxnRl0QEL8luB9D4EvTVxGT7QGDDCZ3O26kY3A=

Once again, write down keys.

Redirecting traffic through server §

Here, we want the server to be a sort of relay between the client and the rest of the world.

You have to redirect IP forwarding on server :

# sysctl net.inet.ip.forwarding=1

Add the following line in "/etc/sysctl.conf" so changes are applied at reboot:


For IPv6 :


Then, add a nat rule in "/etc/pf.conf" :

# open 4545 UDP
pass in proto udp to port 4545
# open wg0 interface for incoming
pass in on wg0
# What's from VPN (wg0) is NATted to public servers iface
pass out quick on egress from (wg0:network) to any nat-to  (egress)

Reload pf 😉

Dig the Wireguard tunnel §

Now we've got all the material to authenticate clients and server, we can dig up the VPN. To do so, we'll indicate public keys on each devices and which IP are allowed to use the tunnel.

To make things easier, we'll edit "/etc/hostname.wg0" files now. When rebooting, our configuration will remain.

Pay attention to keys, they are the same we got above. 😉

On the server :

"/etc/hostname.wg0" :

wgkey r8uSGD6vyycE5n5/atU9/NX9JQPo4SJryNGpjbQG+rA=
wgport 4545
wgpeer V3pCAhxnRl0QEL8luB9D4EvTVxGT7QGDDCZ3O26kY3A= wgaip

You can add as many "wgpeer" line as you want. However, each client must have its own IP :

wgpeer V3pCAhxnRl0QEL8luB9D4EvTVxGT7QGDDCZ3O26kY3A= wgaip
wgpeer m7K/gfmMPYRJx1IOP01zYrNbEuMnnZ29xN4OBgRoRXo= wgaip
wgpeer qnuq5MgezCDHXsYYGmrcegPCNcJvz9EOIG3XyHp1DBk= wgaip

On client :

Here you must specify where the client can find the server ("wgendpoint") and set default routes so traffic goes through the tunnel.

wgkey q/7uIx6wBIRUIdxOi5D6OWEQRVUt2AXhMj7j29W/s3s=
wgpeer x9VXlh4AMa2YRjTMRVE39pQRsFHRJHUYrATL6vkqFmU= wgendpoint athome.tld 4545 wgaip
wgrtable 1
!route add -net default

Dig the tunnel on client and server with :

# sh /etc/netstart wg0

Edit client's interface used to reach the internet so it uses routing table n°1. As example in "/etc/hostname.em0":

rdomain 1

You can now check client's IP : it's the same as the server's.

Configure clients using other OSes §

Wiregard supported OS

F-droid (android)

Below is the minimal requirements to set up a client :

Interface :

Peer :

Ressources §

This page is heavily inspired from :

Thank you very much to Solène Rapenne who had the brilliant idea to use "rdomain".

VPN with OpenIKED §

OpenIKED is a free implementation of IKEv2 protocol.

As it is already very well documented, and istead of duplicating good documentation, I suggest you to read OpenBSD's official documentation about IKEv2:

See FAQ 17

Get a static IP thanks to a VPN §

If your ISP don't offer static IP, then you could set up a VPN to use endpoint's static IP.

It also gets handy if :

Here is a scheme of what we're going to achieve :

+--~sniper~-+        +-------~tank~--------+      +--------------+
|           |        |                     |      |              |
| Your own  +<------>+   Endpoint Server   +<-----+   Visitor    |
|  Server   |  VPN   |   with static IP    |      |              |
|           |        |                     |      |              |
+-----------+        +---------------------+      +--------------+
  hidden IP               Public IP

To reach your server, one will use Endpoint Public IP.

To avoid confusion, I'll refer your server with "sniper" and the VPN endpoint with "tank".

Warning for pf §

⚠ Remember to adjust your pf.conf. Now, incoming traffic is on VPN interface, i.e. "wg0" if you use Wireguard or "enc0" with IKED.

The easiest is maybe to replace "egress" by a macro with a list of all interfaces you want to handle. As example :

pass in quick on egress proto tcp to port $tcp_pass


ifaces = "{ em0 wg0 }"
# ifaces = "{ egress wg0 }" # alternative
pass in quick on $ifaces proto tcp to port $tcp_pass

Prerequisite : simple VPN tunnel §

Tank don't have to do nat (yet), and you don't need to change sniper's default routes.

For a Wireguard tunnel, here is a short summary of what you may have configured :

On tank :

# cat /etc/hostname.wg0
wgkey [..snip..]
wgport 4545
wgpeer [...snip...] wgaip

# cat /etc/sysctl.conf

# cat /etc/pf.conf
# wireguard tunnel
pass in proto udp to port 4545
# no nat-to

On sniper :

# cat /etc/hostname.wg0
wgkey [..snip..]
wgpeer [..snip..] wgendpoint chezmoi.tld 4545 wgaip

# cat /etc/pf.conf
# paranoid commented below
#  pass in on wg0 from
#  pass out on wg0
# less paranoid:
pass on wg0

Setup a new IP §

From now on, we suppose there is a tunnel between sniper and tank.

Now edit pf.conf on tank to add "nat-to" and "rdr-to" rules :

serv_int = ""
serv_ext = ""
int_if = "wg0"
ext_if = "egress" # change-me maybe

set skip on lo


# let vpn on
pass in proto udp to port 4545
pass in on wg0

pass in on $ext_if proto tcp from any to $serv_ext \
        rdr-to $serv_int

match out on $ext_if from $serv_int to any \
       nat-to $serv_ext static-port
# One can replace the two previous rules with :
# match on $ext_if from $serv_int to any binat-to $serv_ext

match out on $int_if from any to $serv_int \
        received-on $ext_if nat-to $int_if

pass out

Make sure to edit "serv_*" and maybe "$ext_if", the public interface.

The "rdr-to" rule link what's coming from anywhere ("any") on the public interface "$ext_if" to sniper ("$serv_int").

Then, the "nat-to" does the same the other way. We use a "match" so the state remains until "pass out".

It is equivalent to a "binat-to" rule, but it won't let you specify ports numbers (see later).

In the end, another "nat-to" is necessary since "$ext_if" and "$int_if" are different.

Reload pf and you're done 😊.

Some may want to fine tune their firewall and redirect only a few port numbers :

ports_tcp = "{ ssh www https smtp submission imaps domain }"
ports_udp = "{ domain spamd-sync }"

# pass in on $ext_if from any to $serv_ext rdr-to $serv_int
# previous line is replaced by
pass in on $ext_if proto tcp from any to $serv_ext port $ports_tcp rdr-to $serv_int
pass in on $ext_if proto udp from any to $serv_ext port $ports_udp rdr-to $serv_int

Warning about SSH and host §

If you access your server with SSH, understand the above rules will redirect you to you client automatically. You have to set up an alternative SSH port on the VPN endpoint and configure pf.conf to avoir redirecting yourself throught the VPN when you want to administer the host.

Edit "/etc/ssh/sshd_config" and set port to i.e. 2222 and add in "/etc/pf.conf" BEFORE VPN redirection. Something like :

pass in quick on egress proto tcp port 2222 # alternative ssh port

Edit DNS §

Now, in your DNS zone, your domain name should point to tank's IP.

Get an IPv6 thanks to a VPN §

Despite the year we live on, some ISP still don't provide IPv6 connectivity. You still can get an IPv6 thanks to a VPN if the exit point is IPv6 ready. As example, there are or vultr amongst others.

We will set up a wireguard tunnel as before and add IPv6 conntectivity to the client.

Prerequisite to get an IPv6 §

This website can help to generate a private IPv6 range

On the remote server §

Remember to enable ip forwarding in "/etc/sysctl.conf"


In file "/etc/hostname.wg0" to configure wireguard interface, you must specify IPv6 of VPN exit point. Here, it's "fd9c:f774:0bfa:acfc::1/64".

Each client should be able to get its own IPv6. We add a new option "wgaip" after the previous one. The configuration will look like this :

# cat /etc/hostname.wg0
inet6 fd9c:f774:0bfa:acfc::1/64
wgkey [...snip...]
wgport 4545
# peer 1
wgpeer [...snip...] wgaip wgaip fd9c:f774:0bfa:acfc::2/128
# peer 2
wgpeer [...snip...] wgaip wgaip fd9c:f774:0bfa:acfc::3/128
# peer 3
wgpeer [...snip...] wgaip wgaip fd9c:f774:0bfa:acfc::4/128


⚠ Each client's IPv6 MUSt end with "/128".

On the client wanting an IPv6 §

In "/etc/hostname.wg0", you have to add a few things:

Now, Wireguard interface on the client look like this :

# cat /etc/hostname.wg0
wgkey [...snip...]
wgpeer [...snip...] \
	wgendpoint <XX.XX.XX.XX> 4545 \
	wgaip \ # <--- !
	wgaip ::0/0 \
	wgpka 25

inet6 fd9c:f774:0bfa:acfc::3/64 # <--- !
wgrtable 1
!route add -inet default
!route add -inet6 default fd9c:f774:0bfa:acfc::1 # <--- !

Here you go, now you have an IPv6 on the Internet.

You can see it with the following command:

curl -6

Various §

Gemini (vger) §

Gemini is a new internet protocol which is heavier than gopher, is lighter than the web, will not replace either, strives for maximum power to weight ratio, takes user privacy very seriously.

It is quite an amazing protocol to post your writings and focus first on content.

Actually, this current documentation is also available with gemini. 😁

There a various servers, but I'd like to give a few advices to install vger, a gemini server designed for OpenBSD involving some of its mitigation mecanism (unveil, pledge...)

In order to keep vger as simple as possible, Solène -- vger's developer -- had the brilliant idea to use tools already in OpenBSD base install :

Install vger as a package :

# pkg_add vger

If you read the README, you can learn how to set up a new capsule (a gemini website).

Edit "/etc/inetd.conf" to set how vger will be run, with the required flags. stream tcp nowait _vger /usr/local/bin/vger vger

By default, vger look for requested files in "/var/gemini".

You can add flags according to the manual. As example, if you want to serve multiple capsules, each one stored in a directory named after the domain name requested in "/var/gemini" (/var/gemini/athome.tld, /var/gemini/other.tld,...) with -v, enable auto index with -i :

# serve files in /var/gemini/domain
localhost:11965 stream tcp6 nowait _vger /usr/local/bin/vger vger -v -d /var/gemini/ -i
localhost:11965 stream tcp nowait _vger /usr/local/bin/vger vger -v -d /var/gemini/ -i

Pay attention to the lines above. inetd listens on localhost on port 11965 and send the incoming request to vger run as user _vger to avoid privilege escalation. A second line with "tcp6" is added to serve on the IpV6. However, you should have filled "/etc/hosts" accordingly so localhost resolve to local ipv6 :   localhost
::1         localhost

Then, you can add a new part to relayd in "/etc/relayd.conf":

ext_ip4 = ""
ext_ip6 = "2001:db8::2"
log connection

tcp protocol "gemini" {
        tls keypair chezmoi.tld

relay "gemini" {
        listen on $ext_ip4 port 1965 tls
        protocol "gemini"
        forward to localhost port 11965
relay "gemini6" {
        listen on $ext_ip6 port 1965 tls
        protocol "gemini"
        forward to localhost port 11965

A few words :

This is what happens when someone reach your capsule:

              1965       11965
Visitor ---> Relayd ---> inetd ---> vger

Finally, enable and reload daemons :

# rcctl enable inetd relayd
# rcctl start inetd relayd

Don't forget to open 1965/TCP in "/etc/pf.conf"

To go further :

Gemini official website

vger security analysis

vger source code

Gopher (geomyidae) §

Gopher protocol is the precursor of widely used http. However, some still use it to transfer files and serve mostly text content.

You'll have to open 70 port.

Put the files you want to serve in "/var/gopher", they will be available on "gopher://athome.tld".

geomyidae server is written in C by one of suckless developpers.

To install it :

# pkg_add geomyidae
# rcctl enable geomyidae
# rcctl start geomyidae

That's it, now fill "/var/gopher" 😊.

However, I strongly recomment to read geomyidae manpage to edit default flags. As example, you may want something like this :

# rcctl set geomyidae flags -e -h athome.tld -b /var/gopher/athome.tld

Logs are in "/var/log/geomyidae.log".

mlmmj : Mailing List §

While there are hundreds of social networks, with their own policies and ads, instant messenging apps, forums and so on, remember mailing lists are a thing. Mailing lists are the future because :

Here, we'll talk about mlmmj since it perform well on OpenBSD, is easy and secured.

mlmmj website

Install mlmmj §

# pkg_add mlmmj

DNS records for the list §

Make sure a MX record is registered for the domain you'll use for your list.

Create a new mailing list §

Use command "mlmmj-make-ml" and follow instructions.

Below is an example to create list "pizza" on the domain "list.athome.tld", so "pizza@list.athome.tld".

# mlmmj-make-ml
Creating Directorys below /var/spool/mlmmj. Use '-s spooldir' to change
What should the name of the Mailinglist be? [mlmmj-test] : pizza
The Domain for the List? [] : athome.tld
The emailaddress of the list owner? [postmaster] : batman@athome.tld

For the list texts you can choose between the following languages or
give a absolute path to a directory containing the texts.

Available languages:
ast   cs    de    en    fi    fr    gr    it    pt    sk    zh-cn
The path to texts for the list? [en] :

Don't forget to add this to /etc/aliases:
pizza:  "|/usr/local/bin/mlmmj-receive -L /var/spool/mlmmj/pizza/"

If you're not starting mlmmj-maintd in daemon mode,
don't forget to add this to your crontab:
0 */2 * * * "/usr/local/bin/mlmmj-maintd -F -L /var/spool/mlmmj/pizza/"

1) The mailinglist directory have to be owned by the user running the
mailserver (i.e. starting the binaries to work the list)
2) Run newaliases

Make sure permissions are correct:

# chown -R _smtpd:_smtpd /var/spool/mlmmj/pizza

Edit root's crontab ("# crontab -e") to add the line given by mlmmj-make-ml :

0 */2 * * *  /usr/bin/mlmmj-maintd -F -L /var/spool/mlmmj/pizza

Make smtpd ready for mlmmj §

Edit "/etc/mail/smtpd.conf" so it handles mailing list messages correctly :

table aliases "/etc/mail/aliases"
action local_mail maildir alias <aliases>
match from any for domain "list.athome.tld" action local_mail

It is important that action ("local_mail" here) handle aliases.

That's why you must edit "/etc/mail/aliases" to pipe incoming messages to mlmmj for the mailing list:

pizza:"|/usr/local/bin/mlmmj-receive -L /var/spool/mlmmj/pizza/"

End with "# newaliases" command or restart smtpd.

Customize a mailing list §

You can customize a list by editing the files in "/var/spool/mlmmj/pizza/control".

If files don't exist, just create them.

You can filter incoming messages depending on the sender, force plaintext, use custom text in templates, modify headers to keep users privacy and much more.

Look at the official documentation to learn more

Monitoring §

Below are a few suggestions to keep an eye on your server's charge.

systat §

cpu0.temp0          51.00 degC
acpitz0.temp0       26.80 degC      zone temperature

Type "q" to quit.

vmstat §

Quick look at system load :

$ vmstat
 procs    memory       page                    disks    traps          cpu
 r   s   avm     fre  flt  re  pi  po  fr  sr sd0 sd1  int   sys   cs us sy id
 1 274 1500M   1253M  657   0   0   0   0   0   1   4  190 15482 1826  2  1 97

Others §

Look at symon/symux/syweb if you wan real-time graphs. It requires PHP.

Grafana draw nice graphs too.

Look at monit to get alerts when the load gets over a threshold.

SmokePing is a deluxe latency measurement tool.

Seedbox §

With rtorrent §

rtorrent is a light and efficient torrent client.

It's text-based interface is nice if you don't want to bother with a webapp and remote control your seedbox with SSH.

# pkg_add rtorrent

Add a dedicated user _rtorrent for privileges separation.

Now log in as _rtorrent :

# su _rtorrent

Create required directories :

$ mkdir -p seedbox/{download,session,torrents}

Now create ~/.rtorrent.rc from the example :

$ cp /usr/local/share/examples/rtorrent/rtorrent.rc ~/.rtorrent.rc

Edit that file.

# Global upload and download rate in KiB. "0" for unlimited.
download_rate = 0
upload_rate = 20

directory = ~/seedbox/download 
session = ~/seedbox/session

# When a torrent file is copied in torrents dir, it's added to rtorrent
schedule = watch_directory,5,5,load_start=~/seedbox/torrents/*.torrent
schedule = untied_directory,5,5,stop_untied=~/seedbox/torrents/*.torrent

check_hash = yes

use_udp_trackers = yes

encryption = allow_incoming,try_outgoing,enable_retry

dht = auto

peer_exchange = yes

# Run script to get alerts when download finish
system.method.set_key =,notify_me,"execute=~/,$d.get_name="

# add dht node so magnets works fine
schedule2 = dht_node_1, 5, 0, ""
schedule2 = dht_node_2, 5, 0, ""
schedule2 = dht_node_3, 5, 0, ""
schedule2 = dht_node_4, 5, 0, ""

Fill the script "~/" to get alerts when a download is complete.

echo "$(date) : $1 - Download completed." | mail -s "[rtorrent] - Download completed : $1" root

To add a new torrent file, you may use scp :

$ scp *.torrent _rtorrent@chezmoi.tld:/home/_rtorrent/seedbox/torrents/

To have rtorrent automatically started at boot, edit _rtorrent user's crontab and add :

@reboot /usr/bin/tmux new -s rtorrent -d /usr/local/bin/rtorrent

We use tmux to put rtorrent in the background.

If you need to display rtorrent, log in as _rtorrent with ssh and run "tmux a -t rtorrent". Press ctrl-b then "d" to detach.

To add a magnet link, press "backspace" and copy the link.

When in doubt :

$ rtorrent -h

Transmission §

Transmission works very well and offer a web interface.

# pkg_add transmission

We start and stop daemon so we can create then edit configuration file.

# rcctl enable transmission_daemon
# rcctl start transmission_daemon
# rcctl stop transmission_daemon

Creates directories to download files and store .torrent.

# mkdir -p /var/transmission/{downloads,incomplete,torrents}
# chown -R _transmission:_transmission /var/transmission

If others can see the above directories :

# chmod a+rX /var/transmission

Now edit this file to configure transmission :


You may set :

"download-dir": "/var/transmission/downloads",
"encryption": 2,
"incomplete-dir": "/var/transmission/incomplete",
"incomplete-dir-enabled": true,
"peer-port-random-on-start": true,

I suggest to add the following lines to automatically start downloading torents you copied (with SFTP as example) in "/var/transmission/torrents".

"watch-dir": "/var/transmission/torrents",
"watch-dir-enabled": true

To get an alert when a download is complete :

"script-torrent-done-enabled": true,
"script-torrent-done-filename": "/var/transmission/",

"" script looks like this :

echo "$(date) : $TR_TORRENT_NAME - Download completed." | mail -s "[transmission] - Download completed : $TR_TORRENT_NAME"

Remember it must be executable :

# chmod +x /var/transmission/

When you're done configuring, restart transmission :

# rcctl start transmission_daemon

The easiest to display the web interface is to use an SSH tunnel. From your computer, dig a tunnel to the server :

ssh -N -L 9999: batman@athome.tld

Now open a browser at "http://localhost:9999".

Syncthing §

Syncthing is an amazing tool to keep your data on multiple devices.

It is supported on all platforms.

Data is encrypted by default.

It is really well done 😋.

Install and configure §

# pkg_add syncthing

Let it run as a daemon:

# rcctl enable syncthing
# rcctl start syncthing

By default, Syncthing configuration is stored in "/var/syncthing".

You may edit those files to add new shares and configure various options, but it is quite complex.

Instead, I suggest to open the UI through a SSH tunnel 😊.

From your computer, run:

ssh -N -L 9999: batman@athome.tld

Then open a browser to "http://localhost:9999".

Now look at official docs.

You will learn what to do if you encounter issues or to run synthing inside a SSH tunnel between clients only.

TOR : relay and hidden services §

Tor is software helping protect privacy on the Internet.

It relies on multiple layers onion-like router, that's why this project needs volunteers to run relay nodes.

Configure a tor relay §

Tor may need to open lots of connexions, so you'll need to edit "/etc/sysctl.conf" to increase limits:


Install and enable tor:

# pkg_add tor
# rcctl enable tor

Then, make sure you open port 9001.

Then, edit "/etc/tor/torrc", with the following lines :

ORPort 9001
Nickname nick
RelayBandwidthRate 75 KB  
RelayBandwidthBurst 100 KB 
ContactInfo yourname <adress AT email dot tld>
ExitPolicy reject *:* # no exits allowed

Adjust values according to your needs and how much bandwidth you want to allocate.

Finally, restart tor and look at such messages in "var/log/messages" :

May 12 12:20:41 athome Tor[12059]: Bootstrapped 80%: Connecting to the Tor network
May 12 12:20:41 athome Tor[12059]: Bootstrapped 85%: Finishing handshake with first hop
May 12 12:20:42 athome Tor[12059]: Bootstrapped 90%: Establishing a Tor circuit
May 12 12:20:44 athome Tor[12059]: Tor has successfully opened a circuit. Looks like client functionality is working.
May 12 12:20:44 athome Tor[12059]: Bootstrapped 100%: Done
May 12 12:20:44 athome Tor[12059]: Now checking whether ORPort is reachable... (this may
take up to 20 minutes -- look for log messages indicating success)
May 12 12:21:10 athome Tor[12059]: Self-testing indicates your ORPort is reachab

Configure a hidden service §

Hidden services are availables with ".onion" URL.

⚠ Understand it is strongly discouraged to run a relay AND a hidden service.

It can be very handy, especially when you notice that the onion URL will remain unchanged even if your IP is modified for reasons in the future.

Enable a hidden service in "/etc/tor/torrc" :

HiddenServiceDir /var/tor/hidden/
HiddenServicePort 80 localhost:80

Reload Tor with "rcctl restart tor". Two new files are in "/var/tor/hidden" : "hostname" and "private_key". Find your onion URL in hostname file :

# cat /var/tor/hidden/hostname

KEEP private_key safe and secret.

In the above example, we provide a website (port 80 on localhost). You can add a new section in httpd.conf to serve this URL :

server "5rud2tr7sm3oskw5.onion" {
        listen on localhost port 80
        # emplacement du site
        root "/htdocs/athome.tld"     
        directory index index.html



Prosody is a light and easy to set up XMPP server

Here are a few notes to install prosody on OpenBSD.

Install prosody §

# pkg_add prosody

Add DNS fields for XMPP §

A record :


SRV records :

_xmpp-client._tcp.athome.tld. 18000 IN SRV 0 5 5222 xmpp.athome.tld.
_xmpp-server._tcp.athome.tld. 18000 IN SRV 0 5 5269 xmpp.athome.tld.

If you host MUCs :

_xmpp-server._tcp.conference.athome.tld. 18000 IN SRV 0 5 5269 xmpp.athome.tld.

Prosody's Configuration §

Edit "/etc/prosody/prosody.cfg.lua"

VirtualHost "athome.tld"
	ssl = {
		certificate = "/etc/prosody/certs/athome.tld.crt";
        key = "/etc/prosody/certs/athome.tld.key";

Certificate must be readable by _prosody user. If you got them with acme-client, then you need to install them in prosody directory :

install -g _prosody -o _prosody -m 400 /etc/ssl/private/athome.tld.key /etc/prosody/certs/
install -g _prosody -o _prosody -m 400 /etc/ssl/athome.tld.crt /etc/prosody/certs/

Add the previous commands to your periodic task (cron? weekly.local?) when certificates are renewed.

Add the admin :

# prosodyctl adduser batman@athome.tld

Check it's allright :

prosodyctl check config

Prosody's Ports §

Open 5222 (xmpp-client) and 5269 (xmpp-server).

Prosody's logs §

Edit "/etc/newsyslog.conf" :

/var/prosody/prosody.log                644  5     300  *     Z
/var/prosody/prosody.err                644  5     300  *     Z

mod_http_file_share §

If you enable http_file_share, make sure you open 5280 and 5281 ports.

Also, add the domain for file sharing in tls certificate.

More on Prosody §

Look official instructions 😉

Tips, FAQ, comments... §

About apu2 §

apu2 is a great board that require few power and well supported by OpenBSD.

However, you can't plug it to a screen but have to use the serial port to install and do admin task. Actually, it may be a good thing if you already have a laptop and don't want to buy an extra screen and keyboard.

Here, we tallk about apu2 tips.

For the record, this documentation is hosted on an apu2d0.

Where and what can I buy an apu2? §

Look at the official list.

Make sure you buy at least the following components if you don't use a pack :

Assemble the apu2 §

Look at the page "apu cooling assembly"

Get to the apu2 console §

Plus the usb dongle or directly the serial cable to a computer. To get to the serial console, you can run on OpenBSD :

cu -s 115200 -l /dev/cuaU0

You should run the above as root or add your username to "dialer" group before:

doas usermod -G dialer $(whoami)

Install OpenBSD on APU2 §

Just proceed as usual with an USB key as installation media. In the console, before booting, add the following options so the installer knows we're in serial console :

stty com0 115200
set tty com0

No need to do if afterwards, the installer save this option.

Upgrade apu2 bios §

You can flash the firmware with flashrom tool :

# pkg_add flashrom

Now read "/usr/local/share/doc/pkg-readmes/flashrom" for up-to-date instructions.

Check file checksum §

When you transfer files, you may ensure the authenticity of data.

To do so, computing a file checksum is useful. Use the "sha256" command :

$ sha256 debian-9.6.0-amd64.qcow2.gz
SHA256 (debian-9.6.0-amd64.qcow2.gz) = 91831ba15446f3ab418ae8a5c2a8ac0d852dc5d43bd595a70b88bd6ea4ded397

Do the same on both ends : the output must be the same.

Notice that on GNU/Linux, "sha256" don't exist. Its equivalent is "sha256sum".

If you are paranoid, you can go further and check signatures with "signify" tool.

How to change password §

Use "passwd" command :

# passwd jdoe

To change superuser password, you have to make sure to be correctly logged with appropriate environment variables. To do so, use "-l" flag:

# su -l
# passwd root

Periodic tasks (cron) §

Everything is ready on OpenBSD to run tasks periodically. You can edit the following scripts :

Make sure you use the full path of commands you need to run as PATH variable can be different than your user's. As example :

echo "You are so handsome" | mail -s "Hi" root # Not OK
echo "You are so handsome" | /usr/bin/mail -s "Hi" root # GOOD

Use "which command" to find absolute paths.

If you need more accurate timing, use cron.

Enter "crontab -e" and edit the file to specify when the user will run automatically a command. As example, to run it hourly (actually, every time the clock display "0 minutes"):

0 * * * * /path/to/command

With the example below, the command is run every 5 minutes :

*/5 * * * * /path/to/command

To learn more, read man crontab(5) manpage.

Look at this website to help you writing crontasks.

crontabs are very handy 😎.

Disk Partitioning §

If you need to set up another disk, we describe here how to prepare it.

⚠ Notice all this can be achieved after disk encryption and havig a RAID identified as "sd1" later.

Identifiy the disk §

Plug the disk and run "dmesg". You'll see something like that :

umass0 at uhub0 port 1 configuration 1 interface 0 "Western Digital Ext HDD 1021" rev 2.00/20.21 addr 2
umass0: using SCSI over Bulk-Only
scsibus2 at umass0: 2 targets, initiator 0
sd1 at scsibus2 targ 1 lun 0: <WD, Ext HDD 1021, 2021> SCSI2 0/direct fixed serial.10581021383235373034
sd1: 1907727MB, 512 bytes/sector, 3907024896 sectors

Here, the disk is identified as "sd1". Adjust according to your case later.

Partitioning §

Actually, we create slices with disklabel :

# disklabel -E sd1

A prompt show up. Now we can create slices.

When in doubt, enter "p" to display the current state.

To create the first slice, enter "a a". It meas "Add a slice a".

Let default offset. Set the size according to your needs. To do so, you may use :

Choose filesystem "4.2BSD".

Add more slices if you want with "a d", "a e", "a f"...

⚠ Notice you can't create a "c" slice since it's reserved to identify the whole disk. Also, "b" is often used for swap, but it's not mandatory.

Once done, enter "q" to exit and apply changes.

Now enter "disklabel sd1" to see disk state :

# /dev/rsd1c:
type: SCSI
disk: SCSI disk
label: Ext HDD 1021
duid: 782f1ddb783cdd13
bytes/sector: 512
sectors/track: 63
tracks/cylinder: 255
sectors/cylinder: 16065
cylinders: 243201
total sectors: 3907024896
boundstart: 64
boundend: 3907024065
drivedata: 0

16 partitions:
#            size       offset  fstype [fsize bsize  cpg]
  a:    629153472           64  4.2BSD   4096 32768    1
  d:   3277870464    629153536  4.2BSD   8192 65536    1 
  c:   3907024896            0  unused

Notice the "duid" :

duid: 782f1ddb783cdd13

You can use it later to identify the disk and avoid confusions if there are several. You can find it also with this command :

# sysctl hw.disknames

You're almost done, you still have to create the filesystem for each slices :

# newfs /dev/rsd1a
# newfs /dev/rsd1d

Don't forget the "r" 😉

Now you can edit "/etc/fstab" to mount the previous slices easyly. Notice the use of the previous duid :

782f1ddb783cdd13.a /home/prx/music/ ffs rw,softdep,noatime,nodev,nosuid,noauto 0 0
782f1ddb783cdd13.d /mnt/backup/ ffs rw,softdep,noatime,nodev,nosuid 1 2

That's a lot of options, you'll learn more in man fstab(5).

In this example :

To mount every mountpoint listed in "/etc/fstab", enter :

# mount -a

How to find my network interface ? §

Use "ifconfig" command.

Your interface might have an "inet" (IP) and might belong to "egress" group.

As example

$ ifconfig                                                   
lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 32768
        priority: 0
        groups: lo
        inet netmask 0xff000000
        lladdr fc:aa:14:65:5f:86
        priority: 0
        groups: egress
        media: Ethernet autoselect (100baseTX full-duplex,rxpause,txpause)
        status: active
        inet netmask 0xffffff00 broadcast
enc0: flags=0<>
        priority: 0
        groups: enc
        status: active
pflog0: flags=141<UP,RUNNING,PROMISC> mtu 33144
        priority: 0
        groups: pflog

Here, "lo0" is not the interface we're looking for: it's local interface used by some program. "enc0" and "pflog0" are not our interface, they are respectively the encrypted interface used by IKED and pf logging inteface.

It remains "re0", the ethernet as written in "media" section.

Logs §

Your server keep records of its activity. It is often stored in text files called "logs". You can find most of them in "/var/log" and "/var/www/logs".

You'll consider amongst others :

There are more.

I advice to read them from time to time to check everything works as expected. You can see them in realtime with the following command :

tail -f /var/log/messages /var/log/daemon /var/log/maillogs /var/log/authlog

Soon, you'll notice new files ending with ".0.gz", ".1.gz"... Those are archived logs. To avoid endlessly growing logs, OpenBSD run periodically "newsyslog" command and compress archives if necessary.

You can configure newsyslog if you edit "/etc/newsyslog.conf". You may add new lines if an application create logs not listed :

/var/log/messages                       644  5     300  *     Z
/var/log/authlog        root:wheel      640  7     *    168   Z

As usual, read man newsyslog(8) to learn more about this file.

Check for improvements with lynis §

Lynis is a tool to scan your system looking for improvements in terms of security.

# pkg_add lynis
# lynis audit system

You'll see a lot of messages. Read "/var/log/lynis-report.dat" carefully then :

# less /var/log/lynis-report.dat

As example, you may read :

suggestion[]=SSH-7408|Consider hardening SSH configuration|AllowTcpForwarding (YES --> NO)|-|
suggestion[]=SSH-7408|Consider hardening SSH configuration|ClientAliveCountMax (3 --> 2)|-|
suggestion[]=SSH-7408|Consider hardening SSH configuration|Compression (YES --> NO)|-|

To filter for suggestions :

# grep suggestion /var/log/lynis-report.dat

How to deal with power loss ? Where my mails are going ? §

Well, that happens...

You can buy an uninterruptible power supply (UPS). It's a kind of backup battery where you plug your server and router. in case of powerloss, the UPS keep your server running giving about 20 minutes or more to shutdown properly or find another solution.

In case a daemon crashed because of some bug, OpenBSD notice in its daily report daemons that should be running but aren't. You can check then.

So, no big deal if sometimes your server is down.

What happens when a mail server is, for some reasons, unreachable ?

SMTP protocol is designed to be reliable.

If your server is unreachable when someone tries to send you an email, things happens like in real life : the mailmain try again after some time. After trying, it may give up, sending a message to the author indicating there is a problem with the recipient.

There are no rules about time between each periodic delivery attemps. Each 4h, then each days, depending on the sender's configuration. It may give up after a week however.

To prevent the worst case where your server has burned, configure a backup mail server -- secondary -- so it keeps your emails while you are fixing your server. It is described in the mail part of this documentation.

I have a problem, what can I do ? §

Daemon issue §

If a daemon isn't starting, try starting it by hand to see errors ouput. As example, with httpd, you can enter :

rcctl -d start http

Read Manpages §

Check the manpage of the tool having an issue.

Look into "/usr/local/share/doc/pkg-readmes" if there are specific instructions.

Problem with a port/package §

Use "pkg_check" command to fix the state of your ports usage.

Look into logs §

Look at logs with "tail" command to see if there are error messages :

# tail -f /var/log/messages /var/log/daemon

For a website:

# tail -f /var/www/logs/*

For login issues :

# tail -f /var/log/authlog

Also, run "dmesg" to see if a device has an issue.

Chech what does the Firewall §

Look at firewall traffic with tcpdump to see where it's failing :

# tcpdump -n -e -ttt -i pflog0

Corrupted filesystem or loop reboots §

Try to boot on the ramdisk bsd.rd.

boot> bsd.rd [Enter]

Then choose to reinstall sets with "U: Upgrade" or use the shell to fix filesystem with fsck.

You also can boot on a disk prepared with "minirootXX.img".

Then choose "S : Shell".

Look after your disk with "dmesg". In this example, we use sd1.

Make sure it is ready with :

cd /dev && sh MAKEDEV sd1

Then, check and fix the filesystem :

fsck -y sd1a
fsck -y sd1d

Do it for each slices. Use "disklabel sd1" to list them.

Ask for help §

Ask on official mailing list

Talk on XMPP room

Generate good random passwords §

The longer the better !

The more combinations a cracking tool will have to try, the more secure is your passphrase.

You may :

You can use OpenBSD's tools to generate random passwords. Below are examples for 15 charater long passwords :

$ openssl rand 15 -base64
$ dd if=/dev/urandom count=128 bs=1M 2>&1 | md5 | cut -b-15
$ jot -rcs '' 20 32 126
$ tr -cd ' -~' < /dev/urandom 
# only alnum
$ tr -cd '[:alnum:]' < /dev/urandom

Here are some interesting links on the topic :

Password strenght XKCD


Online tool to generate a password

Split and reassemble files §

If y ou have huge files to transfer, you can split them.

$ gzip -c big_file | split -b 100m - big_file.gz

You get max 100mb files sorted alphabetically :

$ ls -l
-rw-r--r--  1 xavier  xavier  104857600 Dec 12 16:52 big_file.gzaa
-rw-r--r--  1 xavier  xavier  104857600 Dec 12 16:52 big_file.gzab
-rw-r--r--  1 xavier  xavier   88204071 Dec 12 16:52 big_file.gzac

To recreate the original file, use cat :

cat big_file.gz_* | gunzip -c > big_file

Nice isnt it ? 😎

How to add/delete users? §

Add user §

Creating users let you :

According to the user's taks, you won't create the account the same way.

To add an user, use "adduser". It is interactive istead of "useradd". Yeah, I know...

That's how it looks like :

root@votreserveur[~] # adduser
Use option '-silent' if you don't want to see all warnings and questions.

Reading /etc/shells
Check /etc/master.passwd
Check /etc/group

Ok, let's go.
Don't worry about mistakes. There will be a chance later to correct any input.
Enter username []: toto
Enter full name []: Jean-Eudes
Enter shell csh ksh nologin sh [nologin]: nologin
Uid [1005]:
Login group toto [toto]:
Login group is ``toto''. Invite toto into other groups: guest no
Login class authpf bgpd daemon default dovecot pbuild staff unwind
Enter password []:
Enter password again []:

Name:        toto
Password:    ***************
Fullname:    Jean-Eudes
Uid:         1005
Gid:         1005 (toto)
Groups:      toto
Login Class: default
HOME:        /home/toto
Shell:       /sbin/nologin
OK? (y/n) [y]: y
Added user ``toto''
Add another user? (y/n) [y]: n

Make sure to set a good passphrase.

Delete an user §

# userdel toto
# groupdel toto

Here you go! 😊

SSH Tunnel : Proxy or VPN §

SSH let you create a PROXY to encapsulate some traffic and even creating a rudimentary VPN.

SOCKS Proxy §

To appear with the remote server's IP, dig an SSH tunnel :

From your computer :

ssh -D 9999 -NT batman@athome.tld

Then, configure applications to use a SOCKS proxy on localhost port 9999.

With Firefox, it is configured in "Network parameters".



Configuration examples §

/etc/dovecot/local.conf §

# listen both ipv4 and ipv6
listen = *, [::]

# imap better than pop
protocols = imap 

ssl = yes
ssl_cert = </etc/ssl/athome.tld.crt
ssl_key = </etc/ssl/private/athome.tld.key
disable_plaintext_auth = yes

service auth {
  user = $default_internal_user
  group = _maildaemons

passdb {
    args = scheme=blf-crypt /etc/mail/passwd
    driver = passwd-file

userdb {
    driver = static
    args = uid=_vmail gid=_vmail home=/mnt/bigstorage/_vmail/%d/%n/ 

# Plugins
mail_plugins = $mail_plugins quota zlib
protocol imap {
    mail_plugins = $mail_plugins imap_quota imap_zlib imap_sieve

plugin {
  quota = maildir:User quota
  quota_rule = *:storage=1G
  quota_rule2 = Trash:storage=+100M
  quota_grace = 50%%
  quota_status_success = DUNNO
  quota_status_nouser = DUNNO
  quota_status_overquota = "552 5.2.2 Mailbox is full"

  zlib_save_level = 9 # 1..9; default is 6
  zlib_save = gz # or bz2, xz or lz4

  sieve_plugins = sieve_imapsieve sieve_extprograms

  sieve_default = /usr/local/lib/dovecot/sieve/default.sieve

  imapsieve_mailbox1_name = Junk
  imapsieve_mailbox1_causes = COPY
  imapsieve_mailbox1_before = file:/usr/local/lib/dovecot/sieve/report-spam.sieve

  imapsieve_mailbox2_name = *
  imapsieve_mailbox2_from = Junk
  imapsieve_mailbox2_causes = COPY
  imapsieve_mailbox2_before = file:/usr/local/lib/dovecot/sieve/report-ham.sieve

  sieve_pipe_bin_dir = /usr/local/lib/dovecot/sieve

  sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment

/etc/httpd.conf §

types { include "/usr/share/misc/mime.types" }

server "default" {
    listen on * port 80 
    root "/htdocs/athome.tld" 

server "athome.tld" {
    listen on * port 80
    block return 301 "https://$SERVER_NAME$REQUEST_URI"

server "athome.tld" { 
    alias "www.athome.tld"
    listen on * tls port 443 
    root "/htdocs/athome.tld" 
    directory index index.html
    log style combined

    hsts preload
    tls {
        certificate "/etc/ssl/athome.tld.crt"
        key "/etc/ssl/private/athome.tld.key"

    location "/.well-known/acme-challenge/*" {
        root "/acme"
        request strip 2

    location "/Blog/" {
        directory index index.php

    location "*.php*" {
        fastcgi socket "/run/php-fpm.sock"

    location "/DL/PDF/" {
        directory auto index

    location "/private/" {
        authenticate "education" with "/htdocs/private.htpw"
        directory auto index

server "site2.athome.tld" { 
    alias "www.site2.athome.tld"
    listen on * port 80 
    listen on * tls port 443 
    root "/htdocs/site2" 
    directory index index.html
    log access "site2.log"

    tls {
        certificate "/etc/ssl/athome.tld.crt"
        key "/etc/ssl/private/athome.tld.key"

    location "/.well-known/acme-challenge/*" {
        root "/acme"
        request strip 2

    location "*.php*" {
        fastcgi socket "/run/php-fpm.sock"
    location "/downloads/" {
        directory index index.php

/var/nsd/etc/nsd.conf §

        hide-version: yes
        verbosity: 2
        database: "" # disable database
        zonesdir: "/var/nsd/zones/"
        ip-address: 2a03:6000:9137::148 

        control-enable: yes

        name: "secretkey"
        algorithm: hmac-sha256
        secret: "i8f4FgDsldD11pHAqo9Ko="

        name: ""
        zonefile: "signed/"
        provide-xfr: secretkey
        notify: secretkey

        # GANDI
        provide-xfr: NOKEY
        notify: NOKEY

# slaves
        name: ""
        zonefile: "slave/"
        allow-notify: secretkey
        request-xfr: secretkey

        name: ""
        zonefile: "slave/"
        allow-notify: secretkey
        request-xfr: secretkey

        name: ""
        zonefile: "slave/"
        allow-notify: secretkey
        request-xfr: secretkey

/etc/pf.conf §

# See pf.conf(5) and /etc/examples/pf.conf

# Macros
## Interfaces to take care. egress should be enough
## but it's an example ^^
ifaces = "{ egress em0 em1 }"
## various ports
mail_ports = "{ submission imaps smtp }"
tcp_pass = "{ www https domain 1965 xmpp-client xmpp-server 5280 5281 62882 }" # 5280-5281 are xmpp-http, 62882 transmission
udp_pass = "{ domain 62882 }" # 62882 dht rtorrent
blocking_tcp="{ ftp ftp-data telnet finger sunrpc epmap netbios-ns netbios-dgm netbios-ssn microsoft-ds ipp ldaps ldp ms-sql-s ms-sql-m pptp mysql postgresql rfb rdp 27019 1194 ldap 8080 kerberos socks }"

# Tables
table <evils> persist
table <bruteforce> persist
table <sshguard> persist
table <pfbadhost> persist file "/etc/pf-badhost.txt"
table <solene> persist file "/etc/solene-block.txt"
table <spamd> persist

# Options
## increase limit for huge blocking table files
set limit table-entries 409600
## no not filter local
set skip on { lo }

# Avoid spoofing
antispoof for $ifaces

# Rules
## block by default 
anchor "relayd/*" # so relayd works properly

## "quick" rules : the rest won't be read if it matches.
## This filter bad ip
### block unwanted sources, and don't go further
block log quick from <bruteforce> label "BRUTES"
block log quick from <evils>  label "EVILS"
block log quick from <sshguard> label "SSHGUARD"
block log quick on $ifaces from <pfbadhost> label "PFBADHOST"
block log quick on $ifaces from <solene> label "SOLENE"

### Let in local network, or it is blocked by pfbadhost 
pass in quick from modulate state

### iblock : everything else is banned
pass in quick on $ifaces inet proto tcp to port $blocking_tcp rdr-to port 2507
pass in quick on $ifaces inet6 proto tcp to port $blocking_tcp rdr-to ::1 port 2507

## Allow some incoming traffic
### spamd traps in blacklist only
pass in on $ifaces inet proto tcp from <spamd> to any port smtp \
	divert-to port spamd modulate state

### let ssh in, with anti bruteforce
pass in on $ifaces proto tcp to port ssh modulate state \
	(source-track rule, \
	max-src-conn 8, max-src-conn-rate 15/5, \
	overload <bruteforce> flush global)

### same with email
pass in on $ifaces proto tcp to port $mail_ports modulate state \
	(source-track rule, \
	max-src-conn 100, max-src-conn-rate 50/100, \
	overload <bruteforce> flush global)

### let some ports in 
pass in on $ifaces proto tcp to port $tcp_pass modulate state
pass in on $ifaces proto udp to port $udp_pass

### allow ping, in and out
pass on $ifaces inet6 proto ipv6-icmp all icmp6-type echoreq
pass on $ifaces inet proto icmp all icmp-type echoreq

### Let all out 
pass out on $ifaces proto { tcp udp }

/etc/relayd.conf §

ext_ip4 = ""
ext_ip6 = "2001:db8::2

tcp protocol "tlsrelay" {
	tls keypair
	tcp { nodelay, sack }

relay "gemini" {
	listen on $ext_ip4 port 1965 tls
	protocol "tlsrelay"
	forward to port 11965

relay "gemini6" {
	listen on $ext_ip6 port 1965 tls
	protocol "tlsrelay"
	forward to ::1 port 11965

http protocol "http" {
	include "/etc/relayd.proxy.conf"

http protocol "https" {
	include "/etc/relayd.proxy.conf"
	match query hash "sessid"
	tls keypair

relay "http" {
	listen on $ext_ip4 port 80
	protocol "http"
	forward to port 80

relay "http6" {
	listen on $ext_ip6 port 80
	protocol "http"
	forward to ::1 port 80

relay "https" {
	listen on $ext_ip4 port 443 tls
	protocol "https"
	forward with tls to port 443

relay "tlsforward6" {
	listen on $ext_ip6 port 443 tls
	protocol "https"
	forward with tls to ::1 port 443

/etc/relayd.proxy.conf :

return error

return error style "body { background: silver; color: black; text-align:center } hr {border:0;
background-color:silver; color:silver; height:1px; width:30%; margin-top:50px;}"

match request header set "X-Forwarded-For" \
    value "$REMOTE_ADDR"
match request header set "X-Forwarded-By" \

match header set "Keep-Alive" value "$TIMEOUT"

block quick path "/wp-*" label '<em>Stop scanning for wordpress</em>.'

match request header remove "Proxy"
match response header set "X-Xss-Protection" value "1; mode=block"
match response header set "Frame-Options" value "SAMEORIGIN"
match response header set "X-Frame-Options" value "SAMEORIGIN"
match response header set "X-Robots-Tag" value "index,nofollow"
match response header set "X-Permitted-Cross-Domain-Policies" value "none"
match response header set "X-Download-Options" value "noopen"
match response header set "X-Content-Type-Options" value "nosniff"
match response header set "Referrer-Policy" value "no-referrer"
match response header set "Content-Security-Policy" value "upgrade-insecure-requests"
match response header set "Permissions-Policy" value "interest-cohort=()"

match response header set "X-Powered-By" value "Powered by OpenBSD"

match request path "/*.css" tag "CACHE"
match request path "/*.js" tag "CACHE"
match request path "/*.atom" tag "CACHE"
match request path "/*.rss" tag "CACHE"
match request path "/*.xml" tag "CACHE"
match request path "/*.jpg" tag "CACHE"
match request path "/*.png" tag "CACHE"
match request path "/*.svg" tag "CACHE"
match request path "/*.gif" tag "CACHE"
match request path "/*.ico" tag "CACHE"
match request path "/*.html" tag "CACHE"
match request path "/*.gmi" tag "CACHE"
match request path "*/" tag "CACHE"

match response tagged "CACHE" header set "Cache-Control" value \
    "public, max-age=86400"

match request path "/*.html" tag "HTML"
match response tagged "HTML" header set "Content-Type" value "text/html; charset=utf-8"

match request path "/*.txt" tag "TXT"
match request path "/*.md" tag "TXT"
match request path "/*.gmi" tag "TXT"
match response tagged "TXT" header set "Content-Type" value "text/plain; charset=utf-8"


/etc/mail/smtpd.conf §

Example for multiple domains

Example 1 (rspamd) §

# install :
#   opensmtpd-filter-rspamd
#   opensmtpd-filter-senderscore

table aliases "/etc/mail/aliases"
table domains "/etc/mail/domains"
table passwd "/etc/mail/passwd"
table virtuals "/etc/mail/virtuals"

pki athome.tld key "/etc/ssl/private/athome.tld.key"
pki athome.tld cert "/etc/ssl/athome.tld.crt"
pki key "/etc/ssl/private/"
pki cert "/etc/ssl/"
pki key "/etc/ssl/private/"
pki cert "/etc/ssl/"
# certificat par defaut
pki "*" key "/etc/ssl/private/athome.tld.key"
pki "*" cert "/etc/ssl/athome.tld.crt"

filter senderscore \
         proc-exec "filter-senderscore -junkBelow 70 -slowFactor 2000"
filter rspamd proc-exec "filter-rspamd"

listen on all tls pki athome.tld \
    filter { senderscore, rspamd }
listen on all port submission tls-require pki athome.tld auth <passwd> \
    filter rspamd

action "relay" relay
action relaybackup relay backup tls helo ""
action "local_mail" maildir alias <aliases>
action virtual_maildir maildir "/home/_vmail/%{dest.domain:lowercase}/%{dest.user:lowercase}/Maildir" junk virtual <virtuals>

match from any for domain <domains> action virtual_maildir
match from any for local action local_mail

match from any for domain action relaybackup

match auth from any for any action "relay"
match for any action "relay"

Example 2 (spamassassin + dkimproxy) §

Only one certificate matching all domains is used here.

We add some options on queue, because.

table aliases "/etc/mail/aliases"
table passwd "/etc/mail/passwd"
table virtuals "/etc/mail/virtuals"
table domains "/etc/mail/domains"

pki athome.tld key "/etc/ssl/private/athome.tld.key"
pki athome.tld cert "/etc/ssl/athome.tld.crt"

queue compression # less disk space
queue encryption 7dbecabecabeca45bce4aebc # encrypt all o/

filter senderscore \
         proc-exec "filter-senderscore -junkBelow 70 -slowFactor 2000"

listen on lo0 port 10028 tag DKIM   
listen on lo0 port 10026 tag SPAMASSASSIN

listen on all tls pki athome.tld filter { senderscore }
listen on all port submission tls-require pki athome.tld auth <passwd> 

action "envoi" relay 
action dkimproxy relay host smtp:// 
action spamassassin relay host smtp:// 

action local_mail maildir alias <aliases>

action relaybackup relay backup mx "athome.tld" helo "athome.tld"

action virtual_maildir maildir "/var/vmail/%{dest.domain:lowercase}/%{dest.user:lowercase}/Maildir" junk virtual <virtuals>

match for local action local_mail
match tag SPAMASSASSIN from any for domain <domains> action virtual_maildir
match from any for domain <domains> action spamassassin

match tag DKIM for any action "envoi"
match auth tag DKIM from any for any action "envoi"

match from any for domain action relaybackup

match auth from any for any action dkimproxy
match for any action dkimproxy

/etc/mail/domains §

All MX records


/etc/mail/spamd.conf §


# Nixspam recent sources list.
# Mirrored from
        :msg="Your address %A is in the nixspam list\n\
        See for details":\

        :msg="Your address %A is in the list":\

         :msg="Your address %A has sent mail to a spamtrap\n\
          within the last 24 hours":\


/etc/webalizer.conf §

LogFile		/var/www/logs/access.log
OutputDir	/var/www/htdocs/chezmoi.tld/stats
ReportTitle	Statistiques pour 
HostName	chezmoi.tld
LinkReferrer	yes
HTMLHead	<style type="text/css">
HTMLHead	body {background:#eceff4;color:#2e3440;line-height:1.4;margin:auto}
HTMLHead	table {border: 1px solid; padding:1ex}
HTMLHead	a {color:#5e81ac}
HTMLHead	th, td {border: 0}
HTMLHead	tr:nth-child(even){background-color: #e5e9f0;}
HTMLHead	tr:hover {background-color: #d8dee9;}
HTMLHead	</style>
TopSites	75
TopURLs		50
TopReferrers	100
AllSites	yes
AllURLs		yes
AllReferrers	yes
AllSearchStr	yes
AllErrors	yes
HideSite	*chezmoi.tld
HideReferrer	chezmoi.tld
HideURL       *.gif
HideURL       *.GIF
HideURL       *.jpg
HideURL       *.JPG
HideURL       *.png
HideURL       *.PNG
HideURL       *.css
HideURL       *.woff
GroupReferrer google. Google Intl
HideReferrer google.
IgnoreURL	/atom.xml
IgnoreURL	/sitemap.*
IgnoreURL	/favicon.*
IgnoreURL	/robots.txt
ColorBackground eceff4
ColorText       2e3440
ColorLink       5e81ac
ColorVLink      81a1c1
ColorALink      88c0d0
ColorHeadline   d8dee9
ColorCounter    4c566a
ColorHit        5e81ac
ColorFile       bf616a
ColorSite       d08770
ColorKbyte      ebcb8b
ColorPage       a3be8c
ColorVisit      b48ead
ColorMisc       8fbcbb
ChartBackgroundColor    eceff4
ChartLegendColor        2e3440
ChartShadowColor1       eceff4
ChartShadowColor2       d8dee9
TableBorder     0
ChartBorder     0

Notes §

How to contribute? §

If you want and if you can, you can contribute by donating money.

Sources can be downloaded with this link.

If you notice an error or want to suggest improvements, please follow these guidelines :

Send diff to

$ cp -r src/ src-mod
### edit files in src-mod
$ diff -ur src/ src-mod/ > my-changes.diff
### send my-changes.diff file to

Thank you !

Thanks §

Thank you to :

Solène Rapenne

Vincent Finance

Stéphane aka 22decembre


Péhä, amazing illustrator

Fred Galusik

Henning Stedtnitz for proofreading.

And so much more I may have forgotten 😅.

Special thanks to my beloved one who reboot the server when I'm away.