Debian + Postfix + Dovecot + Multidomain + SSL + IPv6 + OpenVPN + Multi-interfaces + SpamAssassin-learn + Bind

This article is about how to set up a modern mail server.
Postfix + Dovecot. SPF + DKIM + rDNS. With IPv6.
With TSL encryption. Supporting multiple domains — part with a valid SSL certificate.
With anti-spam protection and a high anti-spam rating compared to other mail servers.
With support for multiple physical interfaces.
With OpenVPN, connecting via IPv4, which provides IPv6.

If you don’t want to learn all these technologies, but want to set up such a server — then this article is for you.

The article does not attempt to explain every detail. The explanation focuses on what is non-standard or important from the consumer's perspective.

The motivation to set up a mail server — has been my long-standing dream. It may sound silly, but IMHO, it's much better than dreaming about a new car of my favorite brand.

The motivation to set up IPv6 — is twofold. An IT specialist needs to constantly learn new technologies to survive. I want to make my humble contribution to the fight against censorship.

The motivation for setting up OpenVPN — is solely for getting IPv6 to work on the local machine.
The motivation for configuring multiple physical interfaces — I have one interface on my server that is 'slow but unlimited,' and another that is 'fast but with a tariff.'

The motivation for setting up Bind — my provider offers an unstable DNS server, and Google sometimes experiences outages as well. I want a stable DNS server for personal use.

The motivation to write this article — the draft was written 10 months ago, and I have already looked at it twice. If the author needs it regularly, there's a good chance others will too.

There is no universal solution for a mail server. But I will try to write something like 'do this and then, when everything works as it should — get rid of the extras.'

There is a Colocation server at tech.ru. There is an opportunity to compare with OVH, Hetzner, AWS. For this task, it will be much more efficient to cooperate with tech.ru.

Debian 9 is installed on the server.

There are 2 interfaces on the server, `eno1` and `eno2`. The first is unlimited, while the second is fast respectively.

There are 3 static IP addresses, XX.XX.XX.X0, XX.XX.XX.X1, and XX.XX.XX.X2 on interface `eno1` and XX.XX.XX.X5 on interface `eno2`.

There is a XXXX:XXXX:XXXX:XXXX::/64 pool of IPv6 addresses assigned to interface `eno1`, and from it XXXX:XXXX:XXXX:XXXX:1:2::/96 was assigned to `eno2` at my request.

There are 3 domains `domain1.com`, `domain2.com`, `domain3.com`. The domains `domain1.com` and `domain3.com` have SSL certificates.

There is a Google account to which I want to link the email address `vasya.pupkin@domain1.com` (to send and receive email directly from the Gmail interface).
There should be an email account `support@domain2.com`, and I want to receive a copy of the emails from it in my Gmail. I should also rarely have the ability to send emails from `support@domain2.com` through the web interface.

There should be an email account `ivanov@domain3.com`, which Ivanov will use from his iPhone.

Sent emails must comply with all modern anti-spam requirements.
The highest level of encryption available in public networks should be implemented.
There should be support for IPv6 for both sending and receiving emails.
There must be SpamAssassin, which will never delete emails but will either bounce, allow through, or send them to the IMAP folder 'Spam'.
SpamAssassin's auto-learning must be configured: if I move an email to the 'Spam' folder, it should learn from this; if I move an email from the 'Spam' folder, it should learn from that. The results of SpamAssassin's training should affect the placement of emails in the 'Spam' folder.
PHP scripts should be able to send emails on behalf of any domain on this server.
There should be an OpenVPN service that allows the use of IPv6 on a client that does not have IPv6.

First, interfaces and routing must be configured, including IPv6.
Next, OpenVPN should be configured to connect via IPv4 and provide the client with a static real IPv6 address. This client will have access to all IPv6 services on the server and to any IPv6 resources on the internet.
Then, Postfix should be configured for sending emails, along with SPF, DKIM, rDNS, and other similar details.
Next, Dovecot should be configured, along with multidomain setup.
After that, SpamAssassin should be configured and set up for training.
Finally, Bind should be installed.

============= Multi-interfaces =============

To configure the interfaces, you need to add the following to '/etc/network/interfaces'.

# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
allow-hotplug eno1
iface eno1 inet static
        address XX.XX.XX.X0/24
        gateway XX.XX.XX.1
        dns-nameservers 127.0.0.1 213.248.1.6
        post-up ip route add XX.XX.XX.0/24 dev eno1 src XX.XX.XX.X0 table eno1t
        post-up ip route add default via XX.XX.XX.1 table eno1t
        post-up ip rule add table eno1t from XX.XX.XX.X0
        post-up ip rule add table eno1t to XX.XX.XX.X0

auto eno1:1
iface eno1:1 inet static
address XX.XX.XX.X1
netmask 255.255.255.0
        post-up ip rule add table eno1t from XX.XX.XX.X1
        post-up ip rule add table eno1t to XX.XX.XX.X1
        post-up   ip route add 10.8.0.0/24 dev tun0 src XX.XX.XX.X1 table eno1t
        post-down ip route del 10.8.0.0/24 dev tun0 src XX.XX.XX.X1 table eno1t

auto eno1:2
iface eno1:2 inet static
address XX.XX.XX.X2
netmask 255.255.255.0
        post-up ip rule add table eno1t from XX.XX.XX.X2
        post-up ip rule add table eno1t to XX.XX.XX.X2

iface eno1 inet6 static
        address XXXX:XXXX:XXXX:XXXX:1:1::/64
        gateway XXXX:XXXX:XXXX:XXXX::1
        up   ip -6 addr add XXXX:XXXX:XXXX:XXXX:1:1:1:1/64 dev $IFACE
        up   ip -6 addr add XXXX:XXXX:XXXX:XXXX:1:1:1:2/64 dev $IFACE
        down ip -6 addr del XXXX:XXXX:XXXX:XXXX:1:1:1:1/64 dev $IFACE
        down ip -6 addr del XXXX:XXXX:XXXX:XXXX:1:1:1:2/64 dev $IFACE

