Grommunio Mail Server: Difference between revisions
No edit summary |
Prabuanand (talk | contribs) m (Added Categories Category:Server and Category:Mail) |
||
(71 intermediate revisions by one other user not shown) | |||
Line 1: | Line 1: | ||
This tutorial covers the steps to set up a mail server on '''Alpine Linux''' using '''grommunio''', an open-source groupware solution that supports email, calendar, and task management. Grommunio stands out with its unique '''MAPI''' support, enabling seamless integration with Microsoft Outlook and other MAPI clients, making it an ideal choice for open-source alternatives to proprietary systems. The setup includes essential components like '''MariaDB''', '''Nginx''', '''PHP''', and '''Postfix''' to create a fully functional mail server. Follow along to build a secure, scalable communication platform using grommunio's powerful features. | |||
== Prerequisites == | |||
Before proceeding with the installation, ensure you have a fresh Alpine Linux system setup. You'll need root privileges to execute these commands. | |||
== Procedure == | |||
# Install and configure MariaDB | |||
# MariaDB performance tuning (optional) | |||
# Install and configure Nginx | |||
# Install and configure PHP | |||
# Install and configure Postfix | |||
# Install and configure Grommunio | |||
# Configure Valkey (Redis replacement) | |||
# Install and configure Rspamd | |||
# Finalize and verify installation | |||
== 1. Install and Configure MariaDB == | |||
=== Install MariaDB === | |||
To start, install MariaDB and necessary utilities | |||
apk add mariadb mariadb-client mariadb-server-utils | |||
Define the variables used later in the setup setup and configuration. | |||
The default datapath is `/var/lib/mysql` and contains the entire database. If you prefer another location define it here and create a symlink. In any case, the grommunio database will be rather small as it only contains the configuration. | |||
DB_DATA_PATH="/srv/mysql" | |||
DB_ROOT_PASS="Passw0rd1" | |||
DB_USER="admin" | |||
DB_PASS="Passw0rd2" | |||
Setup the default system tables: | |||
== | sudo mysql_install_db --user=mysql --datadir=${DB_DATA_PATH} | ||
Set a symlink to the standard mysql data directory for compatibility reason: | |||
ln -s /srv/mysql /var/lib/mysql | |||
Restart service: | |||
rc-service mariadb restart | |||
Run the built-in security script: | |||
Set the new root password to the one defined above and answer everything with 'y' | |||
sudo mysql_secure_installation | |||
=== | === Create MariaDB User === | ||
Create a new user for Grommunio and assign privileges: | Create a new user for Grommunio and assign privileges: | ||
echo "GRANT ALL ON *.* TO ${DB_USER}@'127.0.0.1' IDENTIFIED BY '${DB_PASS}' WITH GRANT OPTION;" > /tmp/sql | |||
echo "GRANT ALL ON *.* TO ${DB_USER}@'127.0.0.1' IDENTIFIED BY '${DB_PASS}' WITH GRANT OPTION;" > /tmp/sql | echo "GRANT ALL ON *.* TO ${DB_USER}@'localhost' IDENTIFIED BY '${DB_PASS}' WITH GRANT OPTION;" >> /tmp/sql | ||
echo "GRANT ALL ON *.* TO ${DB_USER}@'localhost' IDENTIFIED BY '${DB_PASS}' WITH GRANT OPTION;" >> /tmp/sql | echo "GRANT ALL ON *.* TO ${DB_USER}@'::1' IDENTIFIED BY '${DB_PASS}' WITH GRANT OPTION;" >> /tmp/sql | ||
echo "GRANT ALL ON *.* TO ${DB_USER}@'::1' IDENTIFIED BY '${DB_PASS}' WITH GRANT OPTION;" >> /tmp/sql | echo "DELETE FROM mysql.user WHERE User='';" >> /tmp/sql | ||
echo "DELETE FROM mysql.user WHERE User='';" >> /tmp/sql | echo "FLUSH PRIVILEGES;" >> /tmp/sql | ||
echo "FLUSH PRIVILEGES;" >> /tmp/sql | cat /tmp/sql | mysql -u root --password="${DB_ROOT_PASS}" | ||
cat /tmp/sql | mysql -u root --password="${DB_ROOT_PASS}" | |||
=== | === Configure MariaDB === | ||
Edit the MariaDB configuration for better performance: | Edit the MariaDB configuration for better performance: | ||
vi /etc/my.cnf.d/mariadb-server.cnf | |||
vi /etc/my.cnf.d/mariadb-server.cnf | |||
Add the following configuration: | Add the following configuration: | ||
[mysqld] | |||
[mysqld] | #skip-networking | ||
innodb_log_buffer_size=16M | |||
innodb_log_file_size=32M | ## Configuration for grommunio (most values are default) | ||
innodb_read_io_threads=4 | innodb_log_buffer_size=16M | ||
innodb_write_io_threads=4 | innodb_log_file_size=32M | ||
join_buffer_size=512K | innodb_read_io_threads=4 | ||
query_cache_size=0 | innodb_write_io_threads=4 | ||
query_cache_type=0 | |||
query_cache_limit=2M | join_buffer_size=512K | ||
performance_schema=ON | query_cache_size=0 | ||
bind-address = 127.0.0.1 | query_cache_type=0 | ||
skip-name-resolve=ON | query_cache_limit=2M | ||
# Activate performance schema | |||
performance_schema=ON | |||
# Bind to localhost only | |||
bind-address = 127.0.0.1 | |||
# Disable DNS lookups as we only use localhost connections | |||
skip-name-resolve=ON | |||
Create a default charset configuration for MariaDB: | Create a default charset configuration for MariaDB: | ||
cat > /etc/my.cnf.d/mariadb-server-default-charset.cnf << EOF | |||
cat > /etc/my.cnf.d/mariadb-server-default-charset.cnf << EOF | [client] | ||
[client] | default-character-set = utf8mb4 | ||
default-character-set = utf8mb4 | |||
[mysqld] | |||
collation_server = utf8mb4_general_ci | |||
character_set_server = utf8mb4 | |||
[mysql] | |||
default-character-set = utf8mb4 | |||
EOF | |||
Restart MariaDB and enable it to start on boot: | |||
rc-update add mariadb default | |||
rc-service mariadb restart | |||
=== | === Verify MariaDB Setup === | ||
Check if the MariaDB listener is running and bound to the correct | Check if the MariaDB listener is running and bound to the correct interface (127.0.0.1): | ||
ss -tulpn | |||
ss -tulpn | |||
=== | === Create Grommunio Database === | ||
Define the database parameters and create the Grommunio database: | Define the database parameters and create the Grommunio database: | ||
MYSQL_HOST="localhost" | |||
MYSQL_HOST="localhost" | MYSQL_USER="grommunio" | ||
MYSQL_USER="grommunio" | MYSQL_PASS="Passw0rd3" | ||
MYSQL_PASS="Passw0rd3" | MYSQL_DB="grommunio" | ||
MYSQL_DB="grommunio" | |||
echo "create database $MYSQL_DB character set 'utf8mb4';" > /tmp/sql | |||
echo "grant select, insert, update, delete, create, drop, index, alter, create temporary tables, lock tables on $MYSQL_DB.* TO $MYSQL_USER@$MYSQL_HOST identified by '$MYSQL_PASS';" >> /tmp/sql | |||
echo "flush privileges;" >> /tmp/sql | |||
cat /tmp/sql | mysql -u admin --password="${DB_PASS}" | |||
Test the database connection: | Test the database connection: | ||
mysql -hlocalhost -u grommunio -p${MYSQL_PASS} grommunio | |||
mysql -hlocalhost -u grommunio -p${MYSQL_PASS} grommunio | |||
== 2. MariaDB Performance Tuning (Optional) == | == 2. MariaDB Performance Tuning (Optional) == | ||
To assist us in checking and tuning the configuration as per our specific needs, we can install mysqltuner, a script that will provide suggestions to improve the performance of our database server and increase its stability | |||
=== Download MySQLTuner === | |||
wget -v https://raw.githubusercontent.com/major/MySQLTuner-perl/master/mysqltuner.pl -O /tmp/mysqltuner.pl | |||
Move it to the correct directory and set permissions: | |||
mv /tmp/mysqltuner.pl /usr/local/bin/mysqltuner.pl | |||
chmod 755 /usr/local/bin/mysqltuner.pl | |||
=== Install perl === | |||
NOTE: The perl-doc package is installed because it's the only one containing the perldiag.pod required by mysqltuner.pl | |||
apk add perl perl-doc | |||
=== Execute the script === | |||
You will be prompted to enter the credentials of your administrative MariaDB account: | |||
/usr/local/bin/mysqltuner.pl --user admin --pass ${DB_PASS} | |||
== 3. Install and Configure Nginx == | == 3. Install and Configure Nginx == | ||
=== | === Install Nginx === | ||
Install the necessary Nginx modules: | Install the necessary Nginx modules: | ||
apk add nginx nginx-mod-http-headers-more nginx-mod-http-vts nginx-mod-http-brotli | |||
apk add nginx nginx-mod-http-headers-more nginx-mod-http-vts nginx-mod-http-brotli | |||
=== | === Configure Nginx === | ||
Backup the original Nginx configuration and edit it for security headers and TLS settings: | Backup the original Nginx configuration and edit it for security headers and TLS settings: | ||
cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.orig | |||
cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.orig | vi /etc/nginx/nginx.conf | ||
vi /etc/nginx/nginx.conf | |||
Add the following configuration: | Add the following configuration: | ||
#error_log /var/log/nginx/error.log warn; | |||
error_log syslog:server=unix:/dev/log,facility=local2,nohostname warn; | error_log syslog:server=unix:/dev/log,facility=local2,nohostname warn; | ||
more_set_headers "Strict-Transport-Security : max-age=2592000; includeSubDomains;"; | |||
more_set_headers "X-Frame-Options : SAMEORIGIN"; | ### Common headers for security | ||
more_set_headers "Content-Security-Policy : default-src https: data: 'unsafe-inline' 'unsafe-eval' always"; | # TODO: Set temporary to a lower value until we are sure it's working | ||
more_set_headers "X-Xss-Protection : 1; mode=block"; | #more_set_headers "Strict-Transport-Security : max-age=15768000; includeSubDomains; preload"; | ||
more_set_headers "X-Content-Type-Options : nosniff"; | more_set_headers "Strict-Transport-Security : max-age=2592000; includeSubDomains;"; | ||
more_set_headers "Referrer-Policy : strict-origin-when-cross-origin"; | more_set_headers "X-Frame-Options : SAMEORIGIN"; | ||
more_set_headers "Server : Follow the white rabbit."; | more_set_headers "Content-Security-Policy : default-src https: data: 'unsafe-inline' 'unsafe-eval' always"; | ||
more_set_headers "X-Xss-Protection : 1; mode=block"; | |||
more_set_headers "X-Content-Type-Options : nosniff"; | |||
more_set_headers "Referrer-Policy : strict-origin-when-cross-origin"; | |||
more_set_headers "Server : Follow the white rabbit."; | |||
### TLS settings | |||
ssl_protocols TLSv1.2 TLSv1.3; | |||
ssl_session_cache shared:SSL:1m; | |||
ssl_session_timeout 5m; | |||
log_format main_ssl '$remote_addr - $remote_user [$time_local] "$request" ' | |||
'$status $body_bytes_sent "$http_referer" ' | |||
'"$http_user_agent" "$http_x_forwarded_for" ' | |||
'client_ciphers="$ssl_ciphers" client_curves="$ssl_curves"'; | |||
#access_log /var/log/nginx/access.log main_ssl; # Disabled for performance | |||
#access_log syslog:server=unix:/dev/log,facility=local2,nohostname main; # Disabled for performance | |||
access_log off; | |||
Disable the default page by renaming it: | |||
mv /etc/nginx/http.d/default.conf /etc/nginx/http.d/default.orig | |||
Restart Nginx and enable it to start on boot: | Restart Nginx and enable it to start on boot: | ||
rc-update add nginx | |||
rc-update add nginx | rc-service nginx restart | ||
service nginx restart | |||
== 4. Install and Configure PHP == | == 4. Install and Configure PHP == | ||
=== | === Install PHP === | ||
Install the | Install the basic php packages. The modules are installed as grommunio dependencies: | ||
apk add php83 php83-fpm | |||
Disable default fpm conf: | |||
mv /etc/php83/php-fpm.d/www.conf /etc/php83/php-fpm.d/www.conf.default | |||
=== Harden PHP Configuration === | |||
Backup config file /etc/php83/php.ini | |||
cp /etc/php83/php.ini /etc/php83/php-fpm.d/php.ini.orig | |||
Disable remote PHP code execution: | |||
sed 's/^;\?\(allow_url_fopen\).*/\1 = Off/' -i /etc/php83/php.ini | |||
sed 's/^;\?\(allow_url_include\).*/\1 = Off/' -i /etc/php83/php.ini | |||
Disable information leakage: | |||
sed 's/^;\?\(expose_php\).*/\1 = Off/' -i /etc/php83/php.ini | |||
Configure Error handling: | |||
sed 's/^;\?\(display_errors\).*/\1 = Off/' -i /etc/php83/php.ini | |||
sed 's/^;\?\(display_startup_errors\).*/\1 = Off/' -i /etc/php83/php.ini | |||
sed 's/^;\?\(log_errors\).*/\1 = On/' -i /etc/php83/php.ini | |||
sed 's/^;\?\(error_log = syslog\).*/\1/' -i /etc/php83/php.ini | |||
PHP Resource Control (Optional): | |||
#sed 's/^\(max_execution_time\).*/\1 = 25/' -i /etc/php83/php.ini | |||
#sed 's/^\(max_input_time\).*/\1 = 25/' -i /etc/php83/php.ini | |||
#sed 's/^\(memory_limit\).*/\1 = 30M/' -i /etc/php83/php.ini | |||
#sed 's/^\(post_max_size\).*/\1 = 1M/' -i /etc/php83/php.ini | |||
#sed 's/^;\?\(max_input_vars\).*/\1 = 1000/' -i /etc/php83/php.ini | |||
Disable vulnerable functions: | |||
sed 's/^\(disable_functions\).*/\1 = apache_note, apache_setenv, chgrp, curl_multi_exec, define_sys, define_syslog_variables, debugger_off, debugger_on, diskfreespace, _getppid, escapeshellarg, escapeshellcmd, exec, getmyuid, ini_restore, leak, listen, parse_ini_file, passthru, pcntl_alarm, pcntl_async_signals, pcntl_exec, pcntl_fork, pcntl_get_last_error, pcntl_getpriority, pcntl_setpriority, pcntl_signal, pcntl_signal_dispatch, pcntl_signal_get_handler, pcntl_sigprocmask, pcntl_sigtimedwait, pcntl_sigwaitinfo, pcntl_strerror, pcntl_unshare, pcntl_wait, pcntl_waitpid, pcntl_wexitstatus, pcntl_wifcontinued, pcntl_wifexited, pcntl_wifsignaled, pcntl_wifstopped, pcntl_wstopsig, pcntl_wtermsig, posix, posix_ctermid, posix_getcwd, posix_getegid, posix_geteuid, posix_getgid, posix_getgrgid, posix_getgrnam, posix_getgroups, posix_getlogin, posix_getpgid, posix_getpgrp, posix_getpid, posix_getpwnam, posix_getpwuid, posix_getrlimit, posix_getsid, posix_isatty, posix_kill, posix_mkfifo, posix_setegid, posix_seteuid, posix_setgid, posix_setpgid, posix_setsid, posix_setuid, posix_times, posix_ttyname, posix_uname, popen, proc_close, proc_get_status, proc_nice, proc_open, proc_terminate, shell_exec, show_source, system, url_exec/' -i /etc/php83/php.ini | |||
Harden Session Security: | |||
=== | sed 's/^;\?\(session.use_strict_mode\).*/\1 = 1/' -i /etc/php83/php.ini | ||
sed 's/^;\?\(session.use_cookies\).*/\1 = 1/' -i /etc/php83/php.ini | |||
sed 's/^;\?\(session.cookie_secure\).*/\1 = 1/' -i /etc/php83/php.ini | |||
sed 's/^;\?\(session.use_only_cookies\).*/\1 = 1/' -i /etc/php83/php.ini | |||
sed 's/^;\?\(session.cookie_httponly\).*/\1 = 1/' -i /etc/php83/php.ini | |||
== 5. Install and Configure Postfix == | == 5. Install and Configure Postfix == | ||
=== | === Install Postfix === | ||
Install Postfix and | Install the required packages: | ||
apk add postfix postfix-mysql postfix-pcre | |||
=== Configure Postfix === | |||
First backup the original files: | |||
mv /etc/postfix/main.cf /etc/postfix/main.cf.orig | |||
mv /etc/postfix/master.cf /etc/postfix/master.cf.orig | |||
mv /etc/postfix/header_checks /etc/postfix/header_checks.orig | |||
Download the prepared configuration files: | |||
wget https://alpinelinuxsupport.com/downloads/postfix-configuration-files.zip | |||
Copy the prepared configuration files to `/etc/postfix/` and adapt the configuration to your environment. Ensure you change at least the following values: | |||
'''main.cf''': myhostname, mynetworks, smtp_tls_chain_files, smtpd_tls_chain_files, local_recipient_maps, mydestination, | |||
virtual_*, relayhost, mua_sender_restrictions, smtpd_recipient_restrictions | |||
'''master.cf''': smtpd_tls_chain_files=/etc/ssl/private/internal-server-key-and-cert.pem | |||
Create the required postmap files: | |||
newaliases | |||
postmap /etc/postfix/transport | |||
Run Postfix setup: | Run Postfix setup: | ||
newaliases | |||
newaliases | postmap /etc/postfix/transport | ||
postmap /etc/postfix/transport | |||
Enable | Enable the postfix service and restart: | ||
rc-update add postfix | |||
rc-update add postfix | rc-service postfix restart | ||
service postfix restart | |||
=== | === Verify Postfix Logs === | ||
Check the Postfix logs for any errors: | Check the Postfix logs for any errors: | ||
tail -f /var/log/maillog | |||
== 6. Install and Configure Grommunio == | |||
=== Enable IPv6 === | |||
First of all, turn on ipv6 which is mandatory for grommunio daemons | |||
Edit `/etc/hosts` to include IPv6 localhost: | |||
vi /etc/hosts | |||
----- | |||
::1 localhost ipv6-localhost ipv6-loopback | |||
Ensure IPv6 is enabled in `/etc/sysctl.conf`: | |||
sed -i 's/^net\.ipv6\.conf\..*\.disable_ipv6\s=\s1/#&/' /etc/sysctl.conf | |||
sysctl -p | |||
ping ::1 # Test if IPv6 is working | |||
=== Specify Database Parameters === | |||
Specify DB parameters if necessary (already specified above in step 1) | |||
MYSQL_HOST="localhost" | |||
MYSQL_USER="grommunio" | |||
MYSQL_PASS="Passw0rd3" | |||
MYSQL_DB="grommunio" | |||
=== Specify Domain Parameters === | |||
The '''FQDN''' is for example used by Outlook clients to connect. This name will have to be present in the used certificate. | |||
The default '''Mail Domain''' is for example used for Non-Delivery Reports and for generation of some simple TLS certificates. Specify ONLY ONE domain here. | |||
Adjust the following for your specific setup: | |||
FQDN="mail.example.local" | |||
MAILDOMAIN="example.com" | |||
RELAYHOST="123.123.123.1" | |||
ADMIN_PASS="Passw0rd4" | |||
=== Install Packages === | |||
Install necessary dependencies (util-linux-login is required to get pam.d working): | |||
apk add valkey valkey-cli cyrus-sasl cyrus-sasl-login util-linux-login | |||
Install grommunio packages: | |||
apk add grommunio-gromox grommunio-web grommunio-admin-api grommunio-admin-web grommunio-index grommunio-error-pages | |||
Optionally, install deprecated ActiveSync if needed: | |||
apk add grommunio-dav grommunio-sync | |||
=== Relocate Mail Storage === | |||
The directory `/var/lib/gromox` will be the largest directory containing all the mails and attachments. We therefore relocate it to another disk and create a symlink: | |||
mv /var/lib/gromox /srv/gromox | |||
ln -s /srv/gromox /var/lib/gromox | |||
=== Enable Required Services === | |||
Enable all required services: | |||
rc-update add grommunio-admin-api | |||
rc-update add gromox-delivery | |||
rc-update add gromox-delivery-queue | |||
rc-update add gromox-event | |||
rc-update add gromox-http | |||
rc-update add gromox-imap | |||
rc-update add gromox-midb | |||
rc-update add gromox-pop3 | |||
rc-update add gromox-timer | |||
rc-update add gromox-zcore | |||
rc-update add valkey@grommunio | |||
rc-update add php-fpm83 | |||
rc-update add saslauthd | |||
=== Configure Grommunio === | |||
Modify the configuration files with the custom parameters specified above. | |||
Configure gromox *.cfg: | |||
sed -i "s/mail.example.local/${FQDN}/g" /etc/gromox/*.cfg | |||
sed -i "s/example.com/${MAILDOMAIN}/g" /etc/gromox/*.cfg | |||
Configure gromox adaptor: | |||
sed -i "s/<password>/${MYSQL_PASS}/g" /etc/gromox/mysql_adaptor.cfg | |||
cat /etc/gromox/mysql_adaptor.cfg | |||
Configure autodiscovery: | |||
sed -i "s/mail.example.local/${FQDN}/g" /etc/gromox/autodiscover.ini | |||
sed -i "s/<password>/${MYSQL_PASS}/g" /etc/gromox/autodiscover.ini | |||
cat /etc/gromox/autodiscover.ini | |||
Prepare additional postfix file for sender maps: | |||
cp -p /etc/postfix/grommunio-virtual-mailbox-maps.cf /etc/postfix/grommunio-virtual-mailbox-sender-maps.cf | |||
sed -i '/^query =/d' /etc/postfix/grommunio-virtual-mailbox-sender-maps.cf | |||
echo "query = SELECT username FROM users WHERE username='%s' UNION SELECT aliasname FROM aliases WHERE mainname='%s'" >> /etc/postfix/grommunio-virtual-mailbox-sender-maps.cf | |||
Configure postfix files: | |||
sed -i "s/<password>/${MYSQL_PASS}/g" /etc/postfix/grommunio*.cf | |||
Configure TLS certificates: | |||
sed -i "s/mail.example.local/${FQDN}/g" /etc/grommunio-common/nginx/ssl_certificate.conf | |||
sed -i "s/mail.example.local/${FQDN}/g" /etc/grommunio-admin-common/nginx-ssl.conf | |||
Link TLS certificate configuration: | |||
Since both ssl files are identical it might make more sense to link them | |||
ln -s /etc/grommunio-common/nginx/ssl_certificate.conf /etc/grommunio-admin-common/nginx-ssl.conf | |||
Configure grommunio admin: | |||
sed -i "s/mail.example.local/${FQDN}/g" /etc/grommunio-admin-common/config.json | |||
sed -i "s/<password>/${MYSQL_PASS}/g" /etc/grommunio-admin-api/conf.d/database.yaml | |||
cat /etc/grommunio-admin-api/conf.d/database.yaml | |||
Prepare external or self-signed certificate and put them to: | |||
/etc/ssl/certs/${FQDN}.cert.pem | |||
/etc/ssl/private/${FQDN}.key.pem | |||
Concatenate key and cert as needed by postfix: | |||
cat /etc/ssl/private/${FQDN}.key.pem \ | |||
/etc/ssl/certs/${FQDN}.cert.pem \ | |||
> /etc/ssl/private/${FQDN}.key_cert.pem | |||
chmod 640 /etc/ssl/private/*.key_cert.pem | |||
chown root:ssl-cert /etc/ssl/private/*.key_cert.pem | |||
ls -al /etc/ssl/private/ | |||
Add gromox to ssl-cert (or another) group allowed to read the private keys: | |||
addgroup gromox ssl-cert | |||
Retrieve the certificate fingerprint: | |||
NOTE: This is only required if you work with a relay server authenticating the client cert. | |||
Add the output to the relay_clientcerts file on the relay server and hash it with the postmap command. | |||
openssl x509 -noout -fingerprint -sha384 -in /etc/ssl/certs/${FQDN}.cert.pem | |||
Configure PAM for SMTP: | |||
mv /etc/pam.d/smtp /etc/pam.d/smtp.orig | |||
cat > /etc/pam.d/smtp <<EOF | |||
#%PAM-1.0 | |||
# config for grommunio auth services | |||
auth required pam_gromox.so service=smtp | |||
account required pam_permit.so | |||
EOF | |||
Configure SASL authentication: | |||
cat > /etc/conf.d/saslauthd <<EOF | |||
# Configuration for /etc/init.d/saslauthd | |||
SASLAUTHD_OPTS="-a pam -r" | |||
EOF | |||
SASL configuration for SMTP service: | |||
mkdir /etc/sasl2 | |||
cat > /etc/sasl2/smtpd.conf <<EOF | |||
pwcheck_method: saslauthd | |||
mech_list: plain login | |||
EOF | |||
chmod 600 /etc/sasl2/smtpd.conf | |||
In case you migrate from another server replace X500-org-name with the original one: | |||
X500_ORG_NAME_OLD=$(grep 'x500_org_name=' /etc/gromox/zcore.cfg | cut -d= -f2) | |||
X500_ORG_NAME_NEW=<original-key> | |||
sed -i "s/$X500_ORG_NAME_OLD/$X500_ORG_NAME_NEW/g" /etc/gromox/* | |||
=== Initialize Database === | |||
Initialize the database: | |||
gromox-dbop -C | |||
Set the Grommunio admin password: | |||
grommunio-admin passwd --password "${ADMIN_PASS}" | |||
Add grommunio to the adm group to enable admin UI to read `/var/log/maillog`. | |||
NOTE: Additional custom patch is needed to make the log monitoring work | |||
addgroup grommunio adm | |||
=== Configure Firewall === | |||
Open the necessary firewall ports: | |||
25/tcp | |||
80/tcp | |||
110/tcp | |||
143/tcp | |||
443/tcp | |||
587/tcp | |||
993/tcp | |||
995/tcp | |||
8080/tcp | |||
8443/tcp | |||
== 7. Configure Valkey == | |||
=== Enable Syslog === | |||
The syslog facility is set to 'local0'. | |||
NOTE: It is recommended to redirect local0 to the maillog in syslog.conf as all other grommunio processes write to maillog | |||
vi /etc/valkey/grommunio.conf | |||
----- | |||
#pidfile /var/run/valkey/default.pid | |||
#logfile /var/log/valkey/default.log | |||
syslog-enabled yes | |||
syslog-ident valkey | |||
syslog-facility local0 | |||
=== Enable Memory Overcommit === | |||
Add the following sysctl property for valkey | |||
vi /etc/sysctl.conf | |||
----- | |||
# Enable memory overcommit for valkey | |||
vm.overcommit_memory = 1 | |||
- | Update the system configuration | ||
sysctl -p | |||
=== Start Valkey and Test === | |||
- | rcctl restart valkey@grommunio | ||
valkey-cli ping # Expected result: 'PONG' | |||
== 8. Install and Configure Rspamd == | == 8. Install and Configure Rspamd == | ||
Rspamd | === Install Rspamd === | ||
apk add rspamd rspamd-client | |||
=== Configure Rspamd === | |||
Set general options: | |||
cat > /etc/rspamd/local.d/options.inc <<EOF | |||
dns { | |||
enable_dnssec = true; | |||
timeout = 4s; | |||
retransmits = 5; | |||
} | |||
EOF | |||
Configure redis/valkey connection: | |||
cat > /etc/rspamd/local.d/redis.conf <<EOF | |||
read_servers = "127.0.0.1"; | |||
write_servers = "127.0.0.1"; | |||
EOF | |||
Configure normal worker: | |||
cat > /etc/rspamd/local.d/worker-normal.inc << EOF | |||
# Normal worker is intended to scan messages for spam | |||
# You might disable normal worker to free up system resources as we use the proxy worker in self-scan mode | |||
enabled = false; | |||
# If the mailer is running on the same host use a unix socket | |||
#bind_socket = "127.0.0.1:11333"; | |||
#bind_socket = "/var/run/rspamd/worker-normal.sock mode=0660 owner=rspamd"; | |||
EOF | |||
Configure proxy worker: | |||
cat > /etc/rspamd/local.d/worker-proxy.inc << EOF | |||
# Proxy worker is used as postfix milter | |||
# If the mailer is running on the same host use a unix socket | |||
milter = yes; | |||
#bind_socket = "127.0.0.1:11332"; | |||
bind_socket = "/var/run/rspamd/worker-proxy.sock mode=0660 owner=rspamd"; | |||
timeout = 120s; | |||
upstream "local" { | |||
default = yes; # Self-scan upstreams are always default | |||
self_scan = yes; # Enable self-scan | |||
} | |||
count = 4; # Spawn more processes in self-scan mode | |||
EOF | |||
Add postfix to rspamd group to access the socket: | |||
addgroup postfix rspamd | |||
Configure controller worker: | |||
cat > /etc/rspamd/local.d/worker-controller.inc << EOF | |||
# Controller worker is used to manage rspamd stats, to learn rspamd and to serve WebUI | |||
# If the mailer is running on the same host use a unix socket | |||
# NOTE: grommunio connects over http | |||
bind_socket = "127.0.0.1:11334"; | |||
#bind_socket = "/var/run/rspamd/worker-controller.sock mode=0660 owner=rspamd"; | |||
# password for read-only commands | |||
password = "<encrypted_password_string>"; | |||
# password for write commands | |||
enable_password = "<encrypted_password_string>" | |||
secure_ip = "127.0.0.1"; | |||
EOF | |||
Create the user password and replace it in worker-controller.inc: | |||
Command: rspamadm pw --encrypt -p P4ssword | |||
sed -i s/\<encrypted_password_string\>/$(rspamadm pw --encrypt -p "${ADMIN_PASS}")/ /etc/rspamd/local.d/worker-controller.inc | |||
cat /etc/rspamd/local.d/worker-controller.inc | |||
Redirect rspamd logs to syslog: | |||
cat > /etc/rspamd/local.d/logging.inc << EOF | |||
# Redirect rspamd logs to the mail log | |||
type = "syslog"; | |||
facility = "mail"; | |||
level = "notice"; | |||
EOF | |||
Configure external relay: | |||
cat > /etc/rspamd/local.d/external_relay.conf << EOF | |||
enabled = true; | |||
rules { | |||
FETCHMAIL { | |||
strategy = "local"; | |||
} | |||
} | |||
EOF | |||
Configure actions: | |||
cat > /etc/rspamd/local.d/actions.conf << EOF | |||
reject = 15; # Reject when reaching this score | |||
add_header = 4; # Add spam header when reaching this score | |||
greylist = 3; # Apply greylisting when reaching this score (will emit 'soft reject action') | |||
#rewrite_subject = 6; # Rewrite subject to indicate spam. Cannot be combined with add_header | |||
#unknown_weight = 1.0; # Enable if need to set score for all symbols implicitly | |||
#subject = "***SPAM*** %s"; # Set rewrite subject to this value (%s is replaced by the original subject) | |||
EOF | |||
Configure milter_headers: | |||
cat > /etc/rspamd/local.d/milter_headers.conf << EOF | |||
# Enables x-spamd-result, x-rspamd-server and x-rspamd-queue-id headers (default false) | |||
extended_spam_headers = true; | |||
# Set false to always add headers for local IPs (default true) | |||
skip_local = false; | |||
# Set false to always add headers for authenticated users (default true) | |||
skip_authenticated = false; | |||
# Set false to keep pre-existing spam flag added by an upstream spam filter (default true) | |||
remove_upstream_spam_flag = true; | |||
EOF | |||
Configure dkim signing: | |||
cat > /etc/rspamd/local.d/dkim_signing.conf << EOF | |||
# Enable DKIM signing | |||
enabled = true; | |||
# If false, messages with empty envelope from are not signed | |||
allow_envfrom_empty = true; | |||
# If true, envelope/header domain mismatch is ignored | |||
allow_hdrfrom_mismatch = true; | |||
# If true, multiple from headers are allowed (but only first is used) | |||
allow_hdrfrom_multiple = false; | |||
# If true, username does not need to contain matching domain | |||
allow_username_mismatch = true; | |||
# Default path to key, can include '$domain' and '$selector' variables | |||
path = "/var/lib/rspamd/dkim/\$domain-\$selector.key"; | |||
# Default selector to use | |||
selector = "dkim"; | |||
# If false, messages from authenticated users are not selected for signing | |||
sign_authenticated = true; | |||
# If false, messages from local networks are not selected for signing | |||
sign_local = false; | |||
# Map file of IP addresses/subnets to consider for signing | |||
#sign_networks = "/some/file"; # or url | |||
# Symbol to add when message is signed | |||
symbol = "DKIM_SIGNED"; | |||
# If false, messages from domains not defined here will not be signed | |||
try_fallback = false; | |||
# Domain to use for DKIM signing: can be "header" (MIME From), "envelope" (SMTP From), | |||
# "recipient" (SMTP To), "auth" (SMTP username) or directly specified domain name | |||
use_domain = "header"; | |||
# Domain to use for DKIM signing when sender is in sign_networks ("header"/"envelope"/"auth") | |||
#use_domain_sign_networks = "header"; | |||
# Domain to use for DKIM signing when sender is a local IP ("header"/"envelope"/"auth") | |||
#use_domain_sign_local = "header"; | |||
# Whether to normalise domains to eSLD | |||
use_esld = true; | |||
# Whether to get keys from Redis | |||
use_redis = false; | |||
# Hash for DKIM keys in Redis | |||
key_prefix = "DKIM_KEYS"; | |||
# map of domains -> names of selectors (since rspamd 1.5.3) | |||
#selector_map = "/etc/rspamd/dkim_selectors.map"; | |||
# map of domains -> paths to keys (since rspamd 1.5.3) | |||
#path_map = "/etc/rspamd/dkim_paths.map"; | |||
# If `true` get pubkey from DNS record and check if it matches private key | |||
check_pubkey = false; | |||
# Set to `false` if you want to skip signing if public and private keys mismatch | |||
allow_pubkey_mismatch = true; | |||
# Domain specific settings | |||
domain { | |||
<maildomain> { selector = "<selector>"; } | |||
} | |||
EOF | |||
Specify parameters (MAILDOMAIN already defined above): | |||
MAILDOMAIN="example.com" | |||
SELECTOR="202406" | |||
Replace maildomain and selector in configuration: | |||
sed -i "s/<maildomain>/${MAILDOMAIN}/g" /etc/rspamd/local.d/dkim_signing.conf | |||
sed -i "s/<selector>/${SELECTOR}/g" /etc/rspamd/local.d/dkim_signing.conf | |||
Create DKIM Key Pair: | |||
NOTE: The 'pub' file contains the public key and the configuration to create the corresponding dns record | |||
mkdir -p /var/lib/rspamd/dkim | |||
rspamadm dkim_keygen -s ${SELECTOR} -t ED25519 -d ${MAILDOMAIN} -k /var/lib/rspamd/dkim/${MAILDOMAIN}-${SELECTOR}.key > /var/lib/rspamd/dkim/${MAILDOMAIN}-${SELECTOR}.pub | |||
chown -R rspamd:grommunio /var/lib/rspamd/dkim | |||
chmod 640 /var/lib/rspamd/dkim/${MAILDOMAIN}-${SELECTOR}.key | |||
cat /var/lib/rspamd/dkim/${MAILDOMAIN}-${SELECTOR}.pub | |||
=== Start Rspamd and Test === | |||
rc-update add rspamd | |||
rcctl start rspamd | |||
Test rspam client connection getting statistics (you need to wait some minutes until stats are available): | |||
#rspamc -h /run/rspamd/worker-controller.sock stat | |||
rspamc stat | |||
== 9. Finalize and Verify == | |||
=== Restart Services === | |||
Restart all services: | |||
rcctl restart postfix saslauthd rspamd valkey@grommunio nginx php-fpm83 gromox-delivery gromox-event \ | |||
gromox-http gromox-imap gromox-midb gromox-pop3 gromox-delivery-queue gromox-timer gromox-zcore \ | |||
grommunio-admin-api | |||
=== Verify Service Status === | |||
Check the status of all services: | |||
rcctl status | |||
=== Check Logs === | |||
Inspect logs for any errors or issues: | |||
find /var/log -type f | xargs tail -n50 | grep -iE '==>|fail|crit|error|alert|corrupt|warning' | |||
=== Web UI Access === | |||
Admin UI: https://mail.example.local:8443 | |||
Web UI: https://mail.example.local | |||
=== Admin UI - First steps === | |||
Log in with user 'admin' and the previously created ADMIN_PASS | |||
Create and change the following entities: | |||
# '''Defaults:''' Set the overall defaults to be applied to all domains and users. If you use PAM, ensure that the flag 'Allow SMTP sending (used by POP3/IMAP clients)' is activated | |||
# Create an '''Organization''' | |||
# Create a '''Domain''' belonging to this organization. Select as well "Create domain admin role" | |||
# Create '''Users''' belonging to the domain | |||
NOTE: For some reason, the first user is not created properly. Permissions are wrong and the sqlite database is not created. It is suggested to create a second user, ensure that the permissions are set correctly and then delete and recreate the first user | |||
Correct user permissions are like follows: | |||
ls -l /var/lib/gromox/user/example.com | |||
drwxrwx--- 8 grommuni gromox 4.0K Jun 21 14:05 user1 | |||
ls -l /var/lib/gromox/user/example.com/user1 | |||
drwxrwx--- 8 grommuni gromox 4.0K Jun 21 14:05 . | |||
drwxr-xr-x 4 grommuni gromox 4.0K Jun 21 14:05 .. | |||
drwxrwx--- 2 grommuni gromox 4.0K Jun 21 14:05 cid | |||
drwxrwx--- 2 grommuni gromox 4.0K Jun 21 14:05 config | |||
drwxrwx--- 2 grommuni gromox 4.0K Jun 21 14:05 eml | |||
drwxrwx--- 2 grommuni gromox 4.0K Jun 21 14:05 exmdb | |||
drwxrwx--- 2 grommuni gromox 4.0K Jun 21 14:05 ext | |||
-rw-r--r-- 1 gromox gromox 0 Jun 21 14:05 tables.sqlite3 | |||
drwxrwx--- 4 grommuni gromox 4.0K Jun 21 14:05 tmp | |||
=== Web UI Testing === | |||
Verify if you can access the web UI with your new user | |||
=== Test saslauthd authentication === | |||
Finally, test saslauthd authentication with PAM | |||
testsaslauthd -u username -r domain -p password -s smtp | |||
testsaslauthd -u username@domain -p password -s smtp | |||
=== Add/verify the license === | |||
In case you have a license you can configure it in the admin UI under grommunio settings: | |||
https://mail.example.local:8443/license | |||
The default (fallback) community license is defined in the following file | |||
vi /usr/share/grommunio-admin-api/tools/license.py | |||
== See Also == | |||
[[Category:Server]] | |||
[[Category:Mail]] |
Latest revision as of 17:05, 3 December 2024
This tutorial covers the steps to set up a mail server on Alpine Linux using grommunio, an open-source groupware solution that supports email, calendar, and task management. Grommunio stands out with its unique MAPI support, enabling seamless integration with Microsoft Outlook and other MAPI clients, making it an ideal choice for open-source alternatives to proprietary systems. The setup includes essential components like MariaDB, Nginx, PHP, and Postfix to create a fully functional mail server. Follow along to build a secure, scalable communication platform using grommunio's powerful features.
Prerequisites
Before proceeding with the installation, ensure you have a fresh Alpine Linux system setup. You'll need root privileges to execute these commands.
Procedure
- Install and configure MariaDB
- MariaDB performance tuning (optional)
- Install and configure Nginx
- Install and configure PHP
- Install and configure Postfix
- Install and configure Grommunio
- Configure Valkey (Redis replacement)
- Install and configure Rspamd
- Finalize and verify installation
1. Install and Configure MariaDB
Install MariaDB
To start, install MariaDB and necessary utilities
apk add mariadb mariadb-client mariadb-server-utils
Define the variables used later in the setup setup and configuration.
The default datapath is `/var/lib/mysql` and contains the entire database. If you prefer another location define it here and create a symlink. In any case, the grommunio database will be rather small as it only contains the configuration.
DB_DATA_PATH="/srv/mysql" DB_ROOT_PASS="Passw0rd1" DB_USER="admin" DB_PASS="Passw0rd2"
Setup the default system tables:
sudo mysql_install_db --user=mysql --datadir=${DB_DATA_PATH}
Set a symlink to the standard mysql data directory for compatibility reason:
ln -s /srv/mysql /var/lib/mysql
Restart service:
rc-service mariadb restart
Run the built-in security script:
Set the new root password to the one defined above and answer everything with 'y'
sudo mysql_secure_installation
Create MariaDB User
Create a new user for Grommunio and assign privileges:
echo "GRANT ALL ON *.* TO ${DB_USER}@'127.0.0.1' IDENTIFIED BY '${DB_PASS}' WITH GRANT OPTION;" > /tmp/sql echo "GRANT ALL ON *.* TO ${DB_USER}@'localhost' IDENTIFIED BY '${DB_PASS}' WITH GRANT OPTION;" >> /tmp/sql echo "GRANT ALL ON *.* TO ${DB_USER}@'::1' IDENTIFIED BY '${DB_PASS}' WITH GRANT OPTION;" >> /tmp/sql echo "DELETE FROM mysql.user WHERE User=;" >> /tmp/sql echo "FLUSH PRIVILEGES;" >> /tmp/sql cat /tmp/sql | mysql -u root --password="${DB_ROOT_PASS}"
Configure MariaDB
Edit the MariaDB configuration for better performance:
vi /etc/my.cnf.d/mariadb-server.cnf
Add the following configuration:
[mysqld] #skip-networking ## Configuration for grommunio (most values are default) innodb_log_buffer_size=16M innodb_log_file_size=32M innodb_read_io_threads=4 innodb_write_io_threads=4 join_buffer_size=512K query_cache_size=0 query_cache_type=0 query_cache_limit=2M # Activate performance schema performance_schema=ON # Bind to localhost only bind-address = 127.0.0.1 # Disable DNS lookups as we only use localhost connections skip-name-resolve=ON
Create a default charset configuration for MariaDB:
cat > /etc/my.cnf.d/mariadb-server-default-charset.cnf << EOF [client] default-character-set = utf8mb4 [mysqld] collation_server = utf8mb4_general_ci character_set_server = utf8mb4 [mysql] default-character-set = utf8mb4 EOF
Restart MariaDB and enable it to start on boot:
rc-update add mariadb default rc-service mariadb restart
Verify MariaDB Setup
Check if the MariaDB listener is running and bound to the correct interface (127.0.0.1):
ss -tulpn
Create Grommunio Database
Define the database parameters and create the Grommunio database:
MYSQL_HOST="localhost" MYSQL_USER="grommunio" MYSQL_PASS="Passw0rd3" MYSQL_DB="grommunio"
echo "create database $MYSQL_DB character set 'utf8mb4';" > /tmp/sql echo "grant select, insert, update, delete, create, drop, index, alter, create temporary tables, lock tables on $MYSQL_DB.* TO $MYSQL_USER@$MYSQL_HOST identified by '$MYSQL_PASS';" >> /tmp/sql echo "flush privileges;" >> /tmp/sql cat /tmp/sql | mysql -u admin --password="${DB_PASS}"
Test the database connection:
mysql -hlocalhost -u grommunio -p${MYSQL_PASS} grommunio
2. MariaDB Performance Tuning (Optional)
To assist us in checking and tuning the configuration as per our specific needs, we can install mysqltuner, a script that will provide suggestions to improve the performance of our database server and increase its stability
Download MySQLTuner
wget -v https://raw.githubusercontent.com/major/MySQLTuner-perl/master/mysqltuner.pl -O /tmp/mysqltuner.pl
Move it to the correct directory and set permissions:
mv /tmp/mysqltuner.pl /usr/local/bin/mysqltuner.pl chmod 755 /usr/local/bin/mysqltuner.pl
Install perl
NOTE: The perl-doc package is installed because it's the only one containing the perldiag.pod required by mysqltuner.pl
apk add perl perl-doc
Execute the script
You will be prompted to enter the credentials of your administrative MariaDB account:
/usr/local/bin/mysqltuner.pl --user admin --pass ${DB_PASS}
3. Install and Configure Nginx
Install Nginx
Install the necessary Nginx modules:
apk add nginx nginx-mod-http-headers-more nginx-mod-http-vts nginx-mod-http-brotli
Configure Nginx
Backup the original Nginx configuration and edit it for security headers and TLS settings:
cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.orig vi /etc/nginx/nginx.conf
Add the following configuration:
#error_log /var/log/nginx/error.log warn; error_log syslog:server=unix:/dev/log,facility=local2,nohostname warn; ### Common headers for security # TODO: Set temporary to a lower value until we are sure it's working #more_set_headers "Strict-Transport-Security : max-age=15768000; includeSubDomains; preload"; more_set_headers "Strict-Transport-Security : max-age=2592000; includeSubDomains;"; more_set_headers "X-Frame-Options : SAMEORIGIN"; more_set_headers "Content-Security-Policy : default-src https: data: 'unsafe-inline' 'unsafe-eval' always"; more_set_headers "X-Xss-Protection : 1; mode=block"; more_set_headers "X-Content-Type-Options : nosniff"; more_set_headers "Referrer-Policy : strict-origin-when-cross-origin"; more_set_headers "Server : Follow the white rabbit."; ### TLS settings ssl_protocols TLSv1.2 TLSv1.3; ssl_session_cache shared:SSL:1m; ssl_session_timeout 5m; log_format main_ssl '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for" ' 'client_ciphers="$ssl_ciphers" client_curves="$ssl_curves"'; #access_log /var/log/nginx/access.log main_ssl; # Disabled for performance #access_log syslog:server=unix:/dev/log,facility=local2,nohostname main; # Disabled for performance access_log off;
Disable the default page by renaming it:
mv /etc/nginx/http.d/default.conf /etc/nginx/http.d/default.orig
Restart Nginx and enable it to start on boot:
rc-update add nginx rc-service nginx restart
4. Install and Configure PHP
Install PHP
Install the basic php packages. The modules are installed as grommunio dependencies:
apk add php83 php83-fpm
Disable default fpm conf:
mv /etc/php83/php-fpm.d/www.conf /etc/php83/php-fpm.d/www.conf.default
Harden PHP Configuration
Backup config file /etc/php83/php.ini
cp /etc/php83/php.ini /etc/php83/php-fpm.d/php.ini.orig
Disable remote PHP code execution:
sed 's/^;\?\(allow_url_fopen\).*/\1 = Off/' -i /etc/php83/php.ini sed 's/^;\?\(allow_url_include\).*/\1 = Off/' -i /etc/php83/php.ini
Disable information leakage:
sed 's/^;\?\(expose_php\).*/\1 = Off/' -i /etc/php83/php.ini
Configure Error handling:
sed 's/^;\?\(display_errors\).*/\1 = Off/' -i /etc/php83/php.ini sed 's/^;\?\(display_startup_errors\).*/\1 = Off/' -i /etc/php83/php.ini sed 's/^;\?\(log_errors\).*/\1 = On/' -i /etc/php83/php.ini sed 's/^;\?\(error_log = syslog\).*/\1/' -i /etc/php83/php.ini
PHP Resource Control (Optional):
#sed 's/^\(max_execution_time\).*/\1 = 25/' -i /etc/php83/php.ini #sed 's/^\(max_input_time\).*/\1 = 25/' -i /etc/php83/php.ini #sed 's/^\(memory_limit\).*/\1 = 30M/' -i /etc/php83/php.ini #sed 's/^\(post_max_size\).*/\1 = 1M/' -i /etc/php83/php.ini #sed 's/^;\?\(max_input_vars\).*/\1 = 1000/' -i /etc/php83/php.ini
Disable vulnerable functions:
sed 's/^\(disable_functions\).*/\1 = apache_note, apache_setenv, chgrp, curl_multi_exec, define_sys, define_syslog_variables, debugger_off, debugger_on, diskfreespace, _getppid, escapeshellarg, escapeshellcmd, exec, getmyuid, ini_restore, leak, listen, parse_ini_file, passthru, pcntl_alarm, pcntl_async_signals, pcntl_exec, pcntl_fork, pcntl_get_last_error, pcntl_getpriority, pcntl_setpriority, pcntl_signal, pcntl_signal_dispatch, pcntl_signal_get_handler, pcntl_sigprocmask, pcntl_sigtimedwait, pcntl_sigwaitinfo, pcntl_strerror, pcntl_unshare, pcntl_wait, pcntl_waitpid, pcntl_wexitstatus, pcntl_wifcontinued, pcntl_wifexited, pcntl_wifsignaled, pcntl_wifstopped, pcntl_wstopsig, pcntl_wtermsig, posix, posix_ctermid, posix_getcwd, posix_getegid, posix_geteuid, posix_getgid, posix_getgrgid, posix_getgrnam, posix_getgroups, posix_getlogin, posix_getpgid, posix_getpgrp, posix_getpid, posix_getpwnam, posix_getpwuid, posix_getrlimit, posix_getsid, posix_isatty, posix_kill, posix_mkfifo, posix_setegid, posix_seteuid, posix_setgid, posix_setpgid, posix_setsid, posix_setuid, posix_times, posix_ttyname, posix_uname, popen, proc_close, proc_get_status, proc_nice, proc_open, proc_terminate, shell_exec, show_source, system, url_exec/' -i /etc/php83/php.ini
Harden Session Security:
sed 's/^;\?\(session.use_strict_mode\).*/\1 = 1/' -i /etc/php83/php.ini sed 's/^;\?\(session.use_cookies\).*/\1 = 1/' -i /etc/php83/php.ini sed 's/^;\?\(session.cookie_secure\).*/\1 = 1/' -i /etc/php83/php.ini sed 's/^;\?\(session.use_only_cookies\).*/\1 = 1/' -i /etc/php83/php.ini sed 's/^;\?\(session.cookie_httponly\).*/\1 = 1/' -i /etc/php83/php.ini
5. Install and Configure Postfix
Install Postfix
Install the required packages:
apk add postfix postfix-mysql postfix-pcre
Configure Postfix
First backup the original files:
mv /etc/postfix/main.cf /etc/postfix/main.cf.orig mv /etc/postfix/master.cf /etc/postfix/master.cf.orig mv /etc/postfix/header_checks /etc/postfix/header_checks.orig
Download the prepared configuration files:
wget https://alpinelinuxsupport.com/downloads/postfix-configuration-files.zip
Copy the prepared configuration files to `/etc/postfix/` and adapt the configuration to your environment. Ensure you change at least the following values:
main.cf: myhostname, mynetworks, smtp_tls_chain_files, smtpd_tls_chain_files, local_recipient_maps, mydestination, virtual_*, relayhost, mua_sender_restrictions, smtpd_recipient_restrictions master.cf: smtpd_tls_chain_files=/etc/ssl/private/internal-server-key-and-cert.pem
Create the required postmap files:
newaliases postmap /etc/postfix/transport
Run Postfix setup:
newaliases postmap /etc/postfix/transport
Enable the postfix service and restart:
rc-update add postfix rc-service postfix restart
Verify Postfix Logs
Check the Postfix logs for any errors:
tail -f /var/log/maillog
6. Install and Configure Grommunio
Enable IPv6
First of all, turn on ipv6 which is mandatory for grommunio daemons
Edit `/etc/hosts` to include IPv6 localhost:
vi /etc/hosts ----- ::1 localhost ipv6-localhost ipv6-loopback
Ensure IPv6 is enabled in `/etc/sysctl.conf`:
sed -i 's/^net\.ipv6\.conf\..*\.disable_ipv6\s=\s1/#&/' /etc/sysctl.conf sysctl -p ping ::1 # Test if IPv6 is working
Specify Database Parameters
Specify DB parameters if necessary (already specified above in step 1)
MYSQL_HOST="localhost" MYSQL_USER="grommunio" MYSQL_PASS="Passw0rd3" MYSQL_DB="grommunio"
Specify Domain Parameters
The FQDN is for example used by Outlook clients to connect. This name will have to be present in the used certificate.
The default Mail Domain is for example used for Non-Delivery Reports and for generation of some simple TLS certificates. Specify ONLY ONE domain here.
Adjust the following for your specific setup:
FQDN="mail.example.local" MAILDOMAIN="example.com" RELAYHOST="123.123.123.1" ADMIN_PASS="Passw0rd4"
Install Packages
Install necessary dependencies (util-linux-login is required to get pam.d working):
apk add valkey valkey-cli cyrus-sasl cyrus-sasl-login util-linux-login
Install grommunio packages:
apk add grommunio-gromox grommunio-web grommunio-admin-api grommunio-admin-web grommunio-index grommunio-error-pages
Optionally, install deprecated ActiveSync if needed:
apk add grommunio-dav grommunio-sync
Relocate Mail Storage
The directory `/var/lib/gromox` will be the largest directory containing all the mails and attachments. We therefore relocate it to another disk and create a symlink:
mv /var/lib/gromox /srv/gromox ln -s /srv/gromox /var/lib/gromox
Enable Required Services
Enable all required services:
rc-update add grommunio-admin-api rc-update add gromox-delivery rc-update add gromox-delivery-queue rc-update add gromox-event rc-update add gromox-http rc-update add gromox-imap rc-update add gromox-midb rc-update add gromox-pop3 rc-update add gromox-timer rc-update add gromox-zcore rc-update add valkey@grommunio rc-update add php-fpm83 rc-update add saslauthd
Configure Grommunio
Modify the configuration files with the custom parameters specified above.
Configure gromox *.cfg:
sed -i "s/mail.example.local/${FQDN}/g" /etc/gromox/*.cfg sed -i "s/example.com/${MAILDOMAIN}/g" /etc/gromox/*.cfg
Configure gromox adaptor:
sed -i "s/<password>/${MYSQL_PASS}/g" /etc/gromox/mysql_adaptor.cfg cat /etc/gromox/mysql_adaptor.cfg
Configure autodiscovery:
sed -i "s/mail.example.local/${FQDN}/g" /etc/gromox/autodiscover.ini sed -i "s/<password>/${MYSQL_PASS}/g" /etc/gromox/autodiscover.ini cat /etc/gromox/autodiscover.ini
Prepare additional postfix file for sender maps:
cp -p /etc/postfix/grommunio-virtual-mailbox-maps.cf /etc/postfix/grommunio-virtual-mailbox-sender-maps.cf sed -i '/^query =/d' /etc/postfix/grommunio-virtual-mailbox-sender-maps.cf echo "query = SELECT username FROM users WHERE username='%s' UNION SELECT aliasname FROM aliases WHERE mainname='%s'" >> /etc/postfix/grommunio-virtual-mailbox-sender-maps.cf
Configure postfix files:
sed -i "s/<password>/${MYSQL_PASS}/g" /etc/postfix/grommunio*.cf
Configure TLS certificates:
sed -i "s/mail.example.local/${FQDN}/g" /etc/grommunio-common/nginx/ssl_certificate.conf sed -i "s/mail.example.local/${FQDN}/g" /etc/grommunio-admin-common/nginx-ssl.conf
Link TLS certificate configuration:
Since both ssl files are identical it might make more sense to link them
ln -s /etc/grommunio-common/nginx/ssl_certificate.conf /etc/grommunio-admin-common/nginx-ssl.conf
Configure grommunio admin:
sed -i "s/mail.example.local/${FQDN}/g" /etc/grommunio-admin-common/config.json sed -i "s/<password>/${MYSQL_PASS}/g" /etc/grommunio-admin-api/conf.d/database.yaml cat /etc/grommunio-admin-api/conf.d/database.yaml
Prepare external or self-signed certificate and put them to:
/etc/ssl/certs/${FQDN}.cert.pem /etc/ssl/private/${FQDN}.key.pem
Concatenate key and cert as needed by postfix:
cat /etc/ssl/private/${FQDN}.key.pem \ /etc/ssl/certs/${FQDN}.cert.pem \ > /etc/ssl/private/${FQDN}.key_cert.pem chmod 640 /etc/ssl/private/*.key_cert.pem chown root:ssl-cert /etc/ssl/private/*.key_cert.pem ls -al /etc/ssl/private/
Add gromox to ssl-cert (or another) group allowed to read the private keys:
addgroup gromox ssl-cert
Retrieve the certificate fingerprint:
NOTE: This is only required if you work with a relay server authenticating the client cert.
Add the output to the relay_clientcerts file on the relay server and hash it with the postmap command.
openssl x509 -noout -fingerprint -sha384 -in /etc/ssl/certs/${FQDN}.cert.pem
Configure PAM for SMTP:
mv /etc/pam.d/smtp /etc/pam.d/smtp.orig cat > /etc/pam.d/smtp <<EOF #%PAM-1.0 # config for grommunio auth services auth required pam_gromox.so service=smtp account required pam_permit.so EOF
Configure SASL authentication:
cat > /etc/conf.d/saslauthd <<EOF # Configuration for /etc/init.d/saslauthd SASLAUTHD_OPTS="-a pam -r" EOF
SASL configuration for SMTP service:
mkdir /etc/sasl2 cat > /etc/sasl2/smtpd.conf <<EOF pwcheck_method: saslauthd mech_list: plain login EOF chmod 600 /etc/sasl2/smtpd.conf
In case you migrate from another server replace X500-org-name with the original one:
X500_ORG_NAME_OLD=$(grep 'x500_org_name=' /etc/gromox/zcore.cfg | cut -d= -f2) X500_ORG_NAME_NEW=<original-key> sed -i "s/$X500_ORG_NAME_OLD/$X500_ORG_NAME_NEW/g" /etc/gromox/*
Initialize Database
Initialize the database:
gromox-dbop -C
Set the Grommunio admin password:
grommunio-admin passwd --password "${ADMIN_PASS}"
Add grommunio to the adm group to enable admin UI to read `/var/log/maillog`.
NOTE: Additional custom patch is needed to make the log monitoring work
addgroup grommunio adm
Configure Firewall
Open the necessary firewall ports:
25/tcp 80/tcp 110/tcp 143/tcp 443/tcp 587/tcp 993/tcp 995/tcp 8080/tcp 8443/tcp
7. Configure Valkey
Enable Syslog
The syslog facility is set to 'local0'.
NOTE: It is recommended to redirect local0 to the maillog in syslog.conf as all other grommunio processes write to maillog
vi /etc/valkey/grommunio.conf ----- #pidfile /var/run/valkey/default.pid #logfile /var/log/valkey/default.log syslog-enabled yes syslog-ident valkey syslog-facility local0
Enable Memory Overcommit
Add the following sysctl property for valkey
vi /etc/sysctl.conf ----- # Enable memory overcommit for valkey vm.overcommit_memory = 1
Update the system configuration
sysctl -p
Start Valkey and Test
rcctl restart valkey@grommunio valkey-cli ping # Expected result: 'PONG'
8. Install and Configure Rspamd
Install Rspamd
apk add rspamd rspamd-client
Configure Rspamd
Set general options:
cat > /etc/rspamd/local.d/options.inc <<EOF dns { enable_dnssec = true; timeout = 4s; retransmits = 5; } EOF
Configure redis/valkey connection:
cat > /etc/rspamd/local.d/redis.conf <<EOF read_servers = "127.0.0.1"; write_servers = "127.0.0.1"; EOF
Configure normal worker:
cat > /etc/rspamd/local.d/worker-normal.inc << EOF # Normal worker is intended to scan messages for spam # You might disable normal worker to free up system resources as we use the proxy worker in self-scan mode enabled = false; # If the mailer is running on the same host use a unix socket #bind_socket = "127.0.0.1:11333"; #bind_socket = "/var/run/rspamd/worker-normal.sock mode=0660 owner=rspamd"; EOF
Configure proxy worker:
cat > /etc/rspamd/local.d/worker-proxy.inc << EOF # Proxy worker is used as postfix milter # If the mailer is running on the same host use a unix socket milter = yes; #bind_socket = "127.0.0.1:11332"; bind_socket = "/var/run/rspamd/worker-proxy.sock mode=0660 owner=rspamd"; timeout = 120s; upstream "local" { default = yes; # Self-scan upstreams are always default self_scan = yes; # Enable self-scan } count = 4; # Spawn more processes in self-scan mode EOF
Add postfix to rspamd group to access the socket:
addgroup postfix rspamd
Configure controller worker:
cat > /etc/rspamd/local.d/worker-controller.inc << EOF # Controller worker is used to manage rspamd stats, to learn rspamd and to serve WebUI # If the mailer is running on the same host use a unix socket # NOTE: grommunio connects over http bind_socket = "127.0.0.1:11334"; #bind_socket = "/var/run/rspamd/worker-controller.sock mode=0660 owner=rspamd"; # password for read-only commands password = "<encrypted_password_string>"; # password for write commands enable_password = "<encrypted_password_string>" secure_ip = "127.0.0.1"; EOF
Create the user password and replace it in worker-controller.inc:
Command: rspamadm pw --encrypt -p P4ssword
sed -i s/\<encrypted_password_string\>/$(rspamadm pw --encrypt -p "${ADMIN_PASS}")/ /etc/rspamd/local.d/worker-controller.inc cat /etc/rspamd/local.d/worker-controller.inc
Redirect rspamd logs to syslog:
cat > /etc/rspamd/local.d/logging.inc << EOF # Redirect rspamd logs to the mail log type = "syslog"; facility = "mail"; level = "notice"; EOF
Configure external relay:
cat > /etc/rspamd/local.d/external_relay.conf << EOF enabled = true; rules { FETCHMAIL { strategy = "local"; } } EOF
Configure actions:
cat > /etc/rspamd/local.d/actions.conf << EOF reject = 15; # Reject when reaching this score add_header = 4; # Add spam header when reaching this score greylist = 3; # Apply greylisting when reaching this score (will emit 'soft reject action') #rewrite_subject = 6; # Rewrite subject to indicate spam. Cannot be combined with add_header #unknown_weight = 1.0; # Enable if need to set score for all symbols implicitly #subject = "***SPAM*** %s"; # Set rewrite subject to this value (%s is replaced by the original subject) EOF
Configure milter_headers:
cat > /etc/rspamd/local.d/milter_headers.conf << EOF # Enables x-spamd-result, x-rspamd-server and x-rspamd-queue-id headers (default false) extended_spam_headers = true; # Set false to always add headers for local IPs (default true) skip_local = false; # Set false to always add headers for authenticated users (default true) skip_authenticated = false; # Set false to keep pre-existing spam flag added by an upstream spam filter (default true) remove_upstream_spam_flag = true; EOF
Configure dkim signing:
cat > /etc/rspamd/local.d/dkim_signing.conf << EOF # Enable DKIM signing enabled = true; # If false, messages with empty envelope from are not signed allow_envfrom_empty = true; # If true, envelope/header domain mismatch is ignored allow_hdrfrom_mismatch = true; # If true, multiple from headers are allowed (but only first is used) allow_hdrfrom_multiple = false; # If true, username does not need to contain matching domain allow_username_mismatch = true; # Default path to key, can include '$domain' and '$selector' variables path = "/var/lib/rspamd/dkim/\$domain-\$selector.key"; # Default selector to use selector = "dkim"; # If false, messages from authenticated users are not selected for signing sign_authenticated = true; # If false, messages from local networks are not selected for signing sign_local = false; # Map file of IP addresses/subnets to consider for signing #sign_networks = "/some/file"; # or url # Symbol to add when message is signed symbol = "DKIM_SIGNED"; # If false, messages from domains not defined here will not be signed try_fallback = false; # Domain to use for DKIM signing: can be "header" (MIME From), "envelope" (SMTP From), # "recipient" (SMTP To), "auth" (SMTP username) or directly specified domain name use_domain = "header"; # Domain to use for DKIM signing when sender is in sign_networks ("header"/"envelope"/"auth") #use_domain_sign_networks = "header"; # Domain to use for DKIM signing when sender is a local IP ("header"/"envelope"/"auth") #use_domain_sign_local = "header"; # Whether to normalise domains to eSLD use_esld = true; # Whether to get keys from Redis use_redis = false; # Hash for DKIM keys in Redis key_prefix = "DKIM_KEYS"; # map of domains -> names of selectors (since rspamd 1.5.3) #selector_map = "/etc/rspamd/dkim_selectors.map"; # map of domains -> paths to keys (since rspamd 1.5.3) #path_map = "/etc/rspamd/dkim_paths.map"; # If `true` get pubkey from DNS record and check if it matches private key check_pubkey = false; # Set to `false` if you want to skip signing if public and private keys mismatch allow_pubkey_mismatch = true; # Domain specific settings domain { <maildomain> { selector = "<selector>"; } } EOF
Specify parameters (MAILDOMAIN already defined above):
MAILDOMAIN="example.com" SELECTOR="202406"
Replace maildomain and selector in configuration:
sed -i "s/<maildomain>/${MAILDOMAIN}/g" /etc/rspamd/local.d/dkim_signing.conf sed -i "s/<selector>/${SELECTOR}/g" /etc/rspamd/local.d/dkim_signing.conf
Create DKIM Key Pair:
NOTE: The 'pub' file contains the public key and the configuration to create the corresponding dns record
mkdir -p /var/lib/rspamd/dkim rspamadm dkim_keygen -s ${SELECTOR} -t ED25519 -d ${MAILDOMAIN} -k /var/lib/rspamd/dkim/${MAILDOMAIN}-${SELECTOR}.key > /var/lib/rspamd/dkim/${MAILDOMAIN}-${SELECTOR}.pub chown -R rspamd:grommunio /var/lib/rspamd/dkim chmod 640 /var/lib/rspamd/dkim/${MAILDOMAIN}-${SELECTOR}.key cat /var/lib/rspamd/dkim/${MAILDOMAIN}-${SELECTOR}.pub
Start Rspamd and Test
rc-update add rspamd rcctl start rspamd
Test rspam client connection getting statistics (you need to wait some minutes until stats are available):
#rspamc -h /run/rspamd/worker-controller.sock stat rspamc stat
9. Finalize and Verify
Restart Services
Restart all services:
rcctl restart postfix saslauthd rspamd valkey@grommunio nginx php-fpm83 gromox-delivery gromox-event \ gromox-http gromox-imap gromox-midb gromox-pop3 gromox-delivery-queue gromox-timer gromox-zcore \ grommunio-admin-api
Verify Service Status
Check the status of all services:
rcctl status
Check Logs
Inspect logs for any errors or issues:
find /var/log -type f | xargs tail -n50 | grep -iE '==>|fail|crit|error|alert|corrupt|warning'
Web UI Access
Admin UI: https://mail.example.local:8443
Web UI: https://mail.example.local
Admin UI - First steps
Log in with user 'admin' and the previously created ADMIN_PASS
Create and change the following entities:
- Defaults: Set the overall defaults to be applied to all domains and users. If you use PAM, ensure that the flag 'Allow SMTP sending (used by POP3/IMAP clients)' is activated
- Create an Organization
- Create a Domain belonging to this organization. Select as well "Create domain admin role"
- Create Users belonging to the domain
NOTE: For some reason, the first user is not created properly. Permissions are wrong and the sqlite database is not created. It is suggested to create a second user, ensure that the permissions are set correctly and then delete and recreate the first user
Correct user permissions are like follows:
ls -l /var/lib/gromox/user/example.com drwxrwx--- 8 grommuni gromox 4.0K Jun 21 14:05 user1 ls -l /var/lib/gromox/user/example.com/user1 drwxrwx--- 8 grommuni gromox 4.0K Jun 21 14:05 . drwxr-xr-x 4 grommuni gromox 4.0K Jun 21 14:05 .. drwxrwx--- 2 grommuni gromox 4.0K Jun 21 14:05 cid drwxrwx--- 2 grommuni gromox 4.0K Jun 21 14:05 config drwxrwx--- 2 grommuni gromox 4.0K Jun 21 14:05 eml drwxrwx--- 2 grommuni gromox 4.0K Jun 21 14:05 exmdb drwxrwx--- 2 grommuni gromox 4.0K Jun 21 14:05 ext -rw-r--r-- 1 gromox gromox 0 Jun 21 14:05 tables.sqlite3 drwxrwx--- 4 grommuni gromox 4.0K Jun 21 14:05 tmp
Web UI Testing
Verify if you can access the web UI with your new user
Test saslauthd authentication
Finally, test saslauthd authentication with PAM
testsaslauthd -u username -r domain -p password -s smtp testsaslauthd -u username@domain -p password -s smtp
Add/verify the license
In case you have a license you can configure it in the admin UI under grommunio settings:
https://mail.example.local:8443/license
The default (fallback) community license is defined in the following file
vi /usr/share/grommunio-admin-api/tools/license.py