Installing Basic Services
This document will guide you through setting up core services. it is based on AlmaLinux 9 installed on a virtual server. The server host has a firewall which makes firewalld redundant, so they disabled it in the install. The host also disabled SELinux as it can cause issues. However, I prefer to use both so these instructions reenable them.
After completing this setup you should have a fully up-to-date system with:
- The EPEL repo installed
- SELinux installed and operating in Permissive mode (which will not break anything).
- firewalld installed and allowing only designated ports.
- Letsencrypt providing valid SSL certificates, which are automatically renewed.
- Cockpit installed and secured with a valid certificate.
- Postix and dovecot installed to provide email.
- Miriadb database installed and secured.
- Apache installed and configured.
- WordPress installed on one virtual host.
- Basic WordPress hardening in place
Installing the EPEL Repository and Updating Current Packages
The EPEL repo adds additional packages which will be needed later. -ADD Remi & the latest PHP
sudo dnf install epel-release -y
sudo dnf update -y
Installing Core Services
SELinux
The default installation typically enables SELinux. However, the host’s install did not. To enable it (in permissive mode), first check that it’s not inhibited in grub:
cat /proc/cmdline
Look for selinux=0 or security=none. If either is present, SELinux will be disabled no matter what /etc/selinux/config says. If that’s the case:
sudo grubby --update-kernel=ALL --remove-args=selinux
sudo grub2-mkconfig -o /boot/grub2/grub.cfg
sudo dnf install selinux-policy-targeted
Ensure /etc/selinux/config has:
SELINUX=permissive
SELINUXTYPE=targeted
Then reboot.
sudo reboot now
Once the system is back, check
getenforce
it should return Permissive.
Set the SELinux types, permissions etc.
Cockpit will alert to any SELinux issues and suggest solutions. These should be reviewed before implementation. However, the following should resolve many issues:
sudo setsebool -P httpd_unified 1
sudo setsebool -P httpd_can_network_connect 1
sudo setsebool -P httpd_can_network_relay 1
sudo setsebool -P httpd_graceful_shutdown 1
sudo setsebool -P nis_enabled 1
Install cockpit (admin webserver).
sudo dnf install cockpit -y
sudo systemctl enable --now cockpit.socket
Enable the firewall (if it’s not already enabled), and allow cockpit access.
sudo systemctl enable --now firewalld
sudo firewall-cmd --permanent --zone=public --add-service=cockpit
sudo firewall-cmd --reload
Install postfix (mail server), mariadb (mysql database), apache (web server), php and core modules and certbot
sudo dnf install postfix mariadb-server mariadb httpd php php-mysqlnd php-fpm php-cli -y
sudo dnf install php-common php-opcache php-mbstring php-gd php-xml php-intl php-soap -y
sudo dnf install php-zip php-pear php-devel ImageMagick ImageMagick-devel s-nail -y
sudo dnf certbot python3-certbot-apache cyrus-sasl cyrus-sasl-plain -y
Enable and start the services
sudo systemctl enable --now httpd mariadb php-fpm certbot-renew.timer
Secure mysql
sudo mysql_secure_installation
Follow the prompts to
- Set root user password
- Use sockets
- Remove anonymous users
- Disallow root login remotely
- Remove test database
- Reload privilege tables
Setting Up Postfix
We need rspamd for message signing and mail verification, but that’s not in the standard repos. So we can add the official respond repo.
sudo tee /etc/yum.repos.d/rspamd.repo > /dev/null <<'EOF'
[rspamd]
name=Rspamd for Centos 9 x86_64
baseurl=https://rspamd.com/rpm/centos-9/x86_64/
gpgcheck=1
enabled=1
gpgkey=https://rspamd.com/rpm/gpg.key
EOF
Clear and refresh the dnf metadata and install and enable rspamd and dovecot
sudo dnf clean all
sudo dnf makecache
sudo dnf install rspamd dovecot -y
sudo systemctl enable --now postfix dovecot rspamd
Create the basic Postfix config
Save the installation file for reference
sudo mv /etc/postfix/main.cf{,.inst}
Create a new /etc/postfix/main.cf file containing:
# ===========================
# Postfix Basic Configuration
# ===========================
# Ensures modern Postfix behavior
compatibility_level = 2
# Paths (default on AlmaLinux, retained for clarity or packaging)
queue_directory = /var/spool/postfix
command_directory = /usr/sbin
daemon_directory = /usr/libexec/postfix
data_directory = /var/lib/postfix
sendmail_path = /usr/sbin/sendmail.postfix
newaliases_path = /usr/bin/newaliases.postfix
mailq_path = /usr/bin/mailq.postfix
# Optional documentation paths
html_directory = no
manpage_directory = /usr/share/man
sample_directory = /usr/share/doc/postfix/samples
readme_directory = /usr/share/doc/postfix/README_FILES
meta_directory = /etc/postfix
shlib_directory = /usr/lib64/postfix
# Security-related settings
setgid_group = postdrop
mail_owner = postfix
# ===========================
# Host & Domain Settings
# ===========================
# Fully-qualified hostname
myhostname = alma.adpca.org
# Local domain name
mydomain = adpca.org
# Determines domain name used in outgoing local mail
# Use $myhostname for 'user@alma.adpca.org' or $mydomain for 'user@adpca.org'
myorigin = $myhostname
# Domains considered local — mail addressed to these is delivered locally
mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain
# Accept connections on all interfaces and use IPv4 + IPv6
inet_interfaces = all
inet_protocols = all
# Trust only local machine for relaying mail
mynetworks = 127.0.0.0/8
# Reject mail for unknown users with a hard error
unknown_local_recipient_reject_code = 550
# ===========================
# Alias Maps
# ===========================
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
# Run `newaliases` after modifying /etc/aliases
# ===========================
# MailerSend Relay Setup
# ===========================
# External SMTP relay (MailerSend)
relayhost = [smtp.mailersend.net]:587
# Enable TLS for outbound connections
smtp_use_tls = yes
smtp_tls_security_level = encrypt
smtp_tls_note_starttls_offer = yes
# Trusted CA certificates (system-wide)
smtp_tls_CAfile = /etc/ssl/certs/ca-bundle.crt
smtp_tls_CApath = /etc/pki/tls/certs
# Require modern encryption
smtp_tls_mandatory_protocols = !SSLv2, !SSLv3
smtp_tls_mandatory_ciphers = medium
# Performance: cache TLS sessions
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
# ===========================
# SMTP AUTH for MailerSend
# ===========================
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
smtp_sasl_tls_security_options = noanonymous
# ===========================
# Inbound TLS (Optional)
# Only needed if accepting incoming mail over TLS
# ===========================
smtpd_tls_cert_file = /etc/pki/tls/certs/postfix.pem
smtpd_tls_key_file = /etc/pki/tls/private/postfix.key
smtpd_tls_security_level = may # Allow STARTTLS if supported
# ===========================
# Debugging (Optional)
# Useful for SMTP debugging sessions
# ===========================
#debug_peer_level = 2
#debugger_command =
# PATH=/bin:/usr/bin:/usr/local/bin:/usr/X11R6/bin
# ddd $daemon_directory/$process_name $process_id & sleep 5
Configure Dovecot (Mail delivery and IMAP/POP3)
Edit /etc/dovecot/conf.d/10
mail_location = maildir:~/Maildir
#If you're using virtual mail users, this would instead be:
#mail_location = maildir:/var/mail/vhosts/%d/%n/Maildir
# Optional for performance and clarity
mail_privileged_group = mail
Edit /etc/dovecot/conf.d/10-auth.conf to include:
disable_plaintext_auth = yes
auth_mechanisms = plain login
!include auth-system.conf.ext
Create a mail directory for each user:
mkdir -p /home/username/Maildir
chown -R username:username /home/username/Maildir
Enable protocols
Edit /etc/dovecot/conf.d/10-master.conf to include
#Enable IMAP and the auth socket for Postfix to use if needed:
service imap-login {
inet_listener imap {
port = 143
}
inet_listener imaps {
port = 993
ssl = yes
}
}
service auth {
unix_listener /var/spool/postfix/private/auth {
mode = 0666
user = postfix
group = postfix
}
}
# The auth socket allows Postfix to use Dovecot for authentication if you configure SASL
# auth that way. Optional if you're doing SMTP auth directly via sasl_passwd.
service lmtp {
unix_listener /var/spool/postfix/private/dovecot-lmtp {
mode = 0600
user = postfix
group = postfix
}
}
Open the ports
sudo firewall-cmd --add-port=110/tcp --permanent
sudo firewall-cmd --add-port=143/tcp --permanent
sudo firewall-cmd --add-port=993/tcp --permanent
sudo firewall-cmd --add-port=995/tcp --permanent
You might need to open them in the host. too.
Add the certificates
ssl = required
ssl_cert = </etc/letsencrypt/live/alma.adpca.org/fullchain.pem
ssl_key = </etc/letsencrypt/live/alma.adpca.org/privkey.pem
ssl_cipher_list = PROFILE=SYSTEM
ssl_min_protocol = TLSv1.2
ssl_prefer_server_ciphers = yes
Add a hook to certbot auto renewal to set the required permissions
Create a group that can access certificates and add dovecot to that group. Set the initial permissions.
sudo groupadd ssl-cert
sudo usermod -aG ssl-cert dovecot
chmod 750 /etc/letsencrypt/ /etc/letsencrypt/live/ /etc/letsencrypt/archive/
chgrp ssl-cert /etc/letsencrypt/ /etc/letsencrypt/live/ /etc/letsencrypt/archive
Add the following line to /usr/local/sbin/fix-cert-permissions.s
deploy-hook = /usr/local/sbin/fix-cert-permissions.sh
Then create the script /usr/local/sbin/fix-cert-permissions.sh
#!/bin/bash
# Variables
DOMAIN="alma.adpca.org"
SSL_GROUP="ssl-cert"
# Paths
# LIVE_DIR="/etc/letsencrypt/live/$DOMAIN"
# ARCHIVE_DIR="/etc/letsencrypt/archive/$DOMAIN"
COCKPIT_DIR=/etc/cockpit/ws-certs.d
LIVE_DIR="/etc/letsencrypt/live"
ARCHIVE_DIR="/etc/letsencrypt/archive"
# Fix permissions on live directory and its contents
chgrp -R "$SSL_GROUP" "$LIVE_DIR"
chmod -R 640 "$LIVE_DIR"/*/*.pem
find "$LIVE_DIR" -type d -exec chmod 750 {} \;
# Fix permissions on archive directory and its contents
chgrp -R "$SSL_GROUP" "$ARCHIVE_DIR"
chmod -R 640 "$ARCHIVE_DIR"/*/*.pem
find "$ARCHIVE_DIR" -type d -exec chmod 750 {} \;
# Rebuild Cockpit cert
sudo cp $LIVE_DIR/$DOMAIN/fullchain.pem $COCKPIT_DIR/99-letsencrypt.cert
sudo cp $LIVE_DIR/$DOMAIN/privkey.pem $COCKPIT_DIR/99-letsencrypt.key
chmod 600 $COCKPIT_DIR/99-letsencrypt.cert
# Restart Cockpit to pick up the new cert
systemctl restart cockpit
echo Certificate Permissions fixed and Cockpit certificate created.
This script also creates the cockpit certificates. Run the script to set the required permissions and create the cockpit keys.
Tell Postfix to use Dovecot LMTP
Add the following to /etc/postfix/main.cf:
virtual_transport = lmtp:unix:private/dovecot-lmtp
Generate DKIM keys and publish DNS record
Generate and secure the keys
sudo mkdir -p /etc/rspamd/dkim/adpca.org
sudo rspamadm dkim_keygen -b 2048 -s default -d adpca.org -k \
/etc/rspamd/dkim/adpca.org/default.key
sudo chown -R _rspamd:_rspamd /etc/rspamd/dkim/adpca.org
sudo chmod 600 /etc/rspamd/dkim/adpca.org/default.key
This will output something like
default._domainkey IN TXT ( "v=DKIM1; k=rsa; "
"p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtLlz8vwzSFZonIyLQDBiXynxUyDNuyt6eBAcNNb5Fyh9wEKCCy6W9Bs633Y6kFwWKVHCz+0l7pS0Tt2tU0Hxxh0XZjEtQAOJCCSkMmByCDAYYubAicQR+QKZEGh2H1dldHmj0i3xHwrUZ4yDB9YZ74uj5PgYAYwerPmrwkwd82wowzY/QMHpVOgPkQpe6IIqxs3ohZEuWhDXZniM2"
"sw0Kzewd6K+7U3jY5m19Qm/dlDlu1xwYknI277mxdNlHvwpGEWiJIziOgQ9Eue5wSeKhmmCdsNj5LoYG2dRXa9pcrHH08JjLnFN+j1DVjbgUs7IWEvx3QXOg3LZI5/ko7jt+wIDAQAB"
) ;
The public key is the value of the p= parameter inside the TXT record Here, the public key (shown in bold) is the whole big base64 string after p= and before the closing parenthesis . It should be formatted one single line with no line breaks, like this:
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtLlz8vwzSFZonIyLQDBiXynxUyDNuyt6eBAcNNb5Fyh9wEKCCy6W9Bs633Y6kFwWKVHCz+0l7pS0Tt2tU0Hxxh0XZjEtQAOJCCSkMmByCDAYYubAicQR+QKZEGh2H1dldHmj0i3xHwrUZ4yDB9YZ74uj5PgYAYwerPmrwkwd82wowzY/QMHpVOgPkQpe6IIqxs3ohZEuWhDXZniM2sw0Kzewd6K+7U3jY5m19Qm/dlDlu1xwYknI277mxdNlHvwpGEWiJIziOgQ9Eue5wSeKhmmCdsNj5LoYG2dRXa9pcrHH08JjLnFN+j1DVjbgUs7IWEvx3QXOg3LZI5/ko7jt+wIDAQAB
Add a TXT record to your DNS server. The record name should be :default._domainkey.adpca.org: and the value
“v=DKIM1; k=rsa; p=PUBLIC_KEY_HERE“.
In this case the value would be:
v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtLlz8vwzSFZonIyLQDBiXynxUyDNuyt6eBAcNNb5Fyh9wEKCCy6W9Bs633Y6kFwWKVHCz+0l7pS0Tt2tU0Hxxh0XZjEtQAOJCCSkMmByCDAYYubAicQR+QKZEGh2H1dldHmj0i3xHwrUZ4yDB9YZ74uj5PgYAYwerPmrwkwd82wowzY/QMHpVOgPkQpe6IIqxs3ohZEuWhDXZniM2sw0Kzewd6K+7U3jY5m19Qm/dlDlu1xwYknI277mxdNlHvwpGEWiJIziOgQ9Eue5wSeKhmmCdsNj5LoYG2dRXa9pcrHH08JjLnFN+j1DVjbgUs7IWEvx3QXOg3LZI5/ko7jt+wIDAQAB
Important: Make sure the value is one continuous string with no line breaks when adding the record.
Create the DNS Records for adpca.org and subdomains
| Type | Name | Value / Content | Notes |
| A | mail.adpca.org | Your mail server IP | Host for mail services |
| MX | adpca.org | mail.adpca.org (priority 10) | Receive mail for domain |
| MX | *.adpca.org | mail.adpca.org (priority 10) | Receive mail for subdomains |
| TXT | adpca.org | “v=spf1 mx ip4:\ -all” | SPF record |
| TXT | default._domainkey.adpca.org | DKIM public key as above | For DKIM verification |
| TXT | _dmarc.adpca.org | “v=DMARC1; p=quarantine; rua=mailto\:postmaster\@adpca.org” | DMARC policy |
Final steps
Restart the services
sudo systemctl restart postfix dovecot rspamd
Test mail sending and receiving.
Check mail logs for errors:
sudo journalctl -u postfix
sudo journalctl -u dovecot
sudo journalctl -u rspamd
Setting up Apache and installing WordPress
We have chosen to configure apache (httpd) virtual hosts using the following paths:
| Document root | /var/www/example.com | The website files go here |
| HTTP Config | /etc/httpd/conf.d/example.com.conf | The basic site config |
| HTTPS Config | /etc/httpd/conf.d/example.com-le-ssl.conf | Created by certbot |
| Access Log | /var/log/httpd/example.com_access.log | |
| Error Log | /var/log/httpd/example.com_error.log |
Creating the virtual host
Create the HTTP config file, which should contain at least:
<VirtualHost *:80>
ServerName example.com
DocumentRoot /var/www/example.com
<Directory /var/www/example.com>
AllowOverride All
</Directory>
ErrorLog /var/log/httpd/example.com_error.log
CustomLog /var/log/httpd/example.com_access.log combined
</VirtualHost>
Reload Apache to enable the site
sudo systemctl reload httpd
Create the document root
n this case, we are going to download WordPress and rename the extracted file to create the document root.
cd ~ # Change to your home directory
curl -O https://wordpress.org/latest.tar.gz
sudo tar -xvzf latest.tar.gz
sudo mv wordpress /var/www/example.com
sudo chown -R apache:apache /var/www/example.com
sudo chmod -R 755 /var/www/example.com
rm latest.tar.gz
If you are not using WordPress, then:
sudo mkdir /var/www/example.com
sudo chown -R apache:apache /var/www/example.com
sudo chmod -R 755 /var/www/example.com
And add your files into /var/www/example.com
Obtain an SSL Certificate and Install
Certbot will take care of this for you.
sudo certbot --apache -d example.com
Follow the prompts and allow Certbot to update the virtual host.
If you are not using WordPress, you can skip the rest of the instructions. Place your files into the document root and check that you can access them.
Create the WordPress Database
sudo mysql -u root -p
CREATE DATABASE wordpress;
CREATE USER 'wpuser'@'localhost' IDENTIFIED BY 'strongpassword';
GRANT ALL PRIVILEGES ON wordpress.* TO 'wpuser'@'localhost';
FLUSH PRIVILEGES;
EXIT;
Replace the database name (wordpress), user (wpuser) and password with your unique values. Do not change “@’localhost”.
Configure WordPress
Go to https://example.com and follow the prompts to configure WordPress
Secure WordPress
Set the file permissions
sudo find /var/www/example.com/ -type d -exec chmod 755 {} \;
sudo find /var/www/example.com/ -type f -exec chmod 644 {} \;
sudo chmod 600 /var/www/example.com/wp-config.php
Change the security keys
Go to https://api.wordpress.org/secret-key/1.1/salt/ to create random ‘salt’ values, then replace the values in the WordPress config with your new values.
sudo nano /var/www/example.com/wp-config.php