How to create ceph on Centos 8 Stream

I assume, that we have working Centos 8 Stream system. So, in this example, we will have three systems (nodes), with identical HW resources (4 GB ram, 4 vCPU, two NICs – one internal for ceph and one for world, and 10 TB spin-up hdd). In this article, every command must be run on all nodes. Public network is and Ceph separate network is

Setting up time

As the first step, we must set up a time, I use chrony:

dnf install chrony -y
systemctl enable chronyd
timedatectl set-timezone Europe/Bratislava

Now, edit some variables in configurations file for chronyd. Add some servers from pool, and edit local subnets, where we delived time:

vim /etc/chrony.conf

pool iburst
pool iburst
pool iburst

Now start/restart our service, and check, if it is working:

systemctl restart chronyd
systemctl status chronyd.service
chronyc sources

Create hostnames, ssh rsa-keys, update and install som packages

Now, we must edit on all nodes our hostnames, set it permanent:

hostnamectl set-hostname ceph1

Now, add all hostnames, and IPs to file /etc/hosts:

tee -a /etc/hosts<<EOF    ceph1    ceph2    ceph3    ceph1-cluster    ceph2-cluster    ceph3-cluster


Now, create rsa-key pair, for password-less connect to and from each node:

ssh-keygen -t rsa -b 4096 -C "ceph1"

-b bits. Number of bits in the key to create
-t type. Specify type of key to create
-C comment

And copy it to other nodes:

for host in ceph1 ceph2 ceph3; do
 ssh-copy-id root@$host

Now update and install packages:

dnf update -y
dnf install git vim bash-completion python3-pip

Preparing for ceph

Now, install epel repository and enable powertools:

dnf -y install dnf-plugins-core
dnf -y install
dnf config-manager --set-enabled powertools

dnf repolist
repo id            repo name
appstream          CentOS Stream 8 - AppStream
epel               Extra Packages for Enterprise Linux 8 - x86_64
epel-modular       Extra Packages for Enterprise Linux Modular 8 - x86_64
epel-next          Extra Packages for Enterprise Linux 8 - Next - x86_64
extras             CentOS Stream 8 - Extras
powertools         CentOS Stream 8 - PowerTools

Clone Ceph Ansible repository:

cd /root/
git clone

Choose ceph-ansible branch you wish to use. The command Syntax is: git checkout $branch

I’ll switch to stable-5.0 which supports Ceph octopus version.

cd ceph-ansible
git checkout stable-5.0

pip3 install setuptools-rust
pip3 install wheel
pip3 install --upgrade pip

pip3 install -r requirements.txt
echo "PATH=\$PATH:/usr/local/bin" >>~/.bashrc
source ~/.bashrc

Confirm Ansible version installed.

ansible --version
ansible 2.9.26
  config file = /root/ceph-ansible/ansible.cfg
  configured module search path = ['/root/ceph-ansible/library']
  ansible python module location = /usr/local/lib/python3.6/site-packages/ansible
  executable location = /usr/local/bin/ansible
  python version = 3.6.8 (default, Sep 10 2021, 09:13:53) [GCC 8.5.0 20210514 (Red Hat 8.5.0-3)]

Now, we find, which OSD (spin-up disks) are ready for us. In each my node, there is free disk /dev/sda. Look via lsblk:

sda                       8:0    0 10.7T  0 disk 
sr0                      11:0    1 1024M  0 rom  
vda                     252:0    0   32G  0 disk 
├─vda1                  252:1    0  512M  0 part /boot
└─vda2                  252:2    0 31.5G  0 part 
  ├─centos--vg0-root    253:0    0    3G  0 lvm  /
  ├─centos--vg0-swap    253:1    0    1G  0 lvm  [SWAP]
  ├─centos--vg0-tmp     253:2    0  512M  0 lvm  /tmp
  ├─centos--vg0-var_log 253:3    0  512M  0 lvm  /var/log
  ├─centos--vg0-var     253:4    0    3G  0 lvm  /var
  └─centos--vg0-home    253:5    0    2G  0 lvm  /home

Now, we are ready for installation of Ceph

Deploy Ceph Octopus (15) Cluster on CentOS 8 stream

Now, we are play some things 🙂 The first node (ceph1) I use as admin node for installation. Configure Ansible Inventory and Playbook files. Create Ceph Cluster group variables file on the admin node

cd /root/ceph-ansible
cp group_vars/all.yml.sample  group_vars/all.yml
vim group_vars/all.yml

And edit some variables of your Ceph cluster, as you fit:

cluster: ceph

# Inventory host group variables
mon_group_name: mons
osd_group_name: osds
rgw_group_name: rgws
mds_group_name: mdss
nfs_group_name: nfss
rbdmirror_group_name: rbdmirrors
client_group_name: clients
iscsi_gw_group_name: iscsigws
mgr_group_name: mgrs
rgwloadbalancer_group_name: rgwloadbalancers
grafana_server_group_name: grafana-server

# Firewalld / NTP
configure_firewall: True
ntp_service_enabled: true
ntp_daemon_type: chronyd

# Ceph packages
ceph_origin: repository
ceph_repository: community
ceph_repository_type: cdn
ceph_stable_release: octopus

# Interface options
monitor_interface: ens18
radosgw_interface: ens18

dashboard_enabled: True
dashboard_protocol: http
dashboard_admin_user: admin
dashboard_admin_password: strongpass

grafana_admin_user: admin
grafana_admin_password: strongpass

Now, set your OSDs. Create a new ceph nodes ansible inventory. Properly set your inventory file. Below is my inventory. Modify inventory groups the way you want services installed in your cluster nodes.

vim hosts

# Ceph admin user for SSH and Sudo

# Ceph Monitor Nodes

# MDS Nodes


# Manager Daemon Nodes

# set OSD (Object Storage Daemon) Node

# Grafana server

Create Playbook file by copying a sample playbook at the root of the ceph-ansible project called site.yml.sample.

cp site.yml.sample site.yml 

Run Playbook.

ansible-playbook -i hosts site.yml 

If installation was successful, a health check should return OK or minimal WARN.

# ceph -s
    id:     dcfd26f5-49e9-4256-86c2-a5a0deac7b54
    health: HEALTH_WARN
            mons are allowing insecure global_id reclaim
    mon: 3 daemons, quorum eu-ceph1,eu-ceph2,eu-ceph3 (age 67m)
    mgr: ceph2(active, since 55m), standbys: ceph3, ceph1
    mds: cephfs:1 {0=ceph1=up:active} 2 up:standby
    osd: 3 osds: 3 up (since 60m), 3 in (since 60m)
    rgw: 3 daemons active (ceph1.rgw0, ceph2.rgw0, ceph3.rgw0)
  task status:
    pools:   7 pools, 169 pgs
    objects: 215 objects, 11 KiB
    usage:   3.1 GiB used, 32 TiB / 32 TiB avail
    pgs:     169 active+clean

This is a screenshot of my installation output once it has been completed.

As you see, I have warning: mons are allowing insecure global_id reclaim

So, silent it, as you fit it, or fix…

ceph config set mon mon_warn_on_insecure_global_id_reclaim_allowed false
Total Page Visits: 88929 - Today Page Visits: 22

How to create a router from Centos 8 Stream

Today, I need to rebuild and create a new one linux router. It will work with many networks, there will be DHCP and DNS server as well.

So, at first, we will secure our server on service: ssh. I assume, that you already have enabled firewalld and public zone, only for sshd service. And internal network with dhcp or anything else.

SSHD deamon

At first, we configure our ssh service to be secure. Edit ssh configuration file and adjust there variables:

vim /etc/ssh/sshd_config
Port 22
Protocol 2
PermitRootLogin no
PubkeyAuthentication yes
PermitEmptyPasswords no
PasswordAuthentication no
AllowUsers bob

Now, we create on our client machine (in my place Ubuntu desktop PC) a new rsa-key pair for ssh authentication. You can set some passphrase, but it is you opinion. In quotas, there is a comment for there key pair.

ssh-keygen -t rsa -b 4096 -C ""
-b bits. Number of bits in the key to create
-t type. Specify type of key to create
-C comment

When you had these key pairs generated (under ~/.ssh/) like: id_rsa and id_rsa_pub, you can adjust your server, for accepting these key pair. Now, you must authorize to your server by password (last time).

ssh-copy-id bob@IP-OF-SERVER

If everything is OK, now you can restart your sshd service on server, to load new configurations:

systemctl restart sshd.service

And we can test, that other users is NOT allowed, and there is a need for key authentication:

ssh Alice@
  Alice@ Permission denied (publickey,gssapi-keyex,gssapi-with-mic).

But Bob is allowed:

ssh bob@
   Last login: Tue Aug 31 14:47:13 2021 from...

Setting up public interface