# The secondary network interface
allow-hotplug eno2
iface eno2 inet static
        address XX.XX.XX.X5
        netmask 255.255.255.0
        post-up   ip route add XX.XX.XX.0/24 dev eno2 src XX.XX.XX.X5 table eno2t
        post-up   ip route add default via XX.XX.XX.1 table eno2t
        post-up   ip rule add table eno2t from XX.XX.XX.X5
        post-up   ip rule add table eno2t to XX.XX.XX.X5
        post-up   ip route add 10.8.0.0/24 dev tun0 src XX.XX.XX.X5 table eno2t
        post-down ip route del 10.8.0.0/24 dev tun0 src XX.XX.XX.X5 table eno2t

iface eno2 inet6 static
        address XXXX:XXXX:XXXX:XXXX:1:2::/96
        up   ip -6 addr add XXXX:XXXX:XXXX:XXXX:1:2:1:1/64 dev $IFACE
        up   ip -6 addr add XXXX:XXXX:XXXX:XXXX:1:2:1:2/64 dev $IFACE
        down ip -6 addr del XXXX:XXXX:XXXX:XXXX:1:2:1:1/64 dev $IFACE
        down ip -6 addr del XXXX:XXXX:XXXX:XXXX:1:2:1:2/64 dev $IFACE

# OpenVPN network
iface tun0 inet6 static
        address XXXX:XXXX:XXXX:XXXX:1:3::/80

These settings can be applied to any server in tech.ru (with minor coordination with support), and it will work right away as expected.

If you have experience configuring similar things for Hetzner, OVH — it's different there. It's more complicated.

eno1 is the name of network card #1 (slow but unlimited).
eno2 is the name of network card #2 (fast but limited).
tun0 is the name of the virtual network card from OpenVPN.
XX.XX.XX.X0 is IPv4 #1 on eno1.
XX.XX.XX.X1 is IPv4 #2 on eno1.
XX.XX.XX.X2 is IPv4 #3 on eno1.
XX.XX.XX.X5 is IPv4 #1 on eno2.
XX.XX.XX.1 is the IPv4 gateway.
XXXX:XXXX:XXXX:XXXX::/64 is the IPv6 for the entire server.
XXXX:XXXX:XXXX:XXXX:1:2::/96 is the IPv6 for eno2; all other traffic comes in through eno1.
XXXX:XXXX:XXXX:XXXX::1 is the IPv6 gateway (it should be noted that this can/should be done differently – specify the IPv6 of the switch).
dns-nameservers are set to 127.0.0.1 (because bind is installed locally) and 213.248.1.6 (this is from tech.ru).

"table eno1t" and "table eno2t" — the purpose of these route rules is that traffic coming in through eno1 should go out through it as well, while traffic coming in through eno2 should exit through it too. Additionally, server-initiated connections should go out through eno1.

ip route add default via XX.XX.XX.1 table eno1t

With this command, we specify that any unclear traffic that falls under any rule marked with "table eno1t" should be directed to the eno1 interface.

ip route add XX.XX.XX.0/24 dev eno1 src XX.XX.XX.X0 table eno1t

With this command, we specify that any server-initiated traffic should be directed to the eno1 interface.

ip rule add table eno1t from XX.XX.XX.X0
ip rule add table eno1t to XX.XX.XX.X0

With this command, we set the rules for traffic marking.

auto eno1:2
iface eno1:2 inet static
address XX.XX.XX.X2
netmask 255.255.255.0
        post-up ip rule add table eno1t from XX.XX.XX.X2
        post-up ip rule add table eno1t to XX.XX.XX.X2

This block assigns the second IPv4 address to the eno1 interface.

ip route add 10.8.0.0/24 dev tun0 src XX.XX.XX.X1 table eno1t

With this command, we specify the route from OpenVPN clients to the local IPv4 addresses except for XX.XX.XX.X0.
I still don't understand why this command is sufficient for all IPv4.

iface eno1 inet6 static
        address XXXX:XXXX:XXXX:XXXX:1:1::/64
        gateway XXXX:XXXX:XXXX:XXXX::1

Here we set the address for the interface itself. The server will use it as the 'outgoing' address. It won't be used in any other way.

Why is it so complicated to specify ":1:1::"? To ensure OpenVPN works correctly and only for this reason. More on that later.

Regarding the gateway — it works as it is. But correctly — this should state the IPv6 of the switch to which the server is connected.

However, for some reason, IPv6 stops working if I do it this way. Perhaps this is some issue with tech.ru.

ip -6 addr add XXXX:XXXX:XXXX:XXXX:1:1:1:1/64 dev $IFACE

This adds an IPv6 address to the interface. If you need a hundred addresses — you'll need a hundred lines in this file.

iface eno1 inet6 static
        address XXXX:XXXX:XXXX:XXXX:1:1::/64
...
iface eno2 inet6 static
        address XXXX:XXXX:XXXX:XXXX:1:2::/96
...
iface tun0 inet6 static
        address XXXX:XXXX:XXXX:XXXX:1:3::/80

I marked the addresses and subnets of all interfaces for clarity.
eno1 — must have «/64» — because that is our entire pool of addresses.
tun0 — the subnet must be larger than eno1; otherwise, it won't be possible to configure the IPv6 gateway for OpenVPN clients.
eno2 — the subnet must be larger than tun0; otherwise, OpenVPN clients won't be able to access local IPv6 addresses.
For clarity, I chose a subnet step of 16, but you can even use a step of «1» if you wish.
Thus, 64+16 = 80, and 80+16 = 96.

For even greater clarity:
XXXX:XXXX:XXXX:XXXX:1:1:YYYY:YYYY — these are the addresses that should be assigned to specific sites or services on the eno1 interface.
XXXX:XXXX:XXXX:XXXX:1:2:YYYY:YYYY — these are the addresses that should be assigned to specific sites or services on the eno2 interface.
XXXX:XXXX:XXXX:XXXX:1:3:YYYY:YYYY — these are the addresses that should be assigned to OpenVPN clients or used as service addresses for OpenVPN.

For network configuration — there must be the ability to reboot the server.
IPv4 changes are picked up when executing (be sure to wrap in screen — otherwise, this command will simply drop the network on the server):

/etc/init.d/networking restart

Add the following to the end of the file «/etc/iproute2/rt_tables»:

100 eno1t
101 eno2t

Without this, custom tables cannot be used in the «/etc/network/interfaces» file.
The numbers must be unique and less than 65535.

IPv6 changes can be adjusted easily without rebooting, but for this, you need to learn at least three commands:

ip -6 addr ...
ip -6 route ...
ip -6 neigh ...