We use nmcli (command-line tool for controlling NetworkManager) for watching out of our interfaces:

nmcli device status
ens1   ethernet  connected    ens1       
ens2    ethernet  unmanaged    --         
ens3    ethernet  unmanaged    --         
ens4   ethernet  unmanaged    --         
ens5   ethernet  unmanaged    --         
lo      loopback  unmanaged    -- 

Unmanaged interfaces are edited here:

vim /etc/NetworkManager/conf.d/99-unmanaged-devices.conf

And our ens1 is in config below. UUID we can generate with command:

uuidgen ens1
cat /etc/sysconfig/network-scripts/ifcfg-ens1 
#ens1 - Internal Subnet

Now, we can configure our public interface. In our example, it will be interface ens2. So, we can copy our first network-script for interface ens2 and edit it. First, generate uuid for this interface. Then adjust your public IP numbers.

uuidgen ens2
cp /etc/sysconfig/network-scripts/ifcfg-ens1 /etc/sysconfig/network-scripts/ifcfg-ens2
vim /etc/sysconfig/network-scripts/ifcfg-ens2


nmcli device status
ens1   ethernet  connected    ens1       
ens2    ethernet  connected    ens2  

Now, after some time, we can see many ssh attacks to our system. We can make more things. One is, change the port for ssh daemon. Or I use fail2ban services, for ban IP, that try to hack our server. Like:

Aug 31 13:50:45 r3 sshd[3551]: Invalid user support from port 17306
Aug 31 13:50:48 r3 sshd[3553]: Invalid user geral from port 17006
Aug 31 13:50:49 r3 sshd[3555]: Invalid user omega from port 57344
Aug 31 13:50:51 r3 sshd[3557]: Invalid user matt from port 49776
Aug 31 13:50:57 r3 sshd[3559]: Invalid user rebecca from port 38964
Aug 31 13:51:02 r3 sshd[3561]: Invalid user admin from port 32926
Aug 31 13:51:24 r3 sshd[3564]: Invalid user support from port 49868
Aug 31 13:55:38 r3 sshd[3603]: Invalid user vasilias from port 54654
Aug 31 13:55:42 r3 sshd[3605]: Invalid user admin from port 54656
Aug 31 13:59:53 r3 sshd[3734]: Invalid user user from port 45564

Or we can count it (after two hours). We had 644 ssh login attempts to our server:

cat /var/log/secure | grep Invalid | wc -l

So, find I will install fail2ban services, for ban these IPs…

dnf install -y fail2ban-firewalld.noarch

Now, we must create a new jail.local, where we define actions for those IPs and services. So, create this file and add some variables. If you wish, adjust by your needs:

vim /etc/fail2ban/jail.local

# Ban IP/hosts for 24 hour ( 24h*3600s = 86400s):
bantime = 86400
# An ip address/host is banned if it has generated "maxretry" during the last "findtime" seconds.
findtime = 900
maxretry = 2

# Enable sshd protection
enabled = true

Now, we can start/restart fail2ban services and view its status. And next, use fail2ban client to view detailed status:

systemctl restart fail2ban
systemctl status fail2ban

fail2ban-client status
fail2ban-client status sshd

Now, we must see some IPs, that are already banned by our fail2ban. If you wish to manually ban/unban IP, lets do this:

fail2ban-client unban
fail2ban-client set sshd banip

Or you can set your trusted IPs to whitelist:

vim /etc/fail2ban/jail.conf

ignoreip =

Time server – chronyd

Now, we install chronyd service, for obtain and distributing time from world to our subnets. So, install chronyd, and set localisation to you system

dnf install chrony -y
systemctl enable chronyd
timedatectl set-timezone Europe/Bratislava

Now, edit some variables in configurations file for chronyd. Add some servers from pool, and edit local subnets, where we delived time:

vim /etc/chrony.conf

pool iburst
pool iburst
pool iburst

Now start/restart our service, and check, if it is working:

systemctl restart chronyd
systemctl status chronyd.service
chronyc sources

IPV4 forwarding

As the first things, we must check, if our system has enabled IP forwarding in kernel. Check it out:

sysctl -a | grep ip_forward

net.ipv4.ip_forward = 1

If not, enable it:

sysctl -w net.ipv4.ip_forward=1

vim /etc/sysctl.d/99-sysctl.conf

Setting up nftables

In out environment, we use nftables. It is default backend firewall module in Centos 8/ RHEL 8. It is already managed by firewalld, but we gonna use native nft command. The nftables is able to collapse firewall management for IPv4, IPv6 and bridging into the single command line utility: nft.

So, at the beginning (or before run our later nft script), we disable firewalld utility:

systemctl disable --now firewalld
systemctl mask firewalld

Creating Tables and Chains

Rebooting the system after disabling firewalld will ensure that we have no remnants of the tables, chains and rules added by firewalld. We start up with nothing in-place. Unlike iptables, nftables does not have any tables or chains created by default. Managing nftables natively requires us to create these objects:

protocol family: At the top level of the configuration tree we have protocol families: ip, ip6, arp, bridge, netdev and inet. Using inet we can cater for both IPv4 and IPv6 rules in the one table.

table: Below the protocol family we define tables to group chains together. The default table was the filter table in iptables but we also had other tables such as the nat table.

chain: A chain presents a set of rules that are read from the top down. We stop reading once we match a rule. When creating a chain in nftables we can specify the type of chain – we use the filter type, the hook – we will be working with input hook and a priority. The lower the number the higher the priority and the highest priority chain takes precedence. We will also specify the default policy which defines the action if no rule is met.

To list existing tables, we shouldn’t have any, we use the following:

nft list tables

Now, we create s script file, in which will be all our rules. So, you have to do some learning about nft. Here you are the basic rules:

cat /root/

# the executable for nftables

# wan and lan ports. Home routers typically have two ports minimum.

# flush/reset rules
${nft} flush ruleset

#create tables called "filter" for ipv4 and ipv6
${nft} add table ip filter
${nft} add table ip6 filter

# one more table called 'nat' for our NAT/masquerading
${nft} add table nat

#create chains
${nft} add chain filter input { type filter hook input priority 0 \; }
${nft} add chain filter output {type filter hook output priority 0 \; }
${nft} add chain filter forward {type filter hook forward priority 0 \; }
${nft} add chain filter postrouting {type filter hook postrouting priority 0 \; }
${nft} add chain nat postrouting {type nat hook postrouting priority 100 \; }

# and for ipv6
${nft} add chain ip6 filter input { type filter hook input priority 0 \; }
${nft} add chain ip6 filter output {type filter hook output priority 0 \; }
${nft} add chain ip6 filter forward {type filter hook forward priority 0 \; }
${nft} add chain ip6 filter postrouting {type filter hook postrouting priority 0 \; }
${nft} add chain ip6 filter nat {type nat hook postrouting priority 100 \; }


#forward traffic from WAN to LAN if related to established context
${nft} add rule filter forward iif $wan oif $lan ct state { established, related } accept

#forward from LAN to WAN always
${nft} add rule filter forward iif $lan oif $wan accept

#drop everything else from WAN to LAN
${nft} add rule filter forward iif $wan oif $lan counter drop

#ipv6 just in case we have this in future.
${nft} add rule ip6 filter forward iif $wan6 oif $lan ct state { established,related } accept
${nft} add rule ip6 filter forward iif $wan6 oif $lan icmpv6 type echo-request accept

#forward ipv6 from LAN to WAN.
${nft} add rule ip6 filter forward iif $lan oif $wan6 counter accept

#drop any other ipv6 from WAN to LAN
${nft} add rule filter forward iif $wan6 oif $lan counter drop

${nft} add rule filter input ct state { established, related } accept

# always accept loopback
${nft} add rule filter input iif lo accept
# comment next rule to disallow ssh in
${nft} add rule filter input tcp dport ssh counter log accept

#accept DNS, SSH, from LAN
${nft} add rule filter input iif $lan tcp dport { 53, 22 } counter log accept
#accept DNS on LAN
${nft} add rule filter input iif $lan udp dport { 53 } accept

#accept ICMP on the LAN,WAN
${nft} add rule filter input iif $lan ip protocol icmp accept
${nft} add rule filter input iif $wan ip protocol icmp accept

${nft} add rule filter input counter drop

################# ipv6 ###################
${nft} add rule ip6 filter input ct state { established, related } accept
${nft} add rule ip6 filter input iif lo accept
#uncomment next rule to allow ssh in over ipv6
#${nft} add rule ip6 filter input tcp dport ssh counter log accept

${nft} add rule ip6 filter input icmpv6 type { nd-neighbor-solicit, echo-request, nd-router-advert, nd-neighbor-advert } accept

${nft} add rule ip6 filter input counter drop

# allow output from us for new, or existing connections.
${nft} add rule filter output ct state { established, related, new } accept

# Always allow loopback traffic
${nft} add rule filter output iif lo accept

${nft} add rule ip6 filter output ct state { established, related, new } accept
${nft} add rule ip6 filter output oif lo accept

${nft} add rule nat postrouting masquerade

So, this script can be run by:

sh /root/

And now, we can view our filter and rules:

nft list tables
nft list table filter

This is only for this session, and after reboot, there will be empty list. To ensure, that filter persist reboot, run following:

nft list ruleset > /etc/sysconfig/nftables.conf
systemctl enable nftables.service
nft list table inet filter

Install and create own DNS resolver

Now we set up a local DNS resolver on CentOS 8/RHEL 8, with the widely-used BIND9 DNS software.

Be aware that a DNS server can also called a name server. Examples of DNS resolver are (Google public DNS server) and (Cloudflare public DNS server).

Normally, your computer or router uses your ISP’s DNS resolver to query domain names in order to get an IP address. Running your own local DNS resolver can speed up DNS lookups, because

  1. The local DNS resolver only listens to your DNS requests and does not answer other people’s DNS requests, so you have a much higher chance of getting DNS responese directly from the cache on the resolver.
  2. The network latency between your computer and DNS resolver is eliminated (almost zero), so DNS queries can be sent to root DNS servers more quickly.

If you own a website and want your own DNS server to handle name resolution for your domain name instead of using your domain registrar’s DNS server, then you will need to set up an authoritative DNS server, which is different than a DNS resolver. BIND can act as an authoritative DNS server and a DNS resolver at the same time

BIND (Berkeley Internet Name Domain) is an open-source DNS server software widely used on Unix/Linux due to it’s stability and high quality.

So, install it first, check version and start it. The BIND daemon is called named.

dnf update
dnf install bind bind-utils

named -v
    BIND 9.11.26-RedHat-9.11.26-6.el8 (Extended Support Version) <id:3ff8620>

systemctl start named
systemctl enable named

Usually, DNS queries are sent to the UDP port 53. The TCP port 53 is for responses size larger than 512 bytes. You must adjust your firewall to allow tcp and udp packets on port 53.

We can check the status of the BIND name server.:

rndc status

Configurations for a Local DNS Resolver

Out of the box, the BIND9 server on CentOS/RHEL provides recursive service for localhost only. Outside queries will be denied. Edit the BIND main configuration file:

vim /etc/named.conf

In the options clause, you can find the following two lines:

listen-on port 53 {; };
listen-on-v6 port 53 { ::1; };

This makes named listen on localhost only. If you want to allow clients in the same network to query domain names, then comment out these two lines. (add double slashes at the beginning of each line)

// listen-on port 53 {; };
// listen-on-v6 port 53 { ::1; };

Find and adjust these variables:

allow-query { localhost;; };
recursion yes;
 // hide version number from clients for security reasons.
 version "not currently available";
 // enable the query log
 querylog yes;

Now, check our config (silent output if is OK), and restart named:

systemctl restart named

Now, you have enabled DNS resolver.

Create own DNS zone, dnssec signed

If you want your own DNS server for your domain, accessible from world and you don’t want you domain provider, to be a DNS server for this domain, let read next.

First, create a new database file for this domain:

vim /var/named/

; BIND data file for
$TTL 604800
@       IN      SOA        (
        2021090608      ; Serial
        3600            ; Refresh
        86400           ; Retry
        2419200         ; Expire
        3600    )       ; Default TTL

        IN      NS

@	IN	A
ns1     IN      A
www	IN	CNAME	ns1

We have three dns names, that direct to our domain (www, ns1 and the domain itself: Now, we must edit the main configuration file for named, and add point to this zone/domain/file. And add som variables for dnssec. Now we comment out previously commented two lines and edit some variables:

vim /etc/named.conf

        listen-on port 53 { any; };
        listen-on-v6 port 53 { any; };
        allow-query { any; };
        dnssec-enable yes;
        dnssec-validation yes;
        recursion no;

zone    "" IN {
        type master;
        file "";
        allow-transfer { none; };

Now, check the config and restart named:

named-checkzone /var/named/
systemctl restart named

Now, we can reach our domain from world, if domain registrator set NameServer ( to point to our public IP and let us for managing our dns zone.

Now, we can continue for dnssec with some theory:

DNS by itself is not secure. DNS was designed in the 1980s when the Internet was much smaller, and security was not a primary consideration in its design. As a result, when a recursive resolver sends a query to an authoritative name server, the resolver has no way to verify the authenticity of the response. The resolver can only check that a response appears to come from the same IP address where the resolver sent the original query. But relying on the source IP address of a response is not a strong authentication mechanism, since the source IP address of a DNS response packet can be easily forged, or spoofed.

DNSSEC strengthens authentication in DNS using digital signatures based on public key cryptography. With DNSSEC, it’s not DNS queries and responses themselves that are cryptographically signed, but rather DNS data itself is signed by the owner of the data…. More about this:

So, lets create a key-pairs:

1 – Create a Zone Signing Key (ZSK)
2 – Create a Key Signing Key (KSK)

cd /var/named
dnssec-keygen -a RSASHA256 -b 2048 -r /dev/urandom
dnssec-keygen -a RSASHA256 -b 4096 -r /dev/urandom -f KSK

eg: Generating key pair......................++ .............................................................................................................................................................................................................++

The directory will now have 4 keys – private/public pairs of ZSK and KSK. We have to add the public keys which contain the DNSKEY record to the zone file. For example:

cat >> /var/named/
cat >> /var/named/

Now, edit owner permisions of these keys to named:

chown named*

And now, check our zone and if is everything ok, sign the zone:

named-checkzone /var/named/

zone loaded serial 2021090901

dnssec-signzone -A -3 $(head -c 1000 /dev/urandom | sha1sum | cut -b 1-16) -N INCREMENT -o -t

Verifying the zone using the following algorithms: RSASHA256.
Zone fully signed:
Algorithm: RSASHA256: KSKs: 1 active, 0 stand-by, 0 revoked
                      ZSKs: 1 active, 0 stand-by, 0 revoked
Signatures generated:                       13
Signatures retained:                         0
Signatures dropped:                          0
Signatures successfully verified:            0
Signatures unsuccessfully verified:          0
Signing time in seconds:                 0.020
Signatures per second:                 649.707
Runtime in seconds:                      0.025

Above command created a signed zone file for zone:

We are now required to edit zone configuration to use new file instead of old file, and add som variables:

vim /etc/named.conf

zone    "" IN {
        type master;
        file "";
        allow-transfer { none; };
   # DNSSEC keys Location
   key-directory "/var/named/"; 

   # Publish and Activate DNSSEC keys
   auto-dnssec maintain;

   # Use Inline Signing
   inline-signing yes;


Now, check named config, and our zone:

named-checkzone /var/named/

zone loaded serial 2021090902 (DNSSEC signed)

systemctl restart named

And now, we can try our zone, if dnssec is working:

dig DNSKEY +multiline

; <<>> DiG 9.11.26-RedHat-9.11.26-6.el8 <<>> DNSKEY +multiline
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 19926
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

; EDNS: version: 0, flags:; udp: 4096

;; ANSWER SECTION:		604795 IN DNSKEY 256 3 8 (
				) ; ZSK; alg = RSASHA256 ; key id = 56241		604795 IN DNSKEY 257 3 8 (
				) ; KSK; alg = RSASHA256 ; key id = 18544

We have configured DNSSEC on our master DNS server.

DHCP server on lan

Now, we install dhcp server on our centos server. I have interface for this server: ens22, and our gateway will be and clients will have IPs from range: We must have a static IP from this subnet on this interface.

dnf makecache
dnf install dhcp-server

Now, create own configuration file for dhcp server, or copy example and adjust:

cp /usr/share/doc/dhcp-server/dhcpd.conf.example /etc/dhcp/dhcpd.conf
vim /etc/dhcp/dhcpd.conf

And edit this variables as you fit:

option domain-name "";
option domain-name-servers;

default-lease-time 600;
max-lease-time 7200;

ddns-update-style none;
log-facility local7;

subnet netmask {
subnet netmask {
  option routers;
  option domain-name-servers;

Now, we can start dhcp server and view our logs. I have 8 interfaces, but only on this is dhcp enabled:

systemctl start dhcpd.service
systemctl enable dhcpd.service
tail /var/log/messages -f

No subnet declaration for ens18 (no IPv4 addresses).
 ** Ignoring requests on ens18.  If this is not what
you want, please write a subnet declaration
in your dhcpd.conf file for the network segment
to which interface ens18 is attached. **

Now, enable UDP port 67 to access dhcp server. Look below for this port. So edit your script and apply it and view:

netstat -tulpen | grep dhcp
udp        0      0

iif "ens22" udp dport { 67 } accept

And start some PC on this network and we will see at logs:

tail -n 10 /var/log/messages
dhcpd[]: DHCPREQUEST for from ac:5a:32:98:0c:xx (pc1) via ens22
dhcpd[]: DHCPACK on to ac:5a:32:98:0c:xx (pc1) via ens22
dhcpd[]: DHCPREQUEST for from ac:5a:32:98:0c:xx (pc1) via ens22
dhcpd[]: DHCPACK on to ac:5a:32:98:0c:xx (pc1) via ens22
Total Page Visits: 88929 - Today Page Visits: 22

How to install mail server (postfix, dovecot, webmail and spamassasin) on Centos 8 with selinux enabled


In order to set up a full simple mail server, this guide takes advantage of Postfix as an SMTP server, Dovecot to provide POP/IMAP functionality, and RoundCube as a webmail program or client so that users can check and receive email from their favorite web browsers.

Dovecot: Dovecot is an open-source IMAP and POP3 email server for Linux/UNIX-like systems, written with security primarily in mind.
Postfix: Postfix is a free and open-source mail transfer agent (MTA) that routes and delivers electronic mail from one server to another over the internet.
Roundcube: Once the mails have been delivered into a mailbox, most users would need an easy to use interface to read their mails. Roundcube does this pretty well. It is a browser-based multilingual IMAP client with an application-like user interface. It provides full functionality you expect from an email client, including MIME support, address book, folder manipulation, message searching and spell checking.

So, at first, as usual, we use fully updated system:

dnf update -y

Now, configure some prerequisites.

Before proceeding further, also ensure that no other MTAs such as Sendmail are existing as this will cause conflict with Postfix configuration. To remove Sendmail, for example, run the command:

dnf remove sendmail

Now set FQDN (Fully Qualifed Domain Name) and set hostname:

hostnamectl set-hostname
exec bash
vim /etc/hosts

How to install mariadb server, apache web server and php version 7.3 (or 7.4) you can find at another post, like:

So, now, we can install out MTA Postfix, very simple, with mysql support (our users will be stored in mysql database):

dnf install postfix postfix-mysql -y
systemctl start postfix
systemctl enable postfix

To check postfix status, write this command:

systemctl status postfix

Now, we enable some ports of firewall. If you want use POP3, enable it. I prefer not to use. In order to send emails from your server, port 25 (outbound) must be open. To be able to send emails using a desktop email client (Thunderbird or Outlook), we need to enable the submission service in Postfix. And to encrypt our communications, we need a TLS certificate.

firewall-cmd --permanent --add-service={http,https,smtp-submission,smtps,imap,imaps}
systemctl reload firewalld

sudo firewall-cmd --permanent --add-service={pop3,pop3s}
systemctl reload firewalld

When we configure a desktop email client, enabling encryption is always a good idea. We can easily obtain a free TLS certificate from Let’s Encrypt. Issue the following commands to install Let’s Encrypt client (certbot) on CentOS 8/RHEL 8 from the EPEL repository. If you don’t have a web server running yet, I recommend you install one (Apache).

dnf install epel-release -y
dnf install certbot python3-certbot-apache
dnf install httpd
systemctl start httpd
systemctl enable httpd

We create and simple virtual host for Apache to obtain certificate. Like this:

vim /etc/httpd/conf.d/


        DocumentRoot /var/www/html/

systemctl reload httpd

Now, if everything is ok (Apache is realoaded), we can obtain our TLS certificate for postfix/dovecot in future settings:

certbot --apache --email -d

and the results:

Congratulations! You have successfully enabled
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

 - Congratulations! Your certificate and chain have been saved at:
   Your key file has been saved at:

Configuring Postfix

To send emails from a desktop email client, we need to enable the submission service of Postfix so that the email client can submit emails to Postfix SMTP server. Edit the file.

vim /etc/postfix/

In submission section, uncomment or add the following lines. Please allow at least one whitespace (tab or spacebar) before each -o.  In postfix configurations, a preceding whitespace character means that this line is continuation of the previous line.

submission inet n - n - - smtpd
-o syslog_name=postfix/submission
-o smtpd_tls_security_level=encrypt
-o smtpd_tls_wrappermode=no
-o smtpd_sasl_auth_enable=yes
-o smtpd_relay_restrictions=permit_sasl_authenticated,reject
-o smtpd_recipient_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
-o smtpd_sasl_type=dovecot
-o smtpd_sasl_path=private/auth

The above configuration enables the submission daemon of Postfix and requires TLS encryption. So later on our desktop email client can connect to the submission daemon in TLS encryption. The submission daemon listens on TCP port 587. STARTTLS is used to encrypt communications between email client and the submission daemon.

Microsoft Outlook only supports submission over port 465. If you are going to use Microsoft outlook mail client, then you also need to enable submission service on port 465 by adding the following lines in the file.

smtps inet n - n - - smtpd
-o syslog_name=postfix/smtps
-o smtpd_tls_wrappermode=yes
-o smtpd_sasl_auth_enable=yes
-o smtpd_relay_restrictions=permit_sasl_authenticated,reject
-o smtpd_recipient_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
-o smtpd_sasl_type=dovecot
-o smtpd_sasl_path=private/auth

Save and close this file for the moment. Now we configure main configurations of Postfix. open file and edit this lines as mine. If you dont have these lines, please add they.

cp /etc/postfix/ /etc/postfix/
vim /etc/postfix/

smtpd_tls_cert_file = /etc/letsencrypt/live/
smtpd_tls_key_file = /etc/letsencrypt/live/
smtpd_tls_loglevel = 1
smtp_tls_loglevel = 1

#Force TLSv1.3 or TLSv1.2 
smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 
smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 
smtp_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1 
smtp_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1

myhostname =
mydomain =
message_size_limit = 31457280

Save and close file. Now restart postfix to ensure, that the change of settings take effect:

systemctl restart postfix

If you run the following command, you will see Postfix is now listening on port 587 and 465.

netstat -lnpt | grep master

bash: netstat: command not found
#if we havent's these command, check who provide it:

dnf provides netstat


net-tools-2.0-0.51.20160912git.el8.x86_64 : Basic networking tools
Repo : BaseOS
Matched from:
Filename : /usr/bin/netstat

So install it:

dnf install net-tools
netstat -lnpt | grep master

tcp 0 0* LISTEN 62343/master
tcp 0 0* LISTEN 62343/master
tcp 0 0* LISTEN 62343/master

Installing Dovecot IMAP Server and configuring

So, as usual, install imap server dovecot with mysql support:

dnf install dovecot dovecot-mysql
dovecot --version

2.3.8 (9df20d2db)

Now start it and enable after boot:

systemctl start dovecot
systemctl enable dovecot

Open dovecot config and edit or add this line:

cp /etc/dovecot/dovecot.conf /etc/dovecot/dovecot.conf.orig
vim /etc/dovecot/dovecot.con

protocols = imap

Save and close file. then restart dovecot:

systemctl restart dovecot.service

systemctl status dovecot.service
● dovecot.service - Dovecot IMAP/POP3 email server
Loaded: loaded (/usr/lib/systemd/system/dovecot.service; enabled; vendor preset: disabled)
Active: active (running) since Fri 2020-09-18 11:32:49 CEST; 28s ago

For storing messages, we use Maildir format. Every mail is stored in separate file in precise directory structure. So, create a directory for your domain/domains and edit line/lines like next:

mkdir -p /var/vmail/vhosts/
cp /etc/dovecot/conf.d/10-mail.conf /etc/dovecot/conf.d/10-mail.conf.orig
vim /etc/dovecot/conf.d/10-mail.conf
mail_location = maildir:/var/vmail/vhosts/%d/%n
mail_privileged_group = mail

Save file and exit. Now assign user dovecot to group mail and vmail, for reading Inbox and writing to folder destinations:

gpasswd -a dovecot mail
usermod -a -G vmail dovecot

Set a database for users, domains and aliases

So, log in mysql as root and create a database, in which we will be storring informations about used domains, users, passwords and mail aliases for users. Then create tables, for this informations. Adjust your informations…

mysql -u root -p

GRANT SELECT ON maildb.* TO 'usermail'@'localhost' IDENTIFIED BY 'PASSWORD';
GRANT update ON maildb.* TO 'usermail'@'localhost' IDENTIFIED BY 'PASSWORD';

USE maildb;

CREATE TABLE virtual_domains (

CREATE TABLE virtual_users (
domain_id INT NOT NULL,
password VARCHAR(106) NOT NULL,
email VARCHAR(120) NOT NULL,
UNIQUE KEY email (email),
FOREIGN KEY (domain_id) REFERENCES virtual_domains(id) ON DELETE CASCADE

CREATE TABLE virtual_aliases (
domain_id INT NOT NULL,
source varchar(200) NOT NULL,
destination varchar(100) NOT NULL,
FOREIGN KEY (domain_id) REFERENCES virtual_domains(id) ON DELETE CASCADE

INSERT INTO maildb.virtual_domains
(id ,name)
('1', '');

INSERT INTO maildb.virtual_users
(id, domain_id, password , email)
('1', '7', 'UserPassword', '');

INSERT INTO maildb.virtual_aliases
(id, domain_id, source, destination)
('1', '1', '', '');

Now, we can see, that the password is stored in our databases in plaintext:

MariaDB [maildb]> select * from virtual_users;
| id | domain_id | password | email |
| 1 | 7 | UserPassword | |
1 row in set (0.000 sec)

So we change it:

update virtual_users set password = ENCRYPT('UserPassword', CONCAT('$6$', SUBSTRING(SHA(RAND()), -16))) where email = '';

select * from virtual_users;

MariaDB [maildb]> select * from virtual_users;
| id | domain_id | password | email |
| 1 | 7 | $6$b308975352080ba6$TFt0bZNCPZdgLtn2S9hHMQSdxFikxGDpLqNVap7r/q9OgHGP/EddEzc9Oc3Ww4nvinbrR2pGNgLUpK.PQ1JVD/ | |
1 row in set (0.000 sec)

And if you want, now you can add your user right with shit encrypt form:

INSERT INTO maildb.virtual_users
(id, domain_id, password , email)
('2', '7', ENCRYPT('password2', CONCAT('$6$', SUBSTRING(SHA(RAND()), -16))), '');

And now, we can see:

select * from virtual_users;
| id | domain_id | password | email |
| 1 | 7 | $6$b308975352080ba6$TFt0bZNCPZdgLtn2S9hHMQSdxFikxGDpLqNVap7r/q9OgHGP/EddEzc9Oc3Ww4nvinbrR2pGNgLUpK.PQ1JVD/ | |
| 2 | 7 | $6$93809b2da2242ede$toapOIav4kqmLiFl03xvZiEe9LXvqDs.nT5Ristkmy0zCyk6fc.JjjlekElcJ9MczPv5e9b4eH/lumkgOpZq6/ | |

Now we can exit from mariadb server with command “exit;”and continue

Now, we add som configuration files for postfix to be sure, that postfix will understand, where our information about users, domains and aliases are and how to connect to them:

cat > /etc/postfix/ << EOF
user = usermail
password = PASSWORD
hosts =
dbname = maildb
query = SELECT 1 FROM virtual_domains WHERE name='%s'

cat > /etc/postfix/ << EOF
user = usermail
password = PASSWORD
hosts =
dbname = maildb
query = SELECT 1 FROM virtual_users WHERE email='%s'

cat > /etc/postfix/ << EOF
user = usermail
password = PASSWORD
hosts =
dbname = maildb
query = SELECT destination FROM virtual_aliases WHERE source='%s'

cat > /etc/postfix/ << EOF
user = usermail
password = PASSWORD
hosts =
dbname = maildb
query = SELECT email FROM virtual_users WHERE email='%s'

cat > << EOF
user = usermail
password = PASSWORD
hosts =
dbname = maildb
query = select destination from virtual_aliases where source='%s'

Now, we can check, if postifx understand this:

postmap -q mysql:/etc/postfix/

postmap -q mysql:/etc/postfix/

postmap -q mysql:/etc/postfix/

Now, we can continue with configuring again Postfix and Dovecot. In Postfix add these lines, we created before:

vim /etc/postfix/

virtual_mailbox_domains = mysql:/etc/postfix/
virtual_mailbox_maps = mysql:/etc/postfix/
virtual_alias_maps = mysql:/etc/postfix/
smtpd_sender_login_maps = mysql:/etc/postfix/, mysql:/etc/postfix/

smtpd_helo_restrictions =

smtpd_recipient_restrictions =

smtpd_sender_restrictions =
 check_sender_access hash:/etc/postfix/access,

Check, if ist Postfix properly configured and there is no syntax mistake:

postfix check
#if nothing is displayed, is OK and then:
systemctl restart postfix.service

Now, continue with dovecot. Uncoment lines, or modify/add:

cp /etc/dovecot/conf.d/10-auth.conf /etc/dovecot/conf.d/10-auth.conf.orig
vim /etc/dovecot/conf.d/10-auth.conf

disable_plaintext_auth = yes
auth_mechanisms = plain login
!include auth-sql.conf.ext

Now, edit /etc/dovecot/conf.d/auth-sql.conf.ext and adjust:

passdb {
driver = sql
args = /etc/dovecot/dovecot-sql.conf.ext

userdb {
driver = static
args = uid=vmail gid=vmail home=/var/vmail/vhosts/%d/%n

And edit/or add file:


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

Next, edit an adjust dovecot ssl for using our LE certificate:

cp /etc/dovecot/conf.d/10-ssl.conf /etc/dovecot/conf.d/10-ssl.conf.orig
vim /etc/dovecot/conf.d/10-ssl.conf

ssl_cert = </etc/letsencrypt/live/
ssl_key = </etc/letsencrypt/live/
ssl_dh = </etc/dovecot/dh.pem

ssl_min_protocol = TLSv1
ssl_prefer_server_ciphers = yes

save and close. Now generate dh.pem. It take a long time, in my case, 10minutes:

openssl dhparam -out /etc/dovecot/dh.pem 4096

Now, edit SASL authentication between Postfix and Dovecot:

cp /etc/dovecot/conf.d/10-master.conf /etc/dovecot/conf.d/10-master.conf.orig
vim /etc/dovecot/conf.d/10-master.conf

service auth {
unix_listener /var/spool/postfix/private/auth {
mode = 0660
user = postfix
group = postfix

Then, we set Dovecot, to auto-create folders, after user first login. To enable this, edit lines like below:

cp /etc/dovecot/conf.d/15-mailboxes.conf  /etc/dovecot/conf.d/15-mailboxes.conf.orig
vim /etc/dovecot/conf.d/15-mailboxes.conf

mailbox Trash {
auto = create
special_use = \Trash

mailbox Drafts {
auto = create
special_use = \Drafts

Now, restart dovecot and check, if working, for now somehow 🙂

systemctl restart dovecot
systemctl restart postfix
netstat -lnpt | grep dovecot
tcp 0 0* LISTEN 164849/dovecot
tcp 0 0* LISTEN 164849/dovecot
tcp6 0 0 :::993 :::* LISTEN 164849/dovecot
tcp6 0 0 :::143 :::* LISTEN 164849/dovecot

#if problem, watch log why:
systemctl status dovecot

By default, Postfix uses its builtin local delivery agent (LDA) to move inbound emails to the message store (inbox, sent, trash, Junk, etc). We can configure it to use Dovecot to deliver emails, via the LMTP protocol, which is a simplified version of SMTP. LMTP allows for a highly scalable and reliable mail system. This step is required if you want to use the sieve plugin to filter inbound messages to different folders.

Edit Dovecot main configuration files, and next postfix configuration:

vim /etc/dovecot/dovecot.conf

protocols = imap lmtp
vim /etc/dovecot/conf.d/10-master.conf

service lmtp { 
unix_listener /var/spool/postfix/private/dovecot-lmtp { 
mode = 0600 
user = postfix 
group = postfix 
vim /etc/postfix/

mailbox_transport = lmtp:unix:private/dovecot-lmtp
virtual_transport = lmtp:unix:private/dovecot-lmtp
smtputf8_enable = no
systemctl restart postfix dovecot

Now, we can set up our desktop clients for serving our mails. I prefer Mozilla Thunderbird. So , in settings, use this variables:

IMAP, port 993, SSL/TLS, normal password
IMAP, port 143, STARTTLS, normal password
SMTP, port 587, STARTTLS, normal password
SMPT, port 465, SSL/TLS, normal password
password: yours
server for incoming and outgoing:

I prefer using port 465/SSL and 993/SSL. Because this are native SSL ports.

Improoving email delivery with SPF and DKIM records

Until now, we have working mail server with Postfix and Dovecot with dekstop email clients (Thunderbird). We can send mail to the world, and world to us (of couse, we must have correct DNS names/records, like MX, A and PTR. But sometimes, may happend, that our email is mark as SPAM. So we are going to look at how to improve email delivery to recipient’s inbox by setting up SPF and DKIM on CentOS/RHEL server.

So, what is SPF?

I use wikipedia: Sender Policy Framework (SPF) is an email authentication method designed to detect forging sender addresses during the delivery of the email. SPF alone, though, is limited only to detect a forged sender claimed in the envelope of the email which is used when the mail gets bounced.

SPF record specifies which hosts or IP addresses are allowed to send emails on behalf of a domain. You should allow only your own email server or your ISP’s server to send emails for your domain.

Now, we must add SPF record to our domain. It is TXT record, like this:

TXT  @   "v=spf1 mx -all"


  • TXT indicates this is a TXT record.
  • @ in the name field represent the apex domain name.
  • v=spf1 indicates this is a SPF record and the SPF record version is SPF1.
  • mx means all hosts listed in the MX records are allowed to send emails for your domain and all other hosts are disallowed.
  • -all indicates that emails from your domain should only come from hosts specified in the SPF record. Emails sent from other hosts will be flagged as fail.

If we test for our domain MX record, this SPF record is pointing only to this one. So receiving mailserver can evaluate, that sender is authorized to use this mail server:

dig -t txt
;; ANSWER SECTION: 600 IN TXT "v=spf1 a mx -all"

dig -t mx


Of course, you can use online SPF validator such as to see which hosts are allowed to send emails for your domain and debug your SPF record if any error occurs.

Configuring SPF Policy Agent

We also need to tell our Postfix SMTP server to check the SPF record of incoming emails to detect forged emails. First install required packages:

dnf install pypolicyd-spf

Then add a user for policyd-spf.

adduser policyd-spf --user-group --no-create-home -s /bin/false

And dit the Postfix master process configuration file. Add the following lines at the end of the file, which tells Postfix to start the SPF policy daemon when it’s starting itself. Policyd-spf will run as the policyd-spf user.

vim /etc/postfix/

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

Save and close the file. Next, edit Postfix main configuration file. Append the following lines at the end of the file. The first line specifies the Postfix policy agent timeout setting (for querying DNS). The following lines will impose restrictions on incoming emails by checking SPF record.

vim /etc/postfix/

smtpd_recipient_restrictions =
check_policy_service unix:private/policyd-spf

Save and close the file. Then restart Postfix.

systemctl restart postfix

Next time, when you receive an email from a domain that has an SPF record, you can see the SPF check results in the raw email header. The following header indicates the sender sent the email from an authorized host:

Received-SPF: Pass (mailfrom) identity=mailfrom;

To test the SPF records with your domain, try:

So, what is DKIM?

According to wiki:

DomainKeys Identified Mail (DKIM) is an email authentication method designed to detect forged sender addresses in emails (email spoofing), a technique often used in phishing and email spam.

DKIM allows the receiver to check that an email claimed to have come from a specific domain was indeed authorized by the owner of that domain. It achieves this by affixing a digital signature, linked to a domain name, to each outgoing email message. The recipient system can verify this by looking up the sender’s public key published in the DNS. A valid signature also guarantees that some parts of the email (possibly including attachments) have not been modified since the signature was affixed. Usually, DKIM signatures are not visible to end-users, and are affixed or verified by the infrastructure rather than the message’s authors and recipients.

Simply: DKIM uses a private key to add a signature to emails sent from your domain. Receiving SMTP servers verify the signature by using the corresponding public key, which is published in your domain’s DNS records.

Now, we must install som package:

dnf install opendkim

At beginig, we must edit main configuration file of opendkim and adjust the line:


Mode sv

By default, OpenDKIM runs in verification mode (v), which will verify the DKIM signature of incoming email messages. We need to sign outgoing emails, so change this line to the following to enable signing mode.

Find the following line and comment it out, because we will use separate keys for each domain name.

KeyFile /etc/opendkim/keys/default.private

Next, find the following 4 lines and uncomment them.

KeyTable /etc/opendkim/KeyTable
SigningTable refile:/etc/opendkim/SigningTable
ExternalIgnoreList refile:/etc/opendkim/TrustedHosts
InternalHosts refile:/etc/opendkim/TrustedHosts

Create Signing Table, Key Table and Trusted Hosts File

Edit the signing table file.

vim /etc/opendkim/SigningTable

Add the following line at the end of this file. This tells OpenDKIM that if a sender on your server is using a address, then it should be signed with the private key identified by


20200925 is the DKIM selector. A domain name might have multiple DKIM keys. The DKIM selector allows you to choose a particular DKIM key. You can use whatever name for the DKIM selector, but I found it’s convenient to use the current date (September 25, 2020) as the DKIM selector. Save and close the file. Then edit the key table file.

vim /etc/opendkim/KeyTable

Add the following line, which specifies the location of the DKIM private key.

Save and close the file. Next, edit the trusted hosts file.

vim /etc/opendkim/TrustedHosts and ::1 are included in this file by default. Now add the following line. This tells OpenDKIM that if an email is coming from your own domain name, then OpenDKIM should not perform DKIM verification on the email.


Save and close the file.

Generate Private/Public Keypair

Since DKIM is used to sign outgoing messages and verify incoming messages, you need to generate a private key to sign outgoing emails and a public key for receiving SMTP servers to verify the DKIM signature of your email. Public key will be published in DNS.

Create a separate folder for the domain.

mkdir /etc/opendkim/keys/

Generate keys using opendkim-genkey tool.

opendkim-genkey -b 2048 -d -D /etc/opendkim/keys/ -s 20200925 -v

The above command will create 2048 bits keys. -d (domain) specifies the domain. -D (directory) specifies the directory where the keys will be stored. I use 20200925 as the DKIM selector. Once the command is executed, the private key will be written to 20200925.private file and the public key will be written to 20200925.txt file:

opendkim-genkey: generating private key
opendkim-genkey: private key written to 20200925.private
opendkim-genkey: extracting public key
opendkim-genkey: DNS TXT record written to 20200925.txt

And adjust ownership:

chown opendkim:opendkim /etc/opendkim/keys/ -R

Publish Your Public Key in DNS Records

Display the public key. The string after the p parameter is the public key:

cat /etc/opendkim/keys/

20200925._domainkey IN TXT ( "v=DKIM1; k=rsa; "
"7jd9GPRkXpwMpbumKPdLD+wINIyr9L4r31/TIVpVDq7ZP6JrksyBHVFSWZQsODLIHjLL2ln/o/VSUcPXxy8H/44Xpzw2RHwcGXrMdQ44IXenhel+4A3M/FTK3cLS8EuHVJ2YDwUQIDAQAB" ) ; ----- DKIM key 20200925 for

In you DNS manager, create a TXT record, enter 20200925._domainkey in the name field. (You need to replace 20200925 with your own DKIM selector.) Then go back to the terminal window, copy everything in the parentheses and paste it into the value field of the DNS record. You need to delete all double quotes and line breaks in the value field. If you don’t delete them, then key test in the next step will probably fail.

Test DKIM Key

Enter the following command on your server to test your key.

sudo opendkim-testkey -d -s 20200925 -vvv

If everything is OK, you will see the key OK message.

opendkim-testkey: using default configfile /etc/opendkim.conf
opendkim-testkey: checking key ''
opendkim-testkey: key OK

Now we can start the opendkim service.

systemctl start opendkim

And enable auto-start at boot time.

systemctl enable opendkim

OpenDKIM listens on

Connect Postfix to OpenDKIM

Edit Postfix main configuration file:

vim /etc/postfix/

Add the following lines at the end of this file, so Postfix will be able to call OpenDKIM via the milter protocol. Note that you should use as the address. Don’t use localhost.

# Milter configuration 
milter_default_action = accept 
milter_protocol = 6 
smtpd_milters = inet: 
non_smtpd_milters = $smtpd_milters

Save and close the file. Then add postfix user to opendkim group.

 gpasswd -a postfix opendkim

Restart postfix service.

 systemctl restart postfix

Now, you can send email from, maybe, to you. In maillog of your server, you will see that dkim works on incoming mails:

opendkim[]: : mail.example.comk [IP of domain] not internal
opendkim[]: : DKIM verification successful

Now, you can send email from you to anybody. In maillog of your server, you will see that dkim works on outcoming mails and that opendkim add signature:

opendkim[]: : DKIM-Signature field added (s=20200925,

Or you can send an empty mail to adress:

And during few seconds, you will get back report with some things. If SPF is fine and if dkim works:

Summary of Results
SPF check: pass 
"iprev" check: pass 
DKIM check: pass 
SpamAssassin check: ham

SPF check details:
DNS record(s): 188 IN TXT "v=spf1 a mx -all" 188

DKIM check details:
Result: pass (matches From: 
ID(s) verified:

How to fight with spam? I will use spamassasin

So what is Spamassasin? According the project web site:

SpamAssassin is a mature, widely-deployed open source project that serves as a mail filter to identify Spam. SpamAssassin uses a variety of mechanisms including header and text analysis, Bayesian filtering, DNS blocklists, and collaborative filtering databases. SpamAssassin runs on a server, and filters spam before it reaches your mailbox.

So we install it first:

dnf install spamassassin

The server binary installed by the spamassassin package is called spamd, which will be listening on TCP port 783 on localhost. Spamc is the client for SpamAssassin spam filtering daemon. By default, the spamassassin systemd service is disabled, you can enable auto start at boot time with:

systemctl enable spamassassin
systemctl start spamassassin

There are several ways you can use to integrate SpamAssassin with Postfix. I use SpamAssassin via the sendmail milter interface, because it allows me to reject an email when it gets a very high score such as 8, so it will never be seen by the recipient.

dnf install spamass-milter-postfix
systemctl start spamass-milter
systemctl enable spamass-milter

Now edit the Postfix main configuration file and add/edit this lines:

vim /etc/postfix/

Milter configuration
milter_default_action = accept
milter_protocol = 6
smtpd_milters = inet:,unix:/run/spamass-milter/postfix/sock
non_smtpd_milters = $smtpd_milters

Save and close the file. Now open the /etc/sysconfig/spamass-milter file and find the following line.

#EXTRA_FLAGS="-m -r 15"

Uncomment this line and change 15 to your preferred reject score such as 8.

EXTRA_FLAGS="-m -r 8"

Previously we discussed 7 effective methods for blocking email spam with Postfix on CentOS/RHEL. In this tutorial, we are going to learn how to use SpamAssassin (SA) to detect spam on CentOS/RHEL mail server. SpamAssassin is a free, open-source, flexible and powerful spam-fighting tool.

SpamAssassin is a score-based system. It will check email message against a large set of rules. Each rule adds or removes points in the message’s score. If the score is high enough (by default 5.0), the message is considered spam.

Set Up SpamAssassin on CentOS RHEL to Block Email Spam

Install antivirus clamd and content filter amavis

What is amavis?

According to wiki:

Amavis is an open-source content filter for electronic mail, implementing mail message transfer, decoding, some processing and checking, and interfacing with external content filters to provide protection against spam and viruses and other malware. It can be considered an interface between a mailer (MTA, Mail Transfer Agent) and one or more content filters.

Amavis can be used to:

  • detect viruses, spam, banned content types or syntax errors in mail messages
  • block, tag, redirect (using sub-addressing), or forward mail depending on its content, origin or size
  • quarantine (and release), or archive mail messages to files, to mailboxes, or to a relational database
  • sanitize passed messages using an external sanitizer
  • generate DKIM signatures
  • verify DKIM signatures and provide DKIM-based whitelisting

And what is clamv?

ClamAV® is an open source antivirus engine for detecting trojans, viruses, malware & other malicious threats.

To install Amavisd and Clamav Server run:

dnf install amavis clamd -y
#it takes 250MB with dependencies

Now edit Clamav configuration file and adjust lines:

vim /etc/clamd.d/scan.conf

LogFile /var/log/clamd.scan
PidFile /run/clamd.scan/
TemporaryDirectory /var/tmp
LocalSocket /run/clamd.scan/clamd.sock

Save and close. Now create log file for Clamav and start it:

touch /var/log/clamd.scan
chown clamscan. /var/log/clamd.scan
restorecon -v /var/log/clamd.scan

systemctl start clamd@scan.service
systemctl enable clamd@

And now, allow Clamav to scan system for Selinux:

setsebool -P antivirus_can_scan_system on

Now configure Amavisd:

vim /etc/amavisd/amavisd.conf

$mydomain = ''; # a convenient default for other settings
$myhostname = ''; # must be a fully-qualified domain name!
$inet_socket_bind = '';
$notify_method = 'smtp:[]:10025';
$forward_method = 'smtp:[]:10025'; # set to undef with milter!

And enable it and start it:

systemctl start amavisd.service
systemctl enable amavisd.service

Now edit Postfix main configuration file and add at the end of file:

vim /etc/postfix/


Now edit and add at the end there lines:

vim /etc/postfix/

smtp-amavis unix - - n - 2 smtp
 -o smtp_data_done_timeout=1200
 -o smtp_send_xforward_command=yes
 -o disable_dns_lookups=yes inet n - n - - smtpd
 -o content_filter=
 -o local_recipient_maps=
 -o relay_recipient_maps=
 -o smtpd_restriction_classes=
 -o smtpd_client_restrictions=
 -o smtpd_helo_restrictions=
 -o smtpd_sender_restrictions=
 -o smtpd_recipient_restrictions=permit_mynetworks,reject
 -o mynetworks=
 -o strict_rfc821_envelopes=yes
 -o smtpd_error_sleep_time=0
 -o smtpd_soft_error_limit=1001
 -o smtpd_hard_error_limit=1000

And restart Postfix:

systemctl restart postfix.service

And if everything is OK, you can see in the detailed headers of mail, that it has been scanned:

X-Virus-Scanned: Amavisd-new at

And in the maiilog:

amavis[]: () Passed CLEAN {RelayedInbound}
amavis[]: () Passed UNCHECKED-ENCRYPTED
amavis[]: () Blocked INFECTED (Win.Test.EICAR_HDB-1) {DiscardedInbound,Quarantined}

Total Page Visits: 88929 - Today Page Visits: 22

How to install nextcloud v18 on Centos 8 Stream

I create a basic installation of Centos 8 stream from iso: CentOS-Stream-8-x86_64-20191219-boot.iso

During installation I choose minimal applications and standard utilities. Please, enable, network time and set lvm for virtio disk. I set password for root and create a new user, which have root privileges.

After instalation, I create and LVM encrypted partition, to store encrypted data of nextcloud on it. I will not use nextcloud data encryption. Command below creates encrypted disk. We must enter a passphrase twice

 cryptsetup -y -v luksFormat /dev/vdb

Now, we open this partition and look at status:

cryptsetup luksOpen /dev/vdb vdb_crypt
cryptsetup -v status vdb_crypt

/dev/mapper/vdb_crypt is active.
   type:    LUKS2
   cipher:  aes-xts-plain64
   keysize: 512 bits
   key location: keyring
   device:  /dev/vdb
   sector size:  512
   offset:  32768 sectors
   size:    209682432 sectors
   mode:    read/write
 Command successful.

Now, I write 4GB zeros to this device to see, if everything is OK. It is possible, to full-up tho whole device, but it can take a long time. But the true reason is, that this will allocate block data with zeros. This ensures that outside world will see this as random data i.e. it protect against disclosure of usage patterns.

dd if=/dev/zero of=/dev/mapper/vdb_crypt bs=4M count=1000
4194304000 bytes (4.2 GB, 3.9 GiB) copied, 130.273 s, 32.2 MB/s

Now try close and open this encrypted device. And then, I create an lvm above the luks encrypted disk:

cryptsetup luksClose vdb_crypt
cryptsetup luksOpen /dev/vdb vdb_crypt
cryptsetup -v status vdb_crypt
pvcreate /dev/mapper/vdb_crypt
vgcreate nextcloud /dev/mapper/vdb_crypt
lvcreate -n data -L+30G nextcloud
mkdir /mnt/test
mkfs.xfs /dev/mapper/nextcloud-data
mount /dev/mapper/nextcloud-data /mnt/test/
touch /mnt/test/hello 
ll /mnt/test/hello
umount /mnt/test/

Installing nextcloud and prerequisites

And now, we can start with preparing our Centos for nextcloud

At first, update system. Via dnf (DNF is the next upcoming major version of YUM, a package manager for RPM-based Linux distributions. It roughly maintains CLI compatibility with YUM and defines a strict API for extensions and plugins.)

dnf update -y

Next, we install and create empty database for our nextcloud. Then we start it and enable for autostart after boot.
If you wish, you can skip installations of MariaDB and you can use built-in SQLite. Then you can continue with installing apache web server.

dnf -y install mariadb-server
systemctl start mariadb
systemctl enable mariadb

Now, we run post installation script to finish setting up mariaDB server:

Set root password? [Y/n] y
Remove anonymous users? [Y/n] y
Disallow root login remotely? [Y/n] y
Remove test database and access to it? [Y/n] y
Reload privilege tables now? [Y/n] y

Now, we can create a database for nextcloud.

mysql -u root -p
GRANT ALL PRIVILEGES ON nextcloud.* TO 'nextclouduser'@'localhost' IDENTIFIED BY 'YOURPASSWORD';

Now, we install Apache web server, and we start it and enable for autostart after boot:

dnf install httpd -y
systemctl start httpd.service
systemctl enable httpd.service

And set up firewall fow port http/80 and ssh/20 only:

systemctl status httpd
firewall-cmd --list-all
firewall-cmd --zone=public --permanent --remove-service=dhcpv6-client
firewall-cmd --zone=public --permanent --add-service=http
firewall-cmd --reload

Now point your browser to this server and look, if you see a Apache test page.

Now we can install php. Nextcloud (at this time is version 18.0.1) and support PHP (7.1, 7.2 or 7.3). So I use remi repositories and install php 7.3:

dnf -y install dnf-utils
dnf module list php
dnf module reset php
dnf module enable php:remi-7.3
dnf info php
dnf install php php-gd php-mbstring php-intl php-pecl-apcu php-mysqlnd php-pecl-imagick.x86_64 php-ldap php-pecl-zip.x86_64 php-process.x86_64
php -v
php --ini |grep Loaded
sed -i "s/post_max_size = 8M/post_max_size = 500M/" /etc/php.ini
sed -i "s/upload_max_filesize = 2M/upload_max_filesize = 500M/" /etc/php.ini
sed -i "s/memory_limit = 128M/memory_limit = 512M/" /etc/php.ini
systemctl start php-fpm.service
systemctl enable php-fpm.service

And now, we can install nextcloud:

mkdir -p /var/www/html/nextcloud/data
cd /var/www/html/nextcloud/
mount /dev/mapper/nextcloud-data /var/www/html/nextcloud/data/
mv nextcloud/* .
mv nextcloud/.htaccess .
mv nextcloud/.user.ini .
rmdir nextcloud/
mkdir /var/www/html/nextcloud/data
chown -R apache:apache /var/www/html/nextcloud/
find /var/www/html/nextcloud/ -type d -exec chmod 750 {} \; 
find /var/www/html/nextcloud/ -type f -exec chmod 640 {} \;

Now create configuration file for nextcloud in httpd:

vim /etc/httpd/conf.d/nextcloud.conf
<VirtualHost *:80>
  DocumentRoot /var/www/html/nextcloud/

  <Directory /var/www/html/nextcloud/>
    Require all granted
    AllowOverride All
    Options FollowSymLinks MultiViews

    <IfModule mod_dav.c>
      Dav off

apachectl graceful

Refer to nextcloud admin manual, you can run into permissions problems. Run these commands as root to adjust permissions:

semanage fcontext -a -t httpd_sys_rw_content_t '/var/www/html/nextcloud/data(/.*)?'
semanage fcontext -a -t httpd_sys_rw_content_t '/var/www/html/nextcloud/config(/.*)?'
semanage fcontext -a -t httpd_sys_rw_content_t '/var/www/html/nextcloud/apps(/.*)?'
semanage fcontext -a -t httpd_sys_rw_content_t '/var/www/html/nextcloud/.htaccess'
semanage fcontext -a -t httpd_sys_rw_content_t '/var/www/html/nextcloud/.user.ini'
restorecon -Rv '/var/www/html/nextcloud/'

If you see error “-bash: semanage: command not found”, install packages:

dnf provides /usr/sbin/semanage
dnf install policycoreutils-python-utils-2.9-3.el8_1.1.noarch

Now, we can check via built-in php scripts, in what state we are:

cd /var/www/html/nextcloud/
sudo -u apache php occ -h
sudo -u apache php occ -V
sudo -u apache php occ status

And finally, we can access our nextcloud and set up administrators password via our web: http://you-ip/

If you see default httpd welcome page, disable all lines in: /etc/httpd/conf.d/welcome.conf
Now you must complete the installation via web interface. Set Administrator’s password and locate to MariaDB with used credentials:

Database user: nextclouduser
Database password: YOURPASSWORD
Database name: nextcloud
host: localhost

In settings of nextcloud, go to section Administration > Overview. You can see some problems. If so, try to fix it. I had three problems. No apcu memory cache configured. So add at nextcloud config.php:

'memcache.local' => '\OC\Memcache\APCu',

Then I must edit som php variables, to set properly opcache: edit and adjust:

vim /etc/php.d/10-opcache.ini

Then I must edit httpd setting, because .htaccess wont working. So change apache config:

vim /etc/httpd/conf/httpd.conf

section: Directory "/var/www/html"
AllowOverride None
change to: 
AllowOverride All

And gracefuly restart apache:

apachectl graceful

Next, I find out, that my nextcloud instance cannot connect to internet and checks for update. I think, that this is on selinux (enforcing mode). So run check and find out, what is happening:

sealert -a /var/log/audit/audit.log

And the result:

SELinux is preventing /usr/sbin/php-fpm from name_connect access on the tcp_socket port 80
Additional Information:
Source Context                system_u:system_r:httpd_t:s0
Source Path                   /usr/sbin/php-fpm
Port                          80
Selinux Enabled               True
Policy Type                   targeted
Enforcing Mode                Enforcing
If you believe that php-fpm should be allowed name_connect access on the port 80 tcp_socket by default.
If you want to allow httpd to can network connect
Then you must tell SELinux about this by enabling the 'httpd_can_network_connect' boolean.

So I allow httpd to can network connect via:

setsebool -P httpd_can_network_connect 1

And that is complete. If you wont secure http (https), try to find out another post on this page.

Have fun

Total Page Visits: 88929 - Today Page Visits: 22

How to set up nfs server on centos 7/8, and display content via httpd

Sometimes I need to use fast, simple and no-password storage over the network in bash, or an ISO storage for Xenserver. So nfs sharing is the best way for this.  I have a linux machine with centos 7 and available storage of 1,5TB disk. So, prepare the disk:

fdisk -l /dev/xvdb
> n (new partition), and use default options. The use -t (change partition ID) and change it to 83 (Linux). The use -w (write)
mkfs.xfs /dev/xvdb1
mkdir /mnt/nfs
mount /dev/xvdb1 /mnt/nfs/

If everything is OK, edit /etc/fstab to automount this partition to ours folder, and add this line:

/dev/xvdb1 /mnt/nfs xfs defaults,nosuid,noatime,nodiratime 0 0

The install package nfs-utils, for nfs server:

yum -y install nfs-utils

And allow nfs service in firewalld:

firewall-cmd --permanent --zone=public --add-service=nfs
firewall-cmd --reload
#if sometimes on clients don't working showmount, and it create an error:
showmount -e
rpc mount export: RPC: Unable to receive; errno = No route to host
#we must add another ports to firewall:
firewall-cmd --permanent --zone=public --add-service=rpc-bind firewall-cmd --permanent --zone=public --add-service=mountd
firewall-cmd --reload

And uncoment this lines in: /etc/sysconfig/nfs (this is no applicable at Centos 8)


Now enable nfs-server to run after poweron server and start it:

systemctl enable nfs-server.service
systemctl start nfs-server.service

Now we must prepare this folder with this permissions, for read and write for everybody: (this is no applicable at Centos 8)

chown nfsnobody:nfsnobody /mnt/nfs/ -R
chmod 755 /mnt/nfs/

And edit file /etc/exports for this folder to by allowed for everybody in network:

/mnt/nfs *(rw,sync,no_root_squash,no_all_squash)

And apply this change:

exportfs -arv

We can see our settings with command “exportfs”:

/mnt/nfs        <world>

And from other linux machine, we can mount this folder:

mount /mnt/nfs/
#see this disk report space
df -h
Filesystem            Size  Used Avail Use% Mounted on
                      1.5T  200G  1.3T  14% /mnt/nfs

And we can test it with 1GB file:

dd if=/dev/zero of=/mnt/nfs/1gb bs=1M count=1000
1048576000 bytes (1.0 GB) copied, 16.4533 s, 63.7 MB/s
ls -lah /mnt/nfs/
drwxr-xr-x. 18 nfsnobody nfsnobody  4.0K Feb 28 10:47 .
drwxr-xr-x.  3 root      root       4.0K Feb 28 10:24 ..
-rw-r--r--.  1 root      root      1000M Feb 28 10:47 1gb

Now we can continue with installing apache web server:

yum install httpd -y
systemctl enable httpd.service
firewall-cmd --add-service=http --permanent
firewall-cmd --reload

Now, we create an configuration file for one foler from nfs storage:

vim /etc/httpd/conf.d/
<VirtualHost *:80>
DocumentRoot "/mnt/nfs/kadeco/installs"
<Directory "/mnt/nfs/kadeco/installs">
AllowOverride All
Require all granted
Options Indexes
ErrorLog /var/log/httpd/
CustomLog /var/log/httpd/ common

But we cannot serve this directory:

AH01276: Cannot serve directory /mnt/nfs/kadeco/installs: No matching DirectoryIndex (index.html) found, and server-generated directory index forbidden by Options directive

So, we install som softvare to modify file and folders context with selinux:

yum install setroubleshoot

And change context to this folder:

semanage fcontext -a -t httpd_sys_content_t "/mnt/nfs/kadeco/installs(/.*)?"
restorecon -R /mnt/nfs/kadeco/installs
rm /etc/httpd/conf.d/welcome.conf
systemctl restart httpd.service

Have a fun 🙂

Total Page Visits: 88929 - Today Page Visits: 22