Configuration of «/etc/sysctl.conf»

# Uncomment the next line to enable packet forwarding for IPv4
net.ipv4.ip_forward = 1

# Do not accept ICMP redirects (prevent MITM attacks)
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0

# Do not send ICMP redirects (we are not a router)
net.ipv4.conf.all.send_redirects = 0

# For receiving ARP replies
net.ipv4.conf.all.arp_filter = 0
net.ipv4.conf.default.arp_filter = 0

# For sending ARP
net.ipv4.conf.all.arp_announce = 0
net.ipv4.conf.default.arp_announce = 0

# Enable IPv6
net.ipv6.conf.all.disable_ipv6 = 0
net.ipv6.conf.default.disable_ipv6 = 0
net.ipv6.conf.lo.disable_ipv6 = 0

# IPv6 configuration
net.ipv6.conf.all.autoconf = 1
net.ipv6.conf.all.accept_ra = 0

# For OpenVPN
net.ipv6.conf.all.forwarding = 1
net.ipv6.conf.all.proxy_ndp = 1

# For nginx on boot
net.ipv6.ip_nonlocal_bind = 1

These are the «sysctl» settings on my server. I’ll note something important.

net.ipv4.ip_forward = 1

Without this, OpenVPN will not function at all.

net.ipv6.ip_nonlocal_bind = 1

Anyone attempting to bind IPv6 (for example, nginx) immediately after the interface is set up will receive an error stating that such an address is unavailable.

This setting is made to avoid such situations.

net.ipv6.conf.all.forwarding = 1
net.ipv6.conf.all.proxy_ndp = 1

Without these settings, IPv6 traffic from OpenVPN clients will not reach the outside world.

Other settings are either irrelevant or I don't remember why they exist.
But just in case, I’ll leave them as they are.

To ensure that changes in this file take effect without rebooting the server — you need to execute the command:

sysctl -p

More details about the «table» rules: habr.com/post/108690

============= OpenVPN =============

OpenVPN IPv4 does not work without iptables.

Here are my iptables rules for the VPN:

iptables -A INPUT -p udp -s YY.YY.YY.YY --dport 1194 -j ACCEPT
iptables -A FORWARD -i tun0 -o eno1 -j ACCEPT
iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eno1 -j SNAT --to-source XX.XX.XX.X0
##iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eno1 -j MASQUERADE
iptables -A FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -A INPUT -p udp --dport 1194 -j DROP
iptables -A FORWARD -p udp --dport 1194 -j DROP

YY.YY.YY.YY is my static IPv4 address for the local machine.
10.8.0.0/24 is the IPv4 network for OpenVPN. IPv4 addresses for OpenVPN clients.
The sequence of rules is important.

iptables -A INPUT -p udp -s YY.YY.YY.YY --dport 1194 -j ACCEPT
iptables -A FORWARD -i tun0 -o eno1 -j ACCEPT
...
iptables -A FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -A INPUT -p udp --dport 1194 -j DROP
iptables -A FORWARD -p udp --dport 1194 -j DROP

This restriction ensures that only I can use OpenVPN from my static IP.

iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eno1 -j SNAT --to-source XX.XX.XX.X0
  -- or --
iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eno1 -j MASQUERADE

To route IPv4 packets between OpenVPN clients and the internet, you need to use one of these commands.

One of the options may not be suitable for different cases.
Both commands are suitable for my case.
After reading the documentation, I chose the first option because it consumes less CPU.

To ensure that all iptables settings are applied after reboot, they need to be saved somewhere.

iptables-save > /etc/iptables/rules.v4
ip6tables-save > /etc/iptables/rules.v6

Such names are not chosen by chance. They are used by the 'iptables-persistent' package.

apt-get install iptables-persistent

Installing the main OpenVPN package:

apt-get install openvpn easy-rsa

Let's set up a template for the certificates (insert your values):

make-cadir ~/openvpn-ca
cd ~/openvpn-ca
ln -s openssl-1.0.0.cnf openssl.cnf

Let's edit the template settings for the certificates:

mcedit vars

# These are the default values for fields
# which will be placed in the certificate.
# Don't leave any of these fields blank.
export KEY_COUNTRY="RU"
export KEY_PROVINCE="Krasnodar"
export KEY_CITY="Dinskaya"
export KEY_ORG="Own"
export KEY_EMAIL="admin@domain1.com"
export KEY_OU="VPN"

# X509 Subject Field
export KEY_NAME="server"
...

Creating the server certificate:

cd ~/openvpn-ca
source vars
./clean-all
./build-ca
./build-key-server server
./build-dh
openvpn --genkey --secret keys/ta.key

Let's prepare the ability to create the final 'client-name.opvn' files:

mkdir -p ~/client-configs/files
chmod 700 ~/client-configs/files
cp /usr/share/doc/openvpn/examples/sample-config-files/client.conf ~/client-configs/base.conf
mcedit ~/client-configs/base.conf

# Client mode
client

# Interface tunnel type
dev tun

# TCP protocol
proto tcp-client

# Address/Port of VPN server
remote XX.XX.XX.X0 1194

# Don't bind to local port/address
nobind

# Don't need to re-read keys and re-create tun at restart
persist-key
persist-tun

# Remote peer must have a signed certificate
remote-cert-tls server
ns-cert-type server

# Enable compression
comp-lzo

# Custom
ns-cert-type server
tls-auth ta.key 1
cipher DES-EDE3-CBC

Let's prepare a script that will stitch all files into a single opvn file.

mcedit ~/client-configs/make_config.sh
chmod 700 ~/client-configs/make_config.sh

#!/bin/bash

# First argument: Client identifier

KEY_DIR=~/openvpn-ca/keys
OUTPUT_DIR=~/client-configs/files
BASE_CONFIG=~/client-configs/base.conf

cat ${BASE_CONFIG} 
    <(echo -e '<ca>') 
    ${KEY_DIR}/ca.crt 
    <(echo -e '</ca>n<cert>') 
    ${KEY_DIR}/${1}.crt 
    <(echo -e '</cert>n<key>') 
    ${KEY_DIR}/${1}.key 
    <(echo -e '</key>n<tls-auth>') 
    ${KEY_DIR}/ta.key 
    <(echo -e '</tls-auth>') 
    > ${OUTPUT_DIR}/${1}.ovpn

Creating the first OpenVPN client:

cd ~/openvpn-ca
source vars
./build-key client-name
cd ~/client-configs
./make_config.sh client-name

Send the file '~/client-configs/files/client-name.ovpn' to the client's device.

For iOS clients, you will need to perform a trick:
The contents of the «tls-auth» tag must be without comments.
Also, add «key-direction 1» immediately before the «tls-auth» tag.

Let's configure the OpenVPN server:

cd ~/openvpn-ca/keys
cp ca.crt ca.key server.crt server.key ta.key dh2048.pem /etc/openvpn
gunzip -c /usr/share/doc/openvpn/examples/sample-config-files/server.conf.gz | tee /etc/openvpn/server.conf
mcedit /etc/openvpn/server.conf

# Listen port
port 1194

# Protocol
proto tcp-server

# IP tunnel
dev tun0
tun-ipv6
push tun-ipv6

# Master certificate
ca ca.crt

# Server certificate
cert server.crt

# Server private key
key server.key

# Diffie-Hellman parameters
dh dh2048.pem

# Allow clients to communicate with each other
client-to-client

# Client config dir
client-config-dir /etc/openvpn/ccd

# Run client-specific script on connection and disconnection
script-security 2
client-connect "/usr/bin/sudo -u root /etc/openvpn/server-clientconnect.sh"
client-disconnect "/usr/bin/sudo -u root /etc/openvpn/server-clientdisconnect.sh"

# Server mode and client subnets
server 10.8.0.0 255.255.255.0
server-ipv6 XXXX:XXXX:XXXX:XXXX:1:3::/80
topology subnet

# IPv6 routes
push "route-ipv6 XXXX:XXXX:XXXX:XXXX::/64"
push "route-ipv6 2000::/3"

# DNS (for Windows)
# These are OpenDNS
push "dhcp-option DNS 208.67.222.222"
push "dhcp-option DNS 208.67.220.220"

# Configure all clients to redirect their default network gateway through the VPN
push "redirect-gateway def1 bypass-dhcp"
push "redirect-gateway ipv6" #For iOS

# Don't need to re-read keys and re-create tun at restart
persist-key
persist-tun

# Ping every 10s. Timeout of 120s.
keepalive 10 120

# Enable compression
comp-lzo

# User and group
user vpn
group vpn

# Log a short status
status openvpn-status.log

# Logging verbosity
##verb 4

# Custom config
tls-auth ta.key 0
cipher DES-EDE3-CBC

This is necessary to assign a static address to each client (not mandatory, but I use it):

# Client config dir
client-config-dir /etc/openvpn/ccd

The most complex and crucial detail.

Unfortunately, OpenVPN still cannot automatically set up an IPv6 gateway for clients.
It has to be manually forwarded for each client.

# Run client-specific script on connection and disconnection
script-security 2
client-connect "/usr/bin/sudo -u root /etc/openvpn/server-clientconnect.sh"
client-disconnect "/usr/bin/sudo -u root /etc/openvpn/server-clientdisconnect.sh"

The file «/etc/openvpn/server-clientconnect.sh»:

#!/bin/sh

# Check client variables
if [ -z "$ifconfig_pool_remote_ip" ] || [ -z "$common_name" ]; then
        echo "Missing environment variable."
        exit 1
fi

# Load server variables
. /etc/openvpn/variables

ipv6=""

# Find out if there is a specific config with fixed IPv6 for this client
if [ -f "/etc/openvpn/ccd/$common_name" ]; then
        # Get fixed IPv6 from client config file
        ipv6=$(sed -nr 's/^.*ifconfig-ipv6-push[ t]+([0-9a-fA-F:]+).*$/1/p' "/etc/openvpn/ccd/$common_name")
        echo $ipv6
fi

# Get IPv6 from IPv4
if [ -z "$ipv6" ]; then
        ipp=$(echo "$ifconfig_pool_remote_ip" | cut -d. -f4)
        if ! [ "$ipp" -ge 2 -a "$ipp" -le 254 ] 2>/dev/null; then
                echo "Invalid IPv4 part."
                exit 1
        fi
        hexipp=$(printf '%x' $ipp)
        ipv6="$prefix$hexipp"
fi

# Create proxy rule
/sbin/ip -6 neigh add proxy $ipv6 dev eno1

The file «/etc/openvpn/server-clientdisconnect.sh»:

#!/bin/sh

# Check client variables
if [ -z "$ifconfig_pool_remote_ip" ] || [ -z "$common_name" ]; then
        echo "Missing environment variable."
        exit 1
fi

# Load server variables
. /etc/openvpn/variables

ipv6=""

# Find out if there is a specific config with fixed IPv6 for this client
if [ -f "/etc/openvpn/ccd/$common_name" ]; then
        # Get fixed IPv6 from client config file
        ipv6=$(sed -nr 's/^.*ifconfig-ipv6-push[ t]+([0-9a-fA-F:]+).*$/1/p' "/etc/openvpn/ccd/$common_name")
fi

# Get IPv6 from IPv4
if [ -z "$ipv6" ]; then
        ipp=$(echo "$ifconfig_pool_remote_ip" | cut -d. -f4)
        if ! [ "$ipp" -ge 2 -a "$ipp" -le 254 ] 2>/dev/null; then
                echo "Invalid IPv4 part."
                exit 1
        fi
        hexipp=$(printf '%x' $ipp)
        ipv6="$prefix$hexipp"
fi

# Delete proxy rule
/sbin/ip -6 neigh del proxy $ipv6 dev eno1

Both scripts use the file «/etc/openvpn/variables»:

# Subnet
prefix=XXXX:XXXX:XXXX:XXXX:2:
# netmask
prefixlen=112

I can't recall why it's written this way.

Currently, it looks strange with netmask = 112 (it should be 96).
And the prefix is strange, not corresponding to the tun0 network.
But okay, I’ll leave it as is.

cipher DES-EDE3-CBC

This is a matter of preference — I chose this method of encrypting the connection.

More details on configuring OpenVPN for IPv4.

More details on configuring OpenVPN for IPv6.

============= Postfix =============

Installing the main package:

apt-get install postfix

During installation, select «internet-site».

My «/etc/postfix/main.cf» looks like this:

smtpd_banner = $myhostname ESMTP $mail_name (Debian/GNU)
biff = no

# appending .domain is the MUA's job.
append_dot_mydomain = no

readme_directory = no

# See http://www.postfix.org/COMPATIBILITY_README.html -- default to 2 on
# fresh installs.
compatibility_level = 2

# TLS parameters
smtpd_tls_cert_file=/etc/ssl/domain1.com.2018.chained.crt
smtpd_tls_key_file=/etc/ssl/domain1.com.2018.key
smtpd_use_tls=yes
smtpd_tls_auth_only = yes
smtp_bind_address = XX.XX.XX.X0
smtp_bind_address6 = XXXX:XXXX:XXXX:XXXX:1:1:1:1

smtp_tls_security_level = may
smtp_tls_ciphers = export
smtp_tls_protocols = !SSLv2, !SSLv3
smtp_tls_loglevel = 1

smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination
myhostname = domain1.com
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
myorigin = domain1.com
mydestination = localhost
relayhost =
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128
mailbox_size_limit = 0
recipient_delimiter = +
inet_interfaces = all
inet_protocols = ipv4

internal_mail_filter_classes = bounce

# Storage type
virtual_transport = lmtp:unix:private/dovecot-lmtp
virtual_mailbox_domains = mysql:/etc/postfix/mysql-virtual-mailbox-domains.cf
virtual_mailbox_maps = mysql:/etc/postfix/mysql-virtual-mailbox-maps.cf
virtual_alias_maps = mysql:/etc/postfix/mysql-virtual-alias-maps.cf

# SMTP-Auth settings
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes
smtpd_recipient_restrictions =
        permit_sasl_authenticated,
        permit_mynetworks,
        #reject_invalid_hostname,
        #reject_unknown_recipient_domain,
        reject_unauth_destination,
        reject_rbl_client sbl.spamhaus.org,
        check_policy_service unix:private/policyd-spf

smtpd_helo_restrictions =
        #reject_invalid_helo_hostname,
        #reject_non_fqdn_helo_hostname,
        reject_unknown_helo_hostname

smtpd_client_restrictions =
        permit_mynetworks,
        permit_sasl_authenticated,
        reject_non_fqdn_helo_hostname,
        permit

# SPF
policyd-spf_time_limit = 3600

# OpenDKIM
milter_default_action = accept
milter_protocol = 6
smtpd_milters = unix:var/run/opendkim/opendkim.sock
non_smtpd_milters = unix:var/run/opendkim/opendkim.sock

# IP address per domain
sender_dependent_default_transport_maps = pcre:/etc/postfix/sdd_transport.pcre

Let's look at the details of this configuration.

smtpd_tls_cert_file=/etc/ssl/domain1.com.2018.chained.crt
smtpd_tls_key_file=/etc/ssl/domain1.com.2018.key

According to Habr users, this block contains `misinformation and incorrect theses`.Only after 8 years of my career did I start to understand how SSL works.

So I will take the liberty to describe how to use SSL (without answering the questions 'How does it work?' and 'Why does it work?').

The basis of modern encryption is the creation of a key pair (two very long strings of characters).

One key is private, and the other key is public. We keep the private key very carefully in secret. The public key is distributed to anyone who wants it.

With the public key, one can encrypt a string of text in such a way that only the owner of the private key can decrypt it.
And that's the foundation of the technology.

Step #1 — https websites.
When the browser accesses the website, it learns from the web server that the site is https and therefore requests the public key.
The web server delivers the public key. The browser uses the public key to encrypt the HTTP request and sends it.
The content of the HTTP request can only be read by someone who has the private key, which means only the server to which the request is made.
An HTTP request contains at least the URI. Therefore, if a country tries to restrict access not to the entire site, but to a specific page — it is impossible for HTTPS sites.

Step #2 — encrypted response.
The web server provides a response that can easily be read on the way.
The solution is straightforward — the browser locally generates the same pair of private-public keys for each HTTPS site.
And along with the request for the site's public key, it sends its local public key.
The web server remembers it and, when sending the HTTP response, encrypts it with that particular client's public key.
Now the HTTP response can only be decrypted by the holder of the client's browser's private key (that is, the client themselves).

Step #3 — establishing a secure connection over a public channel.
In example #2, there is a vulnerability — nothing prevents malicious parties from intercepting the HTTP request and altering the information about the public key.
Thus, the intermediary will see all content of the sent and received messages as long as the communication channel does not change.
The solution is quite simple — just send the browser's public key as a message encrypted with the web server's public key.
The web server then first sends a response of the type 'your public key is like this' and encrypts this message with the same public key.
The browser checks the response — if a message arrived saying 'your public key is like this' — then this is a 100% guarantee that the communication channel is secure.
How secure is it?
The creation of such a secure communication channel occurs at a speed of ping*2. For example, 20ms.
An attacker must either already have the private key of one of the parties or guess the private key within a few milliseconds.
Hacking one modern private key would take decades on a supercomputer.

Step #4 — a public database of public keys.
It is clear that in this whole story, there is a possibility for an attacker sitting on the communication channel between the client and the server.
The client can present itself as the server, and the server can present itself as the client. This allows for the simulation of a pair of keys in both directions.
Then the attacker will see all the traffic and will have the ability to 'edit' the traffic.
For example, change the address to which money is sent, copy the password from an online bank, or block 'undesirable' content.
To fight against such attackers, a public database with public keys for each HTTPS site was created.
Every browser 'knows' about the existence of around 200 such databases. This is pre-installed in every browser.
This 'knowledge' is supported by the public key from each certificate. This means that it is impossible to forge a connection with each specific certification authority.

Now there is a simple understanding of how to use SSL for HTTPS.
If you think about it, it becomes clear how special services can hack something in this setup. However, it will require tremendous effort.
For organizations smaller than the NSA or CIA, it is practically impossible to break through the existing level of protection even for VIPs.

I will also add about SSH connections. There are no public keys involved—so what to do? The question is resolved in two ways.
Option SSH with a password:
Upon the first connection, the SSH client must warn that there is a new public key from the SSH server.
And in subsequent connections, if you receive a warning 'new public key from SSH server'—this means someone is trying to eavesdrop.
Or during the first connection, you were eavesdropped on, but now you are communicating with the server without intermediaries.
In fact, because the fact of eavesdropping is easily, quickly, and effortlessly discovered—this attack is only used in special cases for a specific client.

Option SSH with keys:
Take a flash drive, write your private key for the SSH server onto it (there are terms and many significant nuances for this, but I am providing a basic overview, not an instruction manual).
Leave the public key on the machine where the SSH client will be and keep it secret as well.
Bring the flash drive to the server, insert it, copy the private key, and then burn the flash drive, scattering its ashes to the wind (or at least format it with zero filling).
That's it — after this operation, it will be impossible to hack such an SSH connection. Of course, in about 10 years, it may be possible to analyze the traffic on a supercomputer — but that's a separate story.

I apologize for the off-topic.

So now that we know the theory, let me explain the flow of creating an SSL certificate.

Using 'openssl genrsa', we create a private key and 'templates' for the public key.
We send the 'templates' to a third-party company, to which we pay around $9 for the simplest certificate.

A few hours later, we receive our 'public' key from this third-party company along with a set of several public keys.

The question of why we pay a third party to issue our public key is a separate one, and we won't discuss it here.

Now it’s clear what the inscription means:

smtpd_tls_key_file=/etc/ssl/domain1.com.2018.key

In the folder '/etc/ssl', all files for SSL issues are stored.
domain1.com — the name of the domain.
2018 — the year the keys were created.
'key' — denotes that the file is a private key.

And the meaning of this file is:

smtpd_tls_cert_file=/etc/ssl/domain1.com.2018.chained.crt
domain1.com — the name of the domain.
2018 — the year the keys were created.
chained — denotes that this is a chain of public keys (the first one is our public key, and the rest are what came from the company that issued the public key).
crt — denotes that this is a ready certificate (the public key with technical explanations).

smtp_bind_address = XX.XX.XX.X0
smtp_bind_address6 = XXXX:XXXX:XXXX:XXXX:1:1:1:1

This setting is not used in this case but is written for example.

Because an error in this parameter will lead to spam being sent from your server (without your consent).

Then you'll have to prove to everyone that you are not at fault.

recipient_delimiter = +

Many may not know, but this is the standard character for ranking emails, and it is supported by most modern mail servers.

For example, if you have a mailbox 'username@gmail.com', try sending to 'username+spam@gmail.com' — see what happens.

inet_protocols = ipv4

This may be confusing.

But it's not for nothing. Each new domain defaults to IPv4 only, then I enable IPv6 for each one individually.

virtual_transport = lmtp:unix:private/dovecot-lmtp
virtual_mailbox_domains = mysql:/etc/postfix/mysql-virtual-mailbox-domains.cf
virtual_mailbox_maps = mysql:/etc/postfix/mysql-virtual-mailbox-maps.cf
virtual_alias_maps = mysql:/etc/postfix/mysql-virtual-alias-maps.cf

Here we specify that all incoming mail goes to Dovecot.
And the rules for domain, mailbox, alias — refer to the database.

/etc/postfix/mysql-virtual-mailbox-domains.cf

user = usermail
password = mailpassword
hosts = 127.0.0.1
dbname = servermail
query = SELECT 1 FROM virtual_domains WHERE name='%s'

/etc/postfix/mysql-virtual-mailbox-maps.cf

user = usermail
password = mailpassword
hosts = 127.0.0.1
dbname = servermail
query = SELECT 1 FROM virtual_users WHERE email='%s'

/etc/postfix/mysql-virtual-alias-maps.cf

user = usermail
password = mailpassword
hosts = 127.0.0.1
dbname = servermail
query = SELECT destination FROM virtual_aliases WHERE source='%s'

# SMTP-Auth settings
smtpd_sasl_type = dovecot
smtpd_sasl_path = private/auth
smtpd_sasl_auth_enable = yes

Now postfix knows that it can only accept mail for further delivery with authentication from dovecot.

I really don't understand why this is duplicated here. We already specified everything necessary in 'virtual_transport'.

But the postfix system is very old — these are probably workarounds from old times.

smtpd_recipient_restrictions =
        ...

smtpd_helo_restrictions =
        ...

smtpd_client_restrictions =
        ...

This needs to be configured differently for each mail server.

I have 3 mail servers at my disposal, and these settings are very different due to varying usage requirements.

Configuration needs to be done carefully — otherwise, spam will flood in or even worse — spam will flood out from you.

# SPF
policyd-spf_time_limit = 3600

Configuration for a plugin related to checking SPF of incoming emails.

# OpenDKIM
milter_default_action = accept
milter_protocol = 6
smtpd_milters = unix:var/run/opendkim/opendkim.sock
non_smtpd_milters = unix:var/run/opendkim/opendkim.sock

Configuration that all outgoing emails must be supplied with a DKIM signature.

# IP address per domain
sender_dependent_default_transport_maps = pcre:/etc/postfix/sdd_transport.pcre

This is a key detail in the routing of emails when sending emails from PHP scripts.

File '/etc/postfix/sdd_transport.pcre':

/^www-domain1@domain1.com$/ domain1:
/^www-domain2@domain1.com$/ domain2:
/^www-domain3@domain1.com$/ domain3:
/@domain1.com$/             domain1:
/@domain2.com$/             domain2:
/@domain3.com$/             domain3:

To the left — regular expressions. To the right — a label that marks the email.
Postfix, according to the label, will consider a few more configuration lines for that specific email.

How exactly postfix will be reconfigured for a particular email will be indicated in 'master.cf'.

Lines 4, 5, 6 — they are crucial. The label assigned corresponds to the domain name from which we are sending the email.
But in older PHP scripts, the 'from' field is not always specified. That's when the username comes to the rescue.

The article is already extensive — I would prefer not to get sidetracked on configuring nginx+fpm.

In short — we assign a different linux user owner for each site. Accordingly, its own fpm pool.

The fpm pool uses any PHP version (which is great because different sites on the same server can use different PHP versions without any issues, including different php.ini).

So a particular linux user 'www-domain2' has a site domain2.com. This site has code for sending emails without specifying the from field.

Even in this case, emails will be sent correctly and will never end up in spam.

My '/etc/postfix/master.cf' looks like this:

...
smtp      inet  n       -       y       -       -       smtpd
  -o content_filter=spamassassin
...
submission inet n       -       y       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_client_restrictions=permit_sasl_authenticated,reject
...
policyd-spf  unix  -       n       n       -       0       spawn
    user=policyd-spf argv=/usr/bin/policyd-spf

spamassassin unix -     n       n       -       -       pipe
    user=spamd argv=/usr/bin/spamc -f -e
    /usr/sbin/sendmail -oi -f ${sender} ${recipient}
...
domain1  unix -       -       n       -       -       smtp
   -o smtp_bind_address=XX.XX.XX.X1
   -o smtp_helo_name=domain1.com
   -o inet_protocols=all
   -o smtp_bind_address6=XXXX:XXXX:XXXX:XXXX:1:1:1:1
   -o syslog_name=postfix-domain1

domain2  unix -       -       n       -       -       smtp
   -o smtp_bind_address=XX.XX.XX.X5
   -o smtp_helo_name=domain2.com
   -o inet_protocols=all
   -o smtp_bind_address6=XXXX:XXXX:XXXX:XXXX:1:2:1:1
   -o syslog_name=postfix-domain2

domain3  unix -       -       n       -       -       smtp
   -o smtp_bind_address=XX.XX.XX.X2
   -o smtp_helo_name=domain3
   -o inet_protocols=all
   -o smtp_bind_address6=XXXX:XXXX:XXXX:XXXX:1:1:5:1
   -o syslog_name=postfix-domain3

The file is not fully included — it is already very large.
I only marked what has been changed.

smtp      inet  n       -       y       -       -       smtpd
  -o content_filter=spamassassin
...
spamassassin unix -     n       n       -       -       pipe
    user=spamd argv=/usr/bin/spamc -f -e
    /usr/sbin/sendmail -oi -f ${sender} ${recipient}

These are the settings related to SpamAssassin, more on that later.

submission inet n       -       y       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_client_restrictions=permit_sasl_authenticated,reject

We allow connection to the mail server through port 587.
It is essential to authenticate.

policyd-spf  unix  -       n       n       -       0       spawn
    user=policyd-spf argv=/usr/bin/policyd-spf

We enable SPF checks.

apt-get install postfix-policyd-spf-python

We will install the package for the above SPF checks.

domain1  unix -       -       n       -       -       smtp
   -o smtp_bind_address=XX.XX.XX.X1
   -o smtp_helo_name=domain1.com
   -o inet_protocols=all
   -o smtp_bind_address6=XXXX:XXXX:XXXX:XXXX:1:1:1:1
   -o syslog_name=postfix-domain1

And this is the most interesting part. This allows sending emails for a specific domain from a specific IPv4/IPv6 address.

This is done for rDNS. rDNS is obtaining some string from an IP address.
And for emails, this capability is used to confirm that the HELO matches the rDNS of the address from which the email was sent.

If the HELO does not match the email domain from which the letter was sent, spam points are assigned.

HELO does not match rDNS — many spam points are assigned.
Accordingly, each domain should have its own IP address.
For OVH — in the console, there is an option to specify rDNS.
For tech.ru — the issue is resolved through support.
For AWS — the issue is resolved through support.
«inet_protocols» and «smtp_bind_address6» enable IPv6 support.
rDNS must also be set up for IPv6.
«syslog_name» is for easier log reading.

Purchasing certificates I recommend doing it here.

Postfix+Dovecot setup here.

Setting up SPF.

============= Dovecot =============

apt-get install dovecot-imapd dovecot-pop3d dovecot-lmtpd dovecot-mysql dovecot-antispam

MySQL setup, we install the packages ourselves.

File «/etc/dovecot/conf.d/10-auth.conf»

disable_plaintext_auth = yes
auth_mechanisms = plain login

Authorization is only in encrypted form.

File «/etc/dovecot/conf.d/10-mail.conf»

mail_location = maildir:/var/mail/vhosts/%d/%n

Here we will specify the storage location for emails.

I want them to be stored in files and grouped by domain.

File «/etc/dovecot/conf.d/10-master.conf»

service imap-login {
  inet_listener imap {
    port = 0
  }
  inet_listener imaps {
    address = XX.XX.XX.X1, XX.XX.XX.X2, XX.XX.XX.X5, [XXXX:XXXX:XXXX:XXXX:1:1:1:1], [XXXX:XXXX:XXXX:XXXX:1:2:1:1], [XXXX:XXXX:XXXX:XXXX:1:1:5:1]
    port = 993
    ssl = yes
  }
}
service pop3-login {
  inet_listener pop3 {
    port = 0
  }
  inet_listener pop3s {
    address = XX.XX.XX.X1, XX.XX.XX.X2, XX.XX.XX.X5, [XXXX:XXXX:XXXX:XXXX:1:1:1:1], [XXXX:XXXX:XXXX:XXXX:1:2:1:1], [XXXX:XXXX:XXXX:XXXX:1:1:5:1]
    port = 995
    ssl = yes
  }
}
service lmtp {
  unix_listener /var/spool/postfix/private/dovecot-lmtp {
    mode = 0600
    user = postfix
    group = postfix
  }
}
service imap {
}
service pop3 {
}
service auth {
  unix_listener auth-userdb {
    mode = 0600
    user = vmail
  }

  unix_listener /var/spool/postfix/private/auth {
    mode = 0666
    user = postfix
    group = postfix
  }
  user = dovecot
}
service auth-worker {
  user = vmail
}
service dict {
  unix_listener dict {
  }
}

This is the main configuration file for Dovecot.
Here we disable unencrypted connections.
And enable secure connections.

File «/etc/dovecot/conf.d/10-ssl.conf»

ssl = required
ssl_cert = </etc/nginx/ssl/domain1.com.2018.chained.crt
ssl_key = </etc/nginx/ssl/domain1.com.2018.key
local XX.XX.XX.X5 {
  ssl_cert = </etc/nginx/ssl/domain2.com.2018.chained.crt
  ssl_key =  </etc/nginx/ssl/domain2.com.2018.key
}

Configuring SSL. We specify that SSL is required.
And the certificate itself. An important detail is the «local» directive. It indicates which SSL certificate to use when connecting to a specific local IPv4.

By the way, IPv6 is not configured here; I'll fix this oversight later.
XX.XX.XX.X5 (domain2) — no certificate. For client connections, domain1.com needs to be specified.
XX.XX.XX.X2 (domain3) — certificate exists; for client connections, either domain1.com or domain3.com can be specified.

File «/etc/dovecot/conf.d/15-lda.conf»

protocol lda {
  mail_plugins = $mail_plugins sieve
}

This will be needed later for SpamAssassin.

File «/etc/dovecot/conf.d/20-imap.conf»

protocol imap {
  mail_plugins = $mail_plugins antispam
}

This is the antispam plugin. It is needed for training SpamAssassin when moving to/from the "Spam" folder.

File «/etc/dovecot/conf.d/20-pop3.conf»

protocol pop3 {
}

There is just such a file.

File «/etc/dovecot/conf.d/20-lmtp.conf»

protocol lmtp {
  mail_plugins = $mail_plugins sieve
  postmaster_address = admin@domain1.com
}

LMTP configuration.

File "/etc/dovecot/conf.d/90-antispam.conf"

plugin {
  antispam_backend = pipe
  antispam_trash = Trash;trash
  antispam_spam = Junk;Spam;SPAM
  antispam_pipe_program_spam_arg = --spam
  antispam_pipe_program_notspam_arg = --ham
  antispam_pipe_program = /usr/bin/sa-learn
  antispam_pipe_program_args = --username=%Lu
}

SpamAssassin training settings during moving to/from the "Spam" folder.

File "/etc/dovecot/conf.d/90-sieve.conf"

plugin {
  sieve = ~/dovecot.sieve
  sieve_dir = ~/sieve
  sieve_after = /var/lib/dovecot/sieve/default.sieve
}

File that specifies what to do with incoming emails.

File "/var/lib/dovecot/sieve/default.sieve"

require ["fileinto", "mailbox"];

if header :contains "X-Spam-Flag" "YES" {
        fileinto :create "Spam";
}

The file needs to be compiled: "sievec default.sieve".

File "/etc/dovecot/conf.d/auth-sql.conf.ext"

passdb {
  driver = sql
  args = /etc/dovecot/dovecot-sql.conf.ext
}
userdb {
  driver = static
  args = uid=vmail gid=vmail home=/var/mail/vhosts/%d/%n
}

Specification of SQL files for authorization.
The file itself — as a means of authorization.

File "/etc/dovecot/dovecot-sql.conf.ext"

driver = mysql
connect = host=127.0.0.1 dbname=servermail user=usermail password=password
default_pass_scheme = SHA512-CRYPT
password_query = SELECT email as user, password FROM virtual_users WHERE email='%u';

This corresponds to similar settings for postfix.

File "/etc/dovecot/dovecot.conf"

protocols = imap lmtp pop3
listen = *, ::
dict {
}
!include conf.d/*.conf
!include_try local.conf

Main configuration file.
It is important that we specify/add the protocols here.

============= SpamAssassin =============

apt-get install spamassassin spamc

Let's install the packages.

adduser spamd --disabled-login

Let's add the user under whose credentials.

systemctl enable spamassassin.service

Enable the auto-start of the SpamAssassin service on boot.

File "/etc/default/spamassassin":

CRON=1

Enables automatic rule updates by default.

File "/etc/spamassassin/local.cf":

report_safe 0

use_bayes          1
bayes_auto_learn   1
bayes_auto_expire  1
bayes_store_module Mail::SpamAssassin::BayesStore::MySQL
bayes_sql_dsn      DBI:mysql:sa:localhost:3306
bayes_sql_username sa
bayes_sql_password password

You need to create a MySQL database "sa" with the user "sa" and password "password" (replace with something more appropriate).

report_safe — instead of the email, a report about the spam email will be sent.
use_bayes — this is the machine learning settings for SpamAssassin.

Other SpamAssassin settings were previously applied according to the article.

General settings for SpamAssassin..
About moving new spam emails to the IMAP folder "Spam"..
About the simple integration of Dovecot + SpamAssassin..
I recommend reading the theory of SpamAssassin training when moving emails in IMAP folders (and do not recommend applying it)..

============= Community Engagement =============

I would also like to throw an idea into the community about how to enhance the security level of sent emails. Since I've delved deeply into the topic of mail.

So that the user can create a pair of keys on their client (Outlook, Thunderbird, browser plugin, etc.). A public key to be sent to DNS. A private key to be stored on the client. Mail servers would be able to apply the public key for sending to a specific recipient.

And to protect against spam with such emails (yes, the mail server won't be able to look at the content) — we need to introduce 3 rules:

  1. Mandatory real DKIM signature, mandatory SPF, mandatory rDNS.
  2. A neural network for training anti-spam + a database for it on the client side.
  3. The encryption algorithm should be such that the sending side spends 100 times more CPU power on encryption than the receiving side.

In addition to public emails — develop a standard email proposal to 'start secure correspondence'. One user (email account) sends another email account a letter with an attachment. The email contains a proposal text to start a secure communication channel for correspondence and the public key of the email account owner (while the private key remains on the client side).

It is even possible to create a pair of keys specifically for each correspondence. The recipient user can accept this proposal and send their public key (also made specifically for this correspondence). Then the first user sends a control service email (encrypted with the second user's public key) — upon receiving which the second user can consider the established communication channel reliable. Next, the second user sends a control email — and then the first user can also consider the established channel secure.

To combat key interception along the way — the protocol should provide for the possibility of transferring at least one public key using a flash drive.

And most importantly — for all of this to work (the question 'who will pay for this?'):
To enter email certificates costing from $10 for 3 years, which will allow the sender to specify in DNS that 'my public keys are over there.' This will enable the initiation of a secure connection, while also accepting such connections for free.
Gmail is finally monetizing its users. For $10 in 3 years — the right to create secure communication channels.

============= Conclusion =============

To test the entire article, I planned to rent a dedicated server for a month and purchase a domain with an SSL certificate.

However, due to life circumstances, this issue dragged on for 2 months.
And when I finally had some free time again — I decided to publish the article as is, rather than risk that the publication would be delayed even further.

If there are enough questions like 'this part is not described in enough detail' — then I will probably find the resources to get a dedicated server with a new domain and a new SSL certificate and describe everything in more detail, importantly — to uncover all the missed important details.

I would also like to receive feedback on the idea of email certificates. If the idea is well received — I will try to find the strength to draft an RFC.

When copying large sections of the article — please reference this article.
When translating into any other language — please reference this article.
I will try to translate it into English myself and leave cross-references.


Source: habr.com

Buy reliable website hosting with DDoS protection, VPS VDS servers 🔥 Buy reliable website hosting with DDoS protection, VPS VDS servers | ProHoster