08.メールサーバ構築 – roundcube編[さくらのクラウド/CentOS8]

15. roundcube のインストール

#-- 必要な情報を変数に設定
# DOMAIN=sacloud.ma3ki.net
# HTTPS_DOCROOT=/var/www/html/https_root
# WORKDIR=/root/mailserver

#-- roundcube 1.4系の最新版を取得
# git clone https://github.com/roundcube/roundcubemail.git ${WORKDIR}/git/roundcubemail
# cd ${WORKDIR}/git/roundcubemail
# base_version=1.4
# version=$(git tag | grep "^${base_version}" | sort --version-sort | tail -1)

# git checkout ${version}
# cp -pr ../roundcubemail ${HTTPS_DOCROOT}/roundcubemail-${version}
# ln -s ${HTTPS_DOCROOT}/roundcubemail-${version} ${HTTPS_DOCROOT}/roundcube

#-- roundcube の DB を作成
# export HOME=/root
# mysql -e "create database roundcubemail character set utf8 collate utf8_bin;"
# mysql -e "create user roundcube@localhost identified by 'roundcube';"
# mysql -e "grant all privileges ON roundcubemail.* TO roundcube@localhost ;"
# mysql -e "flush privileges;"
# mysql roundcubemail < ${HTTPS_DOCROOT}/roundcube/SQL/mysql.initial.sql

#-- 必要なPHPのライブラリをインストール
# dnf install -y php-{pdo,mbstring,intl,gd,mysqlnd,pear-Auth-SASL,zip} php-pear-Net-SMTP
# pear channel-update pear.php.net
# pear install -a Mail_mime
# pear install Net_LDAP
# pear install Net_Sieve-1.4.4

# dnf install -y ImageMagick ImageMagick-devel
# pecl channel-update pecl.php.net
# yes | pecl install Imagick
# echo extension=imagick.so >> /etc/php.d/99-imagick.ini

#-- php-fpm の再起動
# systemctl restart php-fpm

#-- roundcube の設定
# cat <<'_EOL_'> ${HTTPS_DOCROOT}/roundcube/config/config.inc.php
<?php
$config['db_dsnw'] = 'mysql://roundcube:roundcube@localhost/roundcubemail';
$config['default_host'] = array('_DOMAIN_');
$config['default_port'] = 993;
$config['smtp_server'] = '_DOMAIN_';
$config['smtp_port'] = 465;
$config['smtp_user'] = '%u';
$config['smtp_pass'] = '%p';
$config['support_url'] = '';
$config['product_name'] = 'Roundcube Webmail';
$config['des_key'] = 'rcmail-!24ByteDESkey*Str';
$config['plugins'] = array('managesieve', 'password', 'archive', 'zipdownload');
$config['managesieve_host'] = 'localhost';
$config['spellcheck_engine'] = 'pspell';
$config['skin'] = 'elastic';
_EOL_

# sed -i "s#_DOMAIN_#ssl://${DOMAIN}#" ${HTTPS_DOCROOT}/roundcube/config/config.inc.php

#-- plugin の設定
# cp -p ${HTTPS_DOCROOT}/roundcube/plugins/managesieve/config.inc.php{.dist,}
# sed -i -e "s/managesieve_vacation'] = 0/managesieve_vacation'] = 1/" ${HTTPS_DOCROOT}/roundcube/plugins/managesieve/config.inc.php
# cp -p ${HTTPS_DOCROOT}/roundcube/plugins/password/config.inc.php{.dist,}
# sed -i -e "s/'sql'/'ldap'/" \
  -e "s/'ou=people,dc=example,dc=com'/''/" \
  -e "s/'dc=exemple,dc=com'/''/" \
  -e "s/'uid=%login,ou=people,dc=exemple,dc=com'/'uid=%name,ou=People,%dc'/" \
  -e "s/'(uid=%login)'/'(uid=%name,ou=People,%dc)'/" ${HTTPS_DOCROOT}/roundcube/plugins/password/config.inc.php

# chown -R nginx. ${HTTPS_DOCROOT}/roundcubemail-${version}
# cd ${HTTPS_DOCROOT}/roundcube
# php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
# php composer-setup.php
# php -r "unlink('composer-setup.php');"
# sed -e 's/suggest/require/' -e 's/ required .*/"/' composer.json-dist | perl -ne '{if(/net_ldap3/){chomp; print "$_,\n";}else{print;}}' > composer.json
# yes | php composer.phar install --no-dev
# bin/install-jsdeps.sh
# mv ${HTTPS_DOCROOT}/roundcube/installer ${HTTPS_DOCROOT}/roundcube/_installer

#-- elastic テーマを使用するため、less コマンドをインストール
# dnf install -y npm
#-- less 4.0.0 だと問題が発生する為、3.13.1 を指定してインストール
# npm install -g less@3.13.1

# cd ${HTTPS_DOCROOT}/roundcube/skins/elastic
# lessc -x styles/styles.less > styles/styles.css
# lessc -x styles/print.less > styles/print.css
# lessc -x styles/embed.less > styles/embed.css

#-- nginx 用設定ファイル作成
# cat <<'_EOL_' > /etc/nginx/conf.d/https.d/roundcube.conf
  location ^~ /roundcube {
    location ~ \.php$ {
      try_files $uri =404;
      fastcgi_pass unix:/run/php-fpm/www.sock;
      fastcgi_index index.php;
      fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name;
      include fastcgi_params;
    }
    location ~ ^/roundcube/(README|INSTALL|LICENSE|CHANGELOG|UPGRADING)$ {
      deny all;
    }

    location ~ ^/roundcube/(bin|SQL)/ {
      deny all;
    }

    # A long browser cache lifetime can speed up repeat visits to your page
    location ~* ^/roundcube/.*\.(jpg|jpeg|gif|png|webp|svg|woff|woff2|ttf|css|js|ico|xml)$ {
      access_log        off;
      log_not_found     off;
      expires           360d;
    }
  }
_EOL_

#-- nginx 再起動
# systemctl restart nginx

07.メールサーバ構築 – mysql編[さくらのクラウド/CentOS8]

13. mysql のインストール

#-- 必要な情報を変数に設定
# DOMAIN=sacloud.ma3ki.net
# HTTPS_DOCROOT=/var/www/html/https_root
# ROOT_PASSWORD=HogeHoge
# WORKDIR=/root/mailserver

#-- mysql のインストール
# dnf -y install mysql-{server,devel}

#-- mysqld の起動
# systemctl enable mysqld
# systemctl start mysqld

# (mysqld --initialize-insecure || true)
# mysqladmin -u root password "${ROOT_PASSWORD}"

# cat <<_EOL_> /root/.my.cnf
[client]
host     = localhost
user     = root
password = "${ROOT_PASSWORD}"
socket   = /var/lib/mysql/mysql.sock
_EOL_

#-- validate_passwordのコンポーネントをインストール
# mysql --user=root --password=${ROOT_PASSWORD} -e "INSTALL COMPONENT 'file://component_validate_password';"

# cat <<_EOL_>> /etc/my.cnf.d/mysql-server.cnf

default_authentication_plugin=mysql_native_password
default_password_lifetime=0
validate_password.length=4
validate_password.mixed_case_count=0
validate_password.number_count=0
validate_password.special_char_count=0
validate_password.policy=LOW
_EOL_

#-- mysql 再起動
# systemctl restart mysqld

14. phpldapadmin のインストール

#-- phpldapadmin本家は php7.x に対応していない為、 fork されて対応した下記を clone
# mkdir -p ${WORKDIR}/git
# git clone https://github.com/breisig/phpLDAPadmin.git ${WORKDIR}/git/phpldapadmin

# cp -pr ${WORKDIR}/git/phpldapadmin ${HTTPS_DOCROOT}/phpldapadmin
# cp -p ${HTTPS_DOCROOT}/phpldapadmin/config/config.php{.example,}
# chown -R nginx. ${HTTPS_DOCROOT}/phpldapadmin

#-- basedn を config.php へ設定
# tmpdc=""
# ARRAY_LIST=$(for dc in $(echo "${DOMAIN}" | sed 's/\./ /g')
  do
    tmpdc="${tmpdc}dc=${dc},"
  done
  dc=$(echo ${tmpdc} | sed -e 's/,$//' -e "s/^/'/" -e "s/$/'/")
  printf "${dc}," | sed 's/,$//')

# sed -i -e "301i \$servers->setValue('server','base',array(${ARRAY_LIST}));" ${HTTPS_DOCROOT}/phpldapadmin/config/config.php

#-- 使用しないテンプレートを移動
# mkdir ${HTTPS_DOCROOT}/phpldapadmin/templates/creation_backup
# for x in courierMailAccount.xml courierMailAlias.xml mozillaOrgPerson.xml sambaDomain.xml sambaGroupMapping.xml sambaMachine.xml sambaSamAccount.xml dNSDomain.xml
do
  mv ${HTTPS_DOCROOT}/phpldapadmin/templates/creation/${x} ${HTTPS_DOCROOT}/phpldapadmin/templates/creation_backup
done

次の投稿では roundcube をセットアップします。

06.メールサーバ構築 – nginx編[さくらのクラウド/CentOS8]

10. nginx のインストール

#-- 必要な情報を変数に設定
# DOMAIN=sacloud.ma3ki.net
# HTTP_DOCROOT=/var/www/html/http_root
# HTTPS_DOCROOT=/var/www/html/https_root
# source /etc/sysconfig/network-scripts/ifcfg-eth0

#-- nginx と php-fpm のインストール
# dnf install -y nginx php php-{fpm,ldap,devel,xml,pear,json}

#-- php, php-fpm の設定
# cp -p /etc/php.ini{,.org}
# sed -i -e "s/^max_execution_time = 30/max_execution_time = 300/" \
       -e "s/^max_input_time = 60/max_input_time = 300/" \
       -e "s/^post_max_size = 8M/post_max_size = 20M/" \
       -e "s/^upload_max_filesize = 2M/upload_max_filesize = 20M/" \
       -e "s/^;date.timezone =/date.timezone = 'Asia\/Tokyo'/" /etc/php.ini

# cp -p /etc/php-fpm.d/www.conf{,.org}
# sed -i -e "s/user = apache/user = nginx/" -e "s/group = apache/group = nginx/" /etc/php-fpm.d/www.conf

#-- phpldapadminの10000ユーザ対応
# sed -i -e 's/^;php_admin_value\[memory_limit\] = 128M/php_admin_value\[memory_limit\] = 256M/' /etc/php-fpm.d/www.conf

# chown nginx /var/log/php-fpm
# chgrp nginx /var/lib/php/*

#-- php を update すると、permission が apache に戻るので、その対策
# cat <<_EOL_>> /etc/cron.d/sacloud
* * * * * root chown nginx /var/log/php-fpm >/dev/null 2>&1
* * * * * root chgrp nginx /var/lib/php/{opcache,session,wsdlcache} >/dev/null 2>&1
_EOL_

#-- nginx mail proxy の設定
# cat <<_EOL_> /etc/nginx/mail.conf
mail {
  auth_http  127.0.0.1/nginx_mail_proxy/ldap_authentication.php ;
  proxy on;
  proxy_pass_error_message on;
  include /etc/nginx/default.d/${DOMAIN}_ssl.conf;
  ssl_session_cache shared:MAIL:10m;
  smtp_capabilities PIPELINING 8BITMIME "SIZE 20480000";
  pop3_capabilities TOP USER UIDL;
  imap_capabilities IMAP4rev1;
  smtp_auth login plain;

  server {
    listen     ${IPADDR}:587;
    protocol   smtp;
    starttls   on;
    xclient    on;
    resolver   8.8.8.8 8.8.4.4;
    auth_http_header PORT 587;
  }
  server {
    listen     ${IPADDR}:465;
    protocol   smtp;
    ssl        on;
    xclient    on;
    resolver   8.8.8.8 8.8.4.4;
    auth_http_header PORT 465;
  }
  server {
    listen     ${IPADDR}:995;
    protocol   pop3;
    ssl        on;
    auth_http_header PORT 995;
  }
  server {
    listen     ${IPADDR}:993;
    protocol   imap;
    ssl        on;
    auth_http_header PORT 993;
  }
}
_EOL_

#-- nginx の設定
# cp -p /etc/nginx/nginx.conf{,.org}

# cat <<'_EOL_'> /etc/nginx/nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

# Load dynamic modules. See /usr/share/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;

events {
    worker_connections  1024;
}

http {
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile            on;
    tcp_nopush          on;
    tcp_nodelay         on;
    keepalive_timeout   65;
    types_hash_max_size 2048;

    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    include /etc/nginx/conf.d/*.conf;
}
include /etc/nginx/mail.conf;
_EOL_

# cat <<_EOL_> /etc/nginx/default.d/${DOMAIN}_ssl.conf
ssl_protocols TLSv1.2 TLSv1.3 ;
ssl_ciphers EECDH+AESGCM;
ssl_ecdh_curve prime256v1;
ssl_prefer_server_ciphers on;
ssl_session_timeout  5m;
ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/${DOMAIN}/chain.pem;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 10s;
_EOL_

# cat <<_EOL_> /etc/nginx/conf.d/http.conf
server {
  listen ${IPADDR}:80;
  server_name _;
  return 301 https://$host$request_uri;
}

server {
  listen 127.0.0.1:80;
  server_name _;
  index index.html, index.php;
  access_log /var/log/nginx/access_auth.log main;
  error_log  /var/log/nginx/error_auth.log  error;
  root ${HTTP_DOCROOT};
  server_tokens off;
  charset     utf-8;

  location ~ \.php\$ {
    allow 127.0.0.1;
    fastcgi_pass unix:/run/php-fpm/www.sock;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
    include fastcgi_params;
    deny all;
  }

  error_page 404 /404.html;
    location = /404.html {
  }
  error_page 500 502 503 504 /50x.html;
    location = /50x.html {
  }
}
_EOL_

# mkdir -p ${HTTP_DOCROOT} ${HTTPS_DOCROOT} /var/log/nginx /var/cache/nginx/client_temp /etc/nginx/conf.d/https.d
# cp -p /usr/share/nginx/html/[45]*.html /usr/share/nginx/html/*.png ${HTTP_DOCROOT}
# cp -p /usr/share/nginx/html/[45]*.html /usr/share/nginx/html/*.png ${HTTPS_DOCROOT}
# chown -R nginx. /var/www/html/ /var/log/nginx /var/cache/nginx/client_temp

# cat <<_EOL_> /etc/nginx/conf.d/https.conf
server {
  listen 443 ssl http2 default_server;
  server_name ${DOMAIN};
  include /etc/nginx/default.d/${DOMAIN}_ssl.conf;
  index index.html, index.php;
  access_log /var/log/nginx/access_ssl.log main;
  error_log  /var/log/nginx/error_ssl.log  error;
  root ${HTTPS_DOCROOT};
  server_tokens off;
  charset     utf-8;

  ssl_session_cache shared:WEB:10m;
  add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
  ssl_stapling on ;
  ssl_stapling_verify on ;

  include /etc/nginx/conf.d/https.d/*.conf;

  location ~ \.php\$ {
    fastcgi_pass unix:/run/php-fpm/www.sock;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME \$document_root/\$fastcgi_script_name;
    include fastcgi_params;
  }

  error_page 404 /404.html;
    location = /404.html {
  }
  error_page 500 502 503 504 /50x.html;
    location = /50x.html {
  }

}
_EOL_


#-- mta-sts へ対応
mkdir -p ${HTTPS_DOCROOT}/.well-known
cat <<_EOL_> ${HTTPS_DOCROOT}/.well-known/mta-sts.txt
version: STSv1
mx: ${DOMAIN}
mode: enforce
max_age: 10368000
_EOL_

#-- nginx-mail-proxy用の認証スクリプト設置先の作成と必要なモジュールのインストール
# mkdir -p ${HTTP_DOCROOT}/nginx_mail_proxy
# echo "no" | pecl install redis
# echo extension=redis.so  >> /etc/php.d/99-redis.ini

#-- nginx, php-fpm の起動
# systemctl enable nginx php-fpm
# systemctl start nginx php-fpm

#-- OS再起動時にnginxの起動に失敗することがあるので、その対応
# sed -i -e "s/^\(After=network.target remote-fs.target nss-lookup.target\)/\1 network-online.target\nWants=network-online.target/" /usr/lib/systemd/system/nginx.service
# systemctl daemon-reload

#-- firewall の設定
# firewall-cmd --permanent --add-port={25,587,465,993,995,80,443}/tcp
# firewall-cmd --reload

11. nginx-mail-proxy用の認証スクリプト作成

# cd ${HTTP_DOCROOT}/nginx_mail_proxy
# cat <<'_EOL_'>> ldap_authentication.php
<?php
error_reporting(0);

// write syslog
function _writelog($message) {
  openlog("nginx-mail-proxy", LOG_PID, LOG_MAIL);
  syslog(LOG_INFO, "$message");
  closelog();
}

// ldap authentication
function _ldapauth($server,$port,$dn,$passwd) {
  $conn = ldap_connect($server, $port);
  if ($conn) {
    ldap_set_option($conn, LDAP_OPT_PROTOCOL_VERSION, 3);
    $bind = ldap_bind($conn, $dn, $passwd);
    if ($bind) {
      ldap_close($conn);
      return true;
    } else {
      ldap_close($conn);
      return false;
    }
  } else {
    return false;
  }
}

function _mail_proxy($server,$port,$base,$filter,$attribute,$proxyport) {
  $message = "" ;
  $proxyhost = _ldapsearch($server, $port, $base, $filter, $attribute);

  if ($proxyhost === '' ) {
    // proxyhost is not found
    $message = "proxy=failure" ;
    header('Content-type: text/html');
    header('Auth-Status: Invalid login');
  } else {
    // proxyhost is found
    $proxyip = gethostbyname($proxyhost);

    $message = sprintf('proxy=%s:%s', $proxyhost, $proxyport);
    header('Content-type: text/html');
    header('Auth-Status: OK');
    header("Auth-Server: $proxyip");
    header("Auth-Port: $proxyport");
  }
  return $message ;
}

// ldap search
function _ldapsearch($server,$port,$base,$filter,$attribute) {
  $conn = ldap_connect($server, $port);
  if ($conn) {
    ldap_set_option($conn, LDAP_OPT_PROTOCOL_VERSION, 3);
    $sresult = ldap_search($conn, $base, $filter, array($attribute));
    $info = ldap_get_entries($conn, $sresult);
    if ($info[0][$attribute][0] != "" ) {
      return $info[0][$attribute][0];
    }
  }
  return "" ;
}

// set $env from nginx
$env['meth']    = getenv('HTTP_AUTH_METHOD');
$env['user']    = getenv('HTTP_AUTH_USER');
$env['passwd']  = getenv('HTTP_AUTH_PASS');
$env['salt']    = getenv('HTTP_AUTH_SALT');
$env['proto']   = getenv('HTTP_AUTH_PROTOCOL');
$env['attempt'] = getenv('HTTP_AUTH_LOGIN_ATTEMPT');
$env['client']  = getenv('HTTP_CLIENT_IP');
$env['host']    = getenv('HTTP_CLIENT_HOST');
$env['port']    = getenv('HTTP_PORT');

$log = "" ;

// protocol port map
$portmap = array(
  "smtp" => 25,
  "pop3" => 110,
  "imap" => 143,
);

// port searvice name map
$protomap = array(
  "995" => "pops",
  "993" => "imaps",
  "110" => "pop",
  "143" => "imap",
  "587" => "smtp",
  "465" => "smtps",
);

// ldap setting  // atribuite は全て小文字で記述すること
$ldap = array(
    "host" => "127.0.0.1",
    "port" => 389,
    "basedn" => "",
    "filter" => "(mailRoutingAddress=" . $env['user'] . ")",
    "attribute" => "mailmessagestore",
    "dn" => "",
    "passwd" => "",
);

// split uid and domain
$spmra = preg_split('/\@/', $env['user']);

// make dn
foreach (preg_split("/\./", $spmra[1]) as $value) {
    $ldap['dn'] = $ldap['dn'] . 'dc=' . $value . ',' ;
}
$tmpdn = preg_split('/,$/',$ldap['dn']);
$ldap['basedn'] = $tmpdn[0];
$ldap['dn'] = 'uid=' . $spmra[0] . ',ou=People,' . $tmpdn[0];

// set search attribute
if ($env['proto'] === 'smtp' ) {
  $ldap['attribute'] = 'mailhost' ;
}

// set log
$log = sprintf('meth=%s, user=%s, client=%s, proto=%s', $env['meth'], $env['user'], $env['client'], $protomap[$env['port']]);

// set password
$ldap['passwd'] = urldecode($env['passwd']);

// set ratelimit
$max_failcnt = 3 ;
$max_rejectcnt = 3 ;
$expire_time = 120 ;
$reject_time = 600 ;
$max_reject_time = 86400 ;

$whitelist = [
  "127.0.0.1",
  "#IPV4"
];

// check whitelist
if ( ! preg_grep("/^$clientip$/", $whitelist) ) {
  $redis = new Redis();
  try {
    $redis->connect('127.0.0.1', 6379);

    $key = $protomap[$env['port']] . ":" . $env['client'] ;
    $clientip = $env['client'] ;
    $failcnt = $redis->Get($key);
    $ttl = $redis->ttl($key);
    $rejectcnt = $redis->hGet('blacklist', $key);

    if ($failcnt >= $max_failcnt && $ttl > 0 ) {
      // $log = sprintf('auth=reject, %s, failcnt=%s, rejectcnt=%s, ttl=%s',$log,$failcnt,$rejectcnt,$ttl);
      $log = sprintf('auth=reject, %s, passwd=%s, failcnt=%s, rejectcnt=%s, ttl=%s',$log,$ldap['passwd'],$failcnt,$rejectcnt,$ttl);
      header('Content-type: text/html');
      header('Auth-Status: Invalid login');
      _writelog($log);
      exit;
    }
  }
  catch(Exception $e) {
  }
  $redis->close();
}

// ldap authentication
if (_ldapauth($ldap['host'], $ldap['port'], $ldap['dn'], $ldap['passwd'])) {
  // authentication successful
  $log = sprintf('auth=successful, %s', $log);
  $proxyport = $portmap[$env['proto']];
  $result = _mail_proxy($ldap['host'], $ldap['port'], $ldap['basedn'], $ldap['filter'], $ldap['attribute'], $proxyport);
  $log = sprintf('%s, %s', $log, $result);
} else {
  // authentication failure
  // $log = sprintf('auth=failure, %s', $log);
  $log = sprintf('auth=failure, %s, passwd=%s', $log, $ldap['passwd']);

  // check whitelist
  if ( ! preg_grep("/^$clientip$/", $whitelist) ) {
    // set failcnt to redis
    $redis = new Redis();
    try {
      $redis->connect('127.0.0.1', 6379);

      $key = $protomap[$env['port']] . ":" . $env['client'] ;
      $ttl = $redis->ttl($key);
      $failcnt = $redis->Get($key) + 1;
      $rejectcnt = $redis->hGet('blacklist', $key);

      if ( $failcnt > $max_failcnt ) {
        $failcnt = 1 ;
      }

      if ( $failcnt < $max_failcnt ) {
        $redis->Set($key,$failcnt,$expire_time + $ttl);
        if ( empty($rejectcnt) ) {
          $rejectcnt = 0;
          $redis->hSet('blacklist', $key, $rejectcnt);
        }
      } else {
        $rejectcnt += 1;
        $redis->hSet('blacklist', $key, $rejectcnt);
        if ( $rejectcnt >= $max_rejectcnt ) {
          $reject_time = $max_reject_time ;
          $redis->hSet('blacklist', $key, 0);
        }
        $redis->Set($key, $failcnt, $reject_time);
      }

      $ttl = $redis->ttl($key);
      $log = sprintf('%s, failcnt=%s, rejectcnt=%s, ttl=%s',$log,$failcnt,$rejectcnt,$ttl);

    }
    catch(Exception $e) {
    }
    $redis->close();
  }
  header('Content-type: text/html');
  header('Auth-Status: Invalid login');
}

_writelog($log);
exit;
?>
_EOL_

# sed -i "s/#IPV4/${IPADDR}/" ${HTTP_DOCROOT}/nginx_mail_proxy/ldap_authentication.php

これでメールの送受信ができるようになりました。

12. Thunderbird 用の autoconfig を作成

#-- thunderbird 用 autoconfig を作成
# mkdir -p ${HTTPS_DOCROOT}/.well-known/autoconfig/mail
# chown -R nginx. ${HTTPS_DOCROOT}/.well-known

# cat <<'_EOL_'>${HTTPS_DOCROOT}/.well-known/autoconfig/mail/config-v1.1.xml
<?xml version="1.0"?>
<clientConfig version="1.1">
    <emailProvider id="sakuravps">
      <domain>_DOMAIN_</domain>
      <displayName>_DOMAIN_</displayName>
      <displayShortName>_DOMAIN_</displayShortName>
      <incomingServer type="imap">
         <username>%EMAILADDRESS%</username>
         <hostname>_DOMAIN_</hostname>
         <port>993</port>
         <socketType>SSL</socketType>
         <authentication>password-cleartext</authentication>
      </incomingServer>
      <incomingServer type="pop3">
         <username>%EMAILADDRESS%</username>
         <hostname>_DOMAIN_</hostname>
         <port>995</port>
         <socketType>SSL</socketType>
         <authentication>password-cleartext</authentication>
         <pop3>
            <leaveMessagesOnServer>true</leaveMessagesOnServer>
            <downloadOnBiff>true</downloadOnBiff>
            <daysToLeaveMessagesOnServer>14</daysToLeaveMessagesOnServer>
         </pop3>
      </incomingServer>
      <outgoingServer type="smtp">
         <username>%EMAILADDRESS%</username>
         <hostname>_DOMAIN_</hostname>
         <port>465</port>
         <socketType>SSL</socketType>
         <authentication>password-cleartext</authentication>
      </outgoingServer>
    </emailProvider>
    <clientConfigUpdate url="https://_DOMAIN_/.well-known/autoconfig/mail/config-v1.1.xml" />
</clientConfig>
_EOL_

# sed -i "s/_DOMAIN_/${DOMAIN}/g" ${HTTPS_DOCROOT}/.well-known/autoconfig/mail/config-v1.1.xml

次の投稿では mysql と phpldapadmin をセットアップします。

05.メールサーバ構築 – postfix編[さくらのクラウド/CentOS8]

9. postfix のインストール

#-- 必要な情報を変数に設定
# DOMAIN=sacloud.ma3ki.net
# source /etc/sysconfig/network-scripts/ifcfg-eth0

#-- 必要なパッケージのインストール
# dnf install -y postfix {pcre,libdb,libnsl2,mysql,openssl}-devel

#-- dnfでインストールできる postfix は systemd のユニットファイルが欲しかっただけなのでアンインストール
# grep -v ExecStartPre= /usr/lib/systemd/system/postfix.service > /var/tmp/postfix.service
# dnf remove -y postfix

#-- dnfでインストールした postfix は ldap が使用できないため、最新バージョン(3.5.8)のソースから build する
# mkdir -p /root/mailserver/postfix
# cd /root/mailserver/postfix
# VERSION=3.5.8
# curl -O http://mirror.postfix.jp/postfix-release/official/postfix-${VERSION}.tar.gz
# tar xvzf postfix-${VERSION}.tar.gz && cd postfix-${VERSION}

# CCARGS="-Wmissing-prototypes -Wformat -Wno-comment -fPIC \
-DHAS_LDAP -DLDAP_DEPRECATED=1 -DHAS_PCRE -I/usr/include/pcre \
-DHAS_MYSQL -I/usr/include/mysql -DUSE_SASL_AUTH -DUSE_CYRUS_SASL \
-I/usr/include/sasl -DUSE_TLS -DDEF_CONFIG_DIR=\\\"/etc/postfix\\\""

# AUXLIBS="-lldap -llber -lpcre -ldb -lnsl -lresolv -L/usr/lib64/mysql -lmysqlclient \
-lm -L/usr/lib64/sasl2 -lsasl2 -lssl -lcrypto  -pie -Wl,-z,relro,-z,now"

#-- build
# make -f Makefile.init makefiles CCARGS="${CCARGS}" AUXLIBS="${AUXLIBS}"
# make
# make upgrade

#-- systemdのユニットファイルの設置と修正
# mv /var/tmp/postfix.service /usr/lib/systemd/system/
# sed -i -e "s/^\(After=syslog.target network.target\)/\1 network-online.target\nWants=network-online.target/" /usr/lib/systemd/system/postfix.service
# systemctl daemon-reload

#-- postfix の設定

#-- postmulti の有効化
# postmulti -e init
# postmulti -I postfix-inbound -e create

#-- outbound用のpostfix固有設定
# postconf -c /etc/postfix -e inet_interfaces=127.0.0.1
# postconf -c /etc/postfix -e smtpd_milters=inet:127.0.0.1:11332
# postconf -c /etc/postfix -e non_smtpd_milters=inet:127.0.0.1:11332
# postconf -c /etc/postfix -e smtpd_authorized_xclient_hosts=127.0.0.1
# postconf -c /etc/postfix -e smtpd_sasl_auth_enable=yes
# postconf -c /etc/postfix -e smtpd_sender_restrictions=reject_sender_login_mismatch
# postconf -c /etc/postfix -e smtpd_sender_login_maps="ldap:/etc/postfix/ldapsendercheck.cf"

#-- inbound用のpostfix固有設定
# postconf -c /etc/postfix-inbound -X master_service_disable
# postconf -c /etc/postfix-inbound -e inet_interfaces=${IPADDR}
# postconf -c /etc/postfix-inbound -e myhostname=${DOMAIN}
# postconf -c /etc/postfix-inbound -e recipient_delimiter=+
# postconf -c /etc/postfix-inbound -e smtpd_milters=inet:127.0.0.1:11332
# postconf -c /etc/postfix-inbound -e smtpd_helo_restrictions="reject_invalid_hostname reject_non_fqdn_hostname reject_unknown_hostname"
# postconf -c /etc/postfix-inbound -e smtpd_sender_restrictions="reject_non_fqdn_sender reject_unknown_sender_domain"
# postconf -c /etc/postfix-inbound -e relay_domains=/etc/postfix-inbound/relay_domains
# postconf -c /etc/postfix-inbound -e authorized_submit_users=static:anyone
# postconf -c /etc/postfix-inbound -e smtpd_tls_CAfile=/etc/pki/tls/certs/ca-bundle.crt
# postconf -c /etc/postfix-inbound -e smtpd_tls_ask_ccert=yes
# postconf -c /etc/postfix-inbound -e smtpd_tls_cert_file=/etc/letsencrypt/live/${DOMAIN}/fullchain.pem
# postconf -c /etc/postfix-inbound -e smtpd_tls_key_file=/etc/letsencrypt/live/${DOMAIN}/privkey.pem
# postconf -c /etc/postfix-inbound -e smtpd_tls_ciphers=high
# postconf -c /etc/postfix-inbound -e smtpd_tls_loglevel=1
# postconf -c /etc/postfix-inbound -e smtpd_tls_mandatory_ciphers=high
# postconf -c /etc/postfix-inbound -e 'smtpd_tls_mandatory_protocols=!SSLv2,!SSLv3,!TLSv1,!TLSv1.1'
# postconf -c /etc/postfix-inbound -e 'smtpd_tls_protocols=!SSLv2,!SSLv3,!TLSv1,!TLSv1.1'
# postconf -c /etc/postfix-inbound -e smtpd_tls_received_header=yes
# postconf -c /etc/postfix-inbound -e smtpd_tls_session_cache_database=btree:/var/lib/postfix-inbound/smtpd_tls_session_cache
# postconf -c /etc/postfix-inbound -e smtpd_use_tls=yes
# postconf -c /etc/postfix-inbound -e lmtp_destination_concurrency_limit=40
# postconf -c /etc/postfix-inbound -e transport_maps="ldap:/etc/postfix-inbound/ldaptransport.cf"
# postconf -c /etc/postfix-inbound -e virtual_alias_maps="ldap:/etc/postfix-inbound/ldapvirtualalias.cf"
# postconf -c /etc/postfix-inbound -e smtpd_recipient_restrictions="check_recipient_access ldap:/etc/postfix-inbound/ldaprcptcheck.cf reject"

#-- outbound,inbound共通設定
# for cf in /etc/postfix /etc/postfix-inbound
do
  postconf -c ${cf} -e alias_maps=hash:/etc/aliases
  postconf -c ${cf} -e inet_protocols=ipv4
  postconf -c ${cf} -e milter_default_action=tempfail
  postconf -c ${cf} -e milter_protocol=6
  postconf -c ${cf} -e milter_command_timeout=15s
  postconf -c ${cf} -e milter_connect_timeout=20s
  postconf -c ${cf} -e smtpd_junk_command_limit=20
  postconf -c ${cf} -e smtpd_helo_required=yes
  postconf -c ${cf} -e smtpd_hard_error_limit=5
  postconf -c ${cf} -e message_size_limit=20480000
  postconf -c ${cf} -e disable_vrfy_command=yes
  postconf -c ${cf} -e smtpd_discard_ehlo_keywords=dsn,enhancedstatuscodes,etrn
  postconf -c ${cf} -e lmtp_host_lookup=native
  postconf -c ${cf} -e smtp_host_lookup=native
  postconf -c ${cf} -e smtp_tls_CAfile=/etc/pki/tls/certs/ca-bundle.crt
  postconf -c ${cf} -e smtp_tls_cert_file=/etc/letsencrypt/live/${DOMAIN}/fullchain.pem
  postconf -c ${cf} -e smtp_tls_key_file=/etc/letsencrypt/live/${DOMAIN}/privkey.pem
  postconf -c ${cf} -e smtp_tls_loglevel=1
  postconf -c ${cf} -e smtp_tls_security_level=may
  postconf -c ${cf} -e smtp_use_tls=yes
  postconf -c ${cf} -e tls_high_cipherlist=EECDH+AESGCM
  postconf -c ${cf} -e tls_preempt_cipherlist=yes
  postconf -c ${cf} -e tls_random_source=dev:/dev/urandom
  postconf -c ${cf} -e tls_ssl_options=NO_RENEGOTIATION
done

#-- ldap用の設定を作成
# cat <<-_EOL_>/etc/postfix-inbound/ldaprcptcheck.cf
server_host = 127.0.0.1
bind = no
version = 3
scope = sub
timeout = 15
query_filter = (&(objectClass=mailRecipient)(mailAlternateAddress=%s))
result_attribute = mailRoutingAddress
result_format = OK
search_base = dc=%3,dc=%2,dc=%1
_EOL_

# cat <<-_EOL_>/etc/postfix-inbound/ldaptransport.cf
server_host = 127.0.0.1
bind = no
version = 3
scope = sub
timeout = 15
query_filter = (&(objectClass=mailRecipient)(mailAlternateAddress=%s))
result_attribute = mailMessageStore
result_format = lmtp:[%s]:24
search_base = dc=%3,dc=%2,dc=%1
_EOL_

# cp /etc/postfix-inbound/ldaprcptcheck.cf /etc/postfix-inbound/ldapvirtualalias.cf
sed -i 's/result_format = OK/result_format = %s/' /etc/postfix-inbound/ldapvirtualalias.cf

# cat <<-_EOL_>/etc/postfix/ldapsendercheck.cf
server_host = 127.0.0.1
bind = no
version = 3
scope = sub
timeout = 15
query_filter = (&(objectClass=mailRecipient)(mailRoutingAddress=%s))
result_attribute = mailRoutingAddress
result_format = %s
search_base = dc=%3,dc=%2,dc=%1
_EOL_

#-- ドメインの追加
# cat <<_EOL_>/etc/postfix-inbound/relay_domains
${DOMAIN}
_EOL_

#-- 送信アーカイブ設定
# echo "/^(.*)@${DOMAIN}\$/    archive+\$1-Sent@${DOMAIN}" >> /etc/postfix/sender_bcc_maps
# postconf -c /etc/postfix -e sender_bcc_maps=regexp:/etc/postfix/sender_bcc_maps

#-- 受信アーカイブ設定
# cat <<_EOL_>/etc/postfix-inbound/recipient_bcc_maps
if !/^archive\+/
/^(.*)@${DOMAIN}\$/  archive+\$1-Recv@${DOMAIN}
endif
_EOL_
# postconf -c /etc/postfix-inbound -e recipient_bcc_maps=regexp:/etc/postfix-inbound/recipient_bcc_maps

#-- postfix の再起動と postfix-inbound インスタンスの有効化
# systemctl enable postfix
# systemctl start postfix
# postmulti -i postfix-inbound -e enable
# postmulti -i postfix-inbound -p start

#-- aliases の設定変更
# sed -i "s/^postmaster:.*/postmaster:	root@${DOMAIN}/" /etc/aliases
# newaliases

次の投稿では nginx をセットアップします。

04.メールサーバ構築 – dovecot編[さくらのクラウド/CentOS8]

8. dovecot のインストール

#-- 必要な情報を変数に設定
# DOMAIN=sacloud.ma3ki.net
# BASE="dc=sacloud,dc=ma3ki,dc=net"

#-- dovecot のインストール
# dnf install -y dovecot dovecot-pigeonhole openldap-devel expat-devel bzip2-devel zlib-devel

#-- dovecot の設定
# cat <<_EOL_> /etc/dovecot/local.conf
postmaster_address = postmater@${DOMAIN}
auth_mechanisms = plain login
deliver_log_format = from=%{from_envelope}, to=%{to_envelope}, size=%p, msgid=%m, delivery_time=%{delivery_time}, session_time=%{session_time}, %\$
disable_plaintext_auth = no
first_valid_uid = 97
mail_location = maildir:/var/dovecot/%Ld/%Ln
mail_plugins = \$mail_plugins zlib
plugin {
  sieve = /var/dovecot/%Ld/%Ln/dovecot.sieve
  sieve_extensions = +notify +imapflags +editheader +vacation-seconds
  sieve_max_actions = 32
  sieve_max_redirects = 10
  sieve_redirect_envelope_from = recipient
  sieve_vacation_min_period = 1h
  sieve_vacation_default_period = 7d
  sieve_vacation_max_period = 60d
  zlib_save = bz2
  zlib_save_level = 5
}
protocols = imap pop3 lmtp sieve
service imap-login {
  inet_listener imap {
    address = 127.0.0.1
  }
}
service lmtp {
  inet_listener lmtp {
    address = 127.0.0.1
    port = 24
  }
}
service pop3-login {
  inet_listener pop3 {
    address = 127.0.0.1
  }
}
service managesieve-login {
  inet_listener sieve {
    address = 127.0.0.1
  }
}
protocol lmtp {
  mail_plugins = \$mail_plugins sieve
}
protocol imap {
  mail_max_userip_connections = 20
}
ssl = no
ssl_cert =
ssl_key =
lmtp_save_to_detail_mailbox = yes
lda_mailbox_autocreate = yes
passdb {
  driver = static
  args = nopassword=y
}
userdb {
  args = uid=dovecot gid=dovecot home=/var/dovecot/%Ld/%Ln allow_all_users=yes
  driver = static
}
userdb {
  args = /etc/dovecot/dovecot-ldap.conf.ext
  driver = ldap
}
_EOL_

# cat <<_EOL_>/etc/dovecot/dovecot-ldap.conf.ext
hosts = 127.0.0.1
auth_bind = yes
base = ${BASE}
pass_attrs=mailRoutingAddress=User,userPassword=password
pass_filter = (mailRoutingAddress=%u)
user_attrs = \
  =uid=dovecot, \
  =gid=dovecot, \
  =mail=maildir:/var/dovecot/%Ld/%Ln, \
  =home=/var/dovecot/%Ld/%Ln
user_filter = (mailRoutingAddress=%u)
iterate_attrs = mailRoutingAddress=user
iterate_filter = (mailRoutingAddress=*)
_EOL_

# sed -i 's/^\!include auth-system.conf.ext/#\|include auth-system.conf.ext/' /etc/dovecot/conf.d/10-auth.conf

#-- メール保存先を作成
# mkdir /var/dovecot
# chown dovecot. /var/dovecot

#-- dovecot の起動
# systemctl enable dovecot
# systemctl start dovecot

次の投稿では postfix をセットアップします。

03.メールサーバ構築 – ユーザDB編[さくらのクラウド/CentOS8]

7. 389 Directory Server のインストール

#-- rootdn、パスワード 等、必要な情報を変数に設定
# ROOT_DN="cn=manager"
# ROOT_PASSWORD=HogeHoge

# DOMAIN=sacloud.ma3ki.net
# BASE=$(echo ${DOMAIN} | sed -e 's/\(^\|\.\)/,dc=/g' -e 's/^,//')
# DC=$(echo ${DOMAIN} | awk -F\. '{print $1}')
# PEOPLE="ou=People,${BASE}"
# TERMED=$(echo ${PEOPLE} | sed 's/ou=People/ou=Termed/')

# WORKDIR=/root/mailserver/ldap
# mkdir -p ${WORKDIR}

#-- 389ds のインストール
# dnf -y module enable 389-ds
# dnf -y install 389-ds-base openldap-clients

#-- LDAPサーバの作成
# dscreate create-template ${WORKDIR}/389ds
# sed -ri "s/;(root_dn).*/\1=${ROOT_DN}/;s/;(root_password).*/\1=${ROOT_PASSWORD}/" ${WORKDIR}/389ds
# dscreate from-file ${WORKDIR}/389ds

#-- LDPAサーバの起動
# systemctl enable dirsrv@localhost.service
# systemctl start dirsrv@localhost

#-- LDAPサーバの設定変更(制限緩和)
# cat <<-_EOL_> ${WORKDIR}/config.ldif
dn: cn=config
changetype: modify
replace: nsslapd-allow-hashed-passwords
nsslapd-allow-hashed-passwords: on

changetype: modify
replace: nsslapd-sizelimit
nsslapd-sizelimit: -1
_EOL_

# ldapmodify -D ${ROOT_DN} -w ${ROOT_PASSWORD} -f ${WORKDIR}/config.ldif

# cat <<-_EOL_> ${WORKDIR}/limit.ldif
dn: cn=config,cn=ldbm database,cn=plugins,cn=config
changetype: modify
replace: nsslapd-lookthroughlimit
nsslapd-lookthroughlimit: -1
_EOL_

# ldapmodify -D ${ROOT_DN} -w ${ROOT_PASSWORD} -f ${WORKDIR}/limit.ldif

#-- ルートDNを作成
# dsconf localhost backend create --suffix ${BASE} --be-name userRoot1

#-- ドメインと adminアカウントの登録
# cat <<_EOL_>${WORKDIR}/${DOMAIN}.ldif
dn: ${BASE}
objectClass: dcObject
objectClass: organization
dc: ${DC}
o: ${DOMAIN}

dn: ${PEOPLE}
ou: People
objectclass: organizationalUnit

dn: ${TERMED}
ou: Termed
objectclass: organizationalUnit

dn: uid=admin,${PEOPLE}
objectClass: mailRecipient
objectClass: top
userPassword: ${ROOT_PASSWORD}
mailMessageStore: 127.0.0.1
mailHost: 127.0.0.1
mailAccessDomain: ${DOMAIN}
mailRoutingAddress: admin@${DOMAIN}
mailAlternateAddress: admin@${DOMAIN}
mailAlternateAddress: dmarc-report@${DOMAIN}
mailAlternateAddress: sts-report@${DOMAIN}
mailAlternateAddress: postmaster@${DOMAIN}
mailAlternateAddress: root@${DOMAIN}
mailAlternateAddress: abuse@${DOMAIN}
mailAlternateAddress: nobody@${DOMAIN}
mailAlternateAddress: archive@${DOMAIN}

_EOL_

# ldapadd -x -D "${ROOT_DN}" -w ${ROOT_PASSWORD} -f ${WORKDIR}/${DOMAIN}.ldif

#-- ドメインの acl を登録
# echo "dn: ${BASE}" > ${WORKDIR}/${DOMAIN}_acl.ldif
# cat <<-'_EOL_'>> ${WORKDIR}/${DOMAIN}_acl.ldif
changeType: modify
replace: aci
aci: (targetattr="UserPassword")(target!="ldap:///uid=*,ou=Termed,dc=*")(version 3.0; acl "1"; allow(write) userdn="ldap:///self";)
aci: (targetattr="*")(target!="ldap:///uid=*,ou=Termed,dc=*")(version 3.0; acl "5"; allow(read) userdn="ldap:///self";)
aci: (targetattr="UserPassword")(target!="ldap:///uid=*,ou=Termed,dc=*")(version 3.0; acl "2"; allow(compare) userdn="ldap:///anyone";)
aci: (targetattr!="UserPassword")(target!="ldap:///uid=*,ou=Termed,dc=*")(version 3.0; acl "3"; allow(search,read) userdn="ldap:///anyone";)
_EOL_

# ldapmodify -D ${ROOT_DN} -w ${ROOT_PASSWORD} -f ${WORKDIR}/${DOMAIN}_acl.ldif

#-- ldapsearch を実行して登録内容が表示できることを確認
# ldapsearch -x -LLL -b "${BASE}"
dn: dc=sacloud,dc=ma3ki,dc=net
objectClass: dcObject
objectClass: organization
objectClass: top
dc: sacloud
o: sacloud.ma3ki.net

dn: ou=People,dc=sacloud,dc=ma3ki,dc=net
ou: People
objectClass: organizationalUnit
objectClass: top

dn: ou=Termed,dc=sacloud,dc=ma3ki,dc=net
ou: Termed
objectClass: organizationalUnit
objectClass: top

dn: uid=admin,ou=People,dc=sacloud,dc=ma3ki,dc=net
objectClass: mailRecipient
objectClass: top
mailMessageStore: 127.0.0.1
mailHost: 127.0.0.1
mailAccessDomain: sacloud.ma3ki.net
mailRoutingAddress: admin@sacloud.ma3ki.net
mailAlternateAddress: admin@sacloud.ma3ki.net
mailAlternateAddress: dmarc-report@sacloud.ma3ki.net
mailAlternateAddress: sts-report@sacloud.ma3ki.net
mailAlternateAddress: postmaster@sacloud.ma3ki.net
mailAlternateAddress: root@sacloud.ma3ki.net
mailAlternateAddress: abuse@sacloud.ma3ki.net
mailAlternateAddress: nobody@sacloud.ma3ki.net
mailAlternateAddress: archive@sacloud.ma3ki.net
uid: admin

#-- rootdn で ldapsearchを実行してパスワードが表示できることを確認
# ldapsearch -x -LLL -b "${BASE}" -D "${ROOT_DN}" -w ${ROOT_PASSWORD} mailRoutingAddress=admin@sacloud.ma3ki.net userPassword
dn: uid=admin,ou=People,dc=sacloud,dc=ma3ki,dc=net
userPassword:: e1BCS0RGMl9TSEEyNTZ9QUFBSUFLZHF4NU5VY1kxWGZNc.............

次の投稿では dovecot をセットアップします。

02.メールサーバ構築 – Milter編[さくらのクラウド/CentOS8]

4.clamav のインストール

#-- clamav のインストール
# dnf install -y clamd clamav clamav-update

#-- virus database の更新
# freshclam

#-- ログ保存先の作成
# mkdir /var/log/clamd
# chown clamscan. /var/log/clamd
# chmod 775 /var/log/clamd

#-- clamd の設定ファイルの作成
# cp -p /etc/clamd.d/scan.conf{,.org}
# cat <<_EOL_>/etc/clamd.d/scan.conf
LogFile /var/log/clamd/clamd.log
LogFileMaxSize 0
LogTime yes
LogSyslog yes
PidFile /var/run/clamd.scan/clamd.pid
TemporaryDirectory /var/tmp
DatabaseDirectory /var/lib/clamav
LocalSocket /var/run/clamd.scan/clamd.sock
FixStaleSocket yes
MaxConnectionQueueLength 30
MaxThreads 50
ReadTimeout 300
User clamscan
ScanPE yes
ScanELF yes
ScanOLE2 yes
ScanMail yes
ScanArchive yes
ArchiveBlockEncrypted yes
StreamMaxLength 128M
_EOL_

#-- clamd の起動設定
# systemctl enable clamd@scan clamav-freshclam
# systemctl start clamd@scan

5. rspamd のインストール

# DOMAIN=sacloud.ma3ki.net

#-- リポジトリの設定と rspamd, redis のインストール
# curl -L -o /etc/yum.repos.d/rspamd.repo https://rspamd.com/rpm-stable/centos-8/rspamd.repo
# dnf install -y rspamd redis

#-- rspamd の設定
# mkdir /etc/rspamd/local.d/keys

# cat <<'_EOL_'> /etc/rspamd/local.d/options.inc
filters = "chartable,dkim,spf,surbl,regexp,fuzzy_check";
check_all_filters = true;
max_message = 128Mb
_EOL_

# cat <<'_EOL_'> /etc/rspamd/local.d/milter_headers.conf
#use = ["x-spamd-result","x-rspamd-server","x-rspamd-queue-id","authentication-results","x-spam-level","x-virus"];
use = ["authentication-results"];
authenticated_headers = ["authentication-results"];
_EOL_

# cat <<_EOL_> /etc/rspamd/local.d/redis.conf
servers = "127.0.0.1";
_EOL_

# cat <<'_EOL_'> /etc/rspamd/local.d/actions.conf
reject = null;
add_header = 6 ;
greylist = null;
_EOL_

# cat <<'_EOL_'> /etc/rspamd/local.d/greylist.conf
enabled = false
_EOL_

# cat <<'_EOL_'> /etc/rspamd/local.d/phishing.conf
openphish_enabled = true;
phishtank_enabled = true;
_EOL_

# cat <<_EOL_> /etc/rspamd/local.d/antivirus.conf
clamav {
  action  = "reject";
  type    = "clamav";
  servers = "/var/run/clamd.scan/clamd.sock";
  symbol = "CLAM_VIRUS";
  patterns {
    #symbol_name = "pattern";
    JUST_EICAR = "^Eicar-Test-Signature$";
  }
}
_EOL_

# usermod -aG clamscan _rspamd
# usermod -aG virusgroup _rspamd

# cat <<'_EOL_'> /etc/rspamd/local.d/url_reputation.conf
enabled = true;

# Key prefix for redis - default "Ur."
key_prefix = "Ur.";

# Symbols to insert - defaults as shown
symbols {
  white = "URL_REPUTATION_WHITE";
  black = "URL_REPUTATION_BLACK";
  grey = "URL_REPUTATION_GREY";
  neutral = "URL_REPUTATION_NEUTRAL";
}

# DKIM/DMARC/SPF allow symbols - defaults as shown
foreign_symbols {
  dmarc = "DMARC_POLICY_ALLOW";
  dkim = "R_DKIM_ALLOW";
  spf = "R_SPF_ALLOW";
}

# SURBL metatags to ignore - default as shown
ignore_surbl = ["URIBL_BLOCKED", "DBL_PROHIBIT", "SURBL_BLOCKED"];

# Amount of samples required for scoring - default 5
threshold = 5;

#Maximum number of TLDs to update reputation on (default 1)
update_limit = 1;

# Maximum number of TLDs to query reputation on (default 100)
query_limit = 100;

# If true, try to find most 'relevant' URL (default true)
relevance = true;
_EOL_

#-- DKIMの設定
# rspamadm dkim_keygen -d ${DOMAIN} -s default -b 1024 > /tmp/${DOMAIN}.keys
# head -16 /tmp/${DOMAIN}.keys > /etc/rspamd/local.d/keys/default.${DOMAIN}.key
# chmod 600 /etc/rspamd/local.d/keys/default.${DOMAIN}.key
# chown _rspamd. /etc/rspamd/local.d/keys/default.${DOMAIN}.key

# cat <<_EOL_> /etc/rspamd/local.d/dkim_signing.conf
allow_hdrfrom_mismatch = true;
sign_local = true;

use_esld = false;
try_fallback = true;

domain {
  ${DOMAIN} {
    path = "/etc/rspamd/local.d/keys/\$selector.\$domain.key";
    selector = "default";
  }
}

sign_headers = '(o)from:(o)sender:(o)reply-to:(o)subject:(o)date:(o)message-id:(o)to:(o)cc:(o)mime-version:(o)content-type:(o)content-transfer-encoding:resent-to:resent-cc:resent-from:resent-sender:resent-message-id:(o)in-reply-to:(o)references:list-id:list-owner:list-unsubscribe:list-subscribe:list-post';
_EOL_

#-- ARCの設定
# cat <<_EOL_> /etc/rspamd/local.d/arc.conf
allow_hdrfrom_mismatch = true;
sign_local = true;
use_domain = "envelope";

use_esld = false;
try_fallback = true;

cat <<-_EOL_>> /etc/rspamd/local.d/arc.conf
domain {
  ${DOMAIN} {
    path = "/etc/rspamd/local.d/keys/\$selector.\$domain.key";
    selector = "default";
  }
}

sign_headers = "(o)from:(o)sender:(o)reply-to:(o)subject:(o)date:(o)message-id:(o)to:(o)cc:(o)mime-version:(o)content-type:(o)content-transfer-encoding:resent-to:resent-cc:resent-from:resent-sender:resent-message-id:(o)in-reply-to:(o)references:list-id:list-owner:list-unsubscribe:list-subscribe:list-post:dkim-signature";
_EOL_

# cat <<_EOL_> /etc/rspamd/local.d/history_redis.conf
servers         = 127.0.0.1:6379;
key_prefix      = "rs_history";
nrows           = 10000;
compress        = true;
subject_privacy = false;
_EOL_

# cat <<_EOL_> /etc/rspamd/local.d/mime_types.conf
bad_extensions = {
    ace = 4,
    arj = 4,
    bat = 2,
    cab = 3,
    com = 2,
    exe = 1,
    jar = 2,
    lnk = 4,
    scr = 4,
};
bad_archive_extensions = {
    pptx = 0.1,
    docx = 0.1,
    xlsx = 0.1,
    pdf  = 0.1,
    jar  = 3,
    js   = 0.5,
    vbs  = 4,
};
archive_extensions = {
    zip = 1,
    arj = 1,
    rar = 1,
    ace = 1,
    7z  = 1,
    cab = 1,
};
_EOL_

#-- web interface のログインパスワードを設定
# ROOT_PASSWORD=HogeHoge
# web_passwd=$(rspamadm pw -p ${ROOT_PASSWORD})

# cat <<_EOL_> /etc/rspamd/local.d/worker-controller.inc
password        = "${web_passwd}";
enable_password = "${web_passwd}";
_EOL_

#-- redis, rspamd の起動
# systemctl enable redis rspamd
# systemctl start redis rspamd

#-- nginx に設定を追加
# mkdir -p /etc/nginx/conf.d/https.d
# cat <<'_EOL_' > /etc/nginx/conf.d/https.d/rspamd.conf
  location ^~ /rspamd {
    location /rspamd/ {
      proxy_pass       http://127.0.0.1:11334/;
      proxy_set_header Host      $host;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
  }
_EOL_

6.DNSレコードの設定(2)

#-- DKIM の公開鍵をDNSレコードに登録
# RECODE=$(cat /tmp/${DOMAIN}.keys | tr '\n' ' ' | sed -e 's/.*( "//' -e 's/".*"p=/p=/' -e 's/" ).*//')
# usacloud dns record-add -y --name default._domainkey --type TXT --value "${RECODE}" ${DOMAIN}

次の投稿ではLDAPサーバの 389 Directory Server をセットアップします。

01.メールサーバ構築 – 準備編[さくらのクラウド/CentOS8]

1. ツールのインストールと設定

#-- ホスト名の確認
# hostname
mail.sacloud.ma3ki.net

#-- 必要なツールのインストールとアップデート
# dnf config-manager --set-enabled PowerTools
# dnf install -y telnet jq expect sysstat mailx
# dnf update -y

#-- usacloud のインストール
# curl -fsSL https://releases.usacloud.jp/usacloud/repos/install.sh | bash
# usacloud config --token <APIキー アクセストークン>
# usacloud config --secret <APIキー アクセストークンシークレット>
# usacloud config --zone $(dmidecode -t system | awk '/Family/{print $NF}')

#-- certbot で使用するアクセストークンとシークレットを設定
# cat <<_EOL_> ~/.sakura
dns_sakuracloud_api_token = "<APIキー アクセストークン>"
dns_sakuracloud_api_secret = "<APIキー アクセストークンシークレット>"
_EOL_
# chmod 600 ~/.sakura

#-- rsyslog の ratelimit の制限を解除
# sed -i '/^module(load="imjournal"/a \       ratelimit.interval="0"' /etc/rsyslog.conf

2.DNSレコードの設定(1)

#-- 変数設定
# DOMAIN=sacloud.ma3ki.net
# source /etc/sysconfig/network-scripts/ifcfg-eth0

#-- DNSレコードの登録
# usacloud dns record-add -y --name @ --type A   --value ${IPADDR} ${DOMAIN}
# usacloud dns record-add -y --name @ --type MX  --value ${DOMAIN}. ${DOMAIN}
# usacloud dns record-add -y --name @ --type TXT --value "v=spf1 +ip4:${IPADDR} -all" ${DOMAIN}
# usacloud dns record-add -y --name mail --type A --value ${IPADDR} ${DOMAIN}
# usacloud dns record-add -y --name _adsp._domainkey --type TXT --value "dkim=discardable" ${DOMAIN}
# usacloud dns record-add -y --name _dmarc --type TXT --value "v=DMARC1; p=reject; rua=mailto:dmarc-report@${DOMAIN}" ${DOMAIN}
# usacloud dns record-add -y --name _mta-sts --type TXT --value "v=STSv1; id=$(date +%Y%m%d%H%M%S);" ${DOMAIN}
# usacloud dns record-add -y --name _smtp._tls --type TXT --value "v=TLSRPTv1; rua=mailto:sts-report@${DOMAIN}" ${DOMAIN}
# usacloud dns record-add -y --name mta-sts --type A --value ${IPADDR} ${DOMAIN}
# usacloud dns record-add -y --name autoconfig --type A --value ${IPADDR} ${DOMAIN}
# usacloud ipv4 ptr-add -y --hostname mail.${DOMAIN} ${IPADDR}

DNSレコードの設定(1)後の状態

3.SSL証明書の取得

#-- certbot のインストール
# dnf install -y certbot python3-certbot-dns-sakuracloud

#-- SSL 証明書の取得
# certbot certonly --dns-sakuracloud --dns-sakuracloud-credentials /root/.sakura --dns-sakuracloud-propagation-seconds 90 -d *.${DOMAIN} -d ${DOMAIN} -m admin@${DOMAIN} --manual-public-ip-logging-ok --agree-tos

#-- SSL 証明書の更新と更新時に nginx と postfix を再起動する cron を設定
# echo "$((${RANDOM}%60)) $((${RANDOM}%24)) * * $((${RANDOM}%7)) root certbot renew --post-hook 'systemctl reload nginx postfix'" > /etc/cron.d/sacloud

次の投稿では clamav と rspamd をセットアップします。

さくらのクラウドにCentOS8(CentOS Stream 8)でメールサーバを構築する

サーバスペック

仮想コア: 2core, メモリ 2GB, ディスク SSD 20GB

  • ウィルスチェック用途の clamd が メモリを消費するので 2GB を選択
  • ディスクは足りなくなったら追加すればいいので 20GB を選択
  • バックアップは自動バックアップ

ホスト名とドメイン名

  • ホスト名は mail.sacloud.ma3ki.net とします。
  • ドメイン名は sacloud.ma3ki.net とします。

メールサーバの機能

  • 送信機能 (SMTP-OUTBOUND)
    • SMTP Submission(587/tcp)
    • STARTTLS
    • SMTP over TLS(465/tcp)
    • SMTP-AUTH
    • Virus Check (パスワード付きZIPファイルは送信拒否)
    • 各種メール認証技術
      • SPF, DKIM, DMARC, ARC 署名
  • 受信機能 (SMTP-INBOUND)
    • SMTP(25/tcp)
    • STARTTLS
    • Virus Check (パスワード付きZIPファイルは受信拒否)
    • Spam Check
    • 各種メール認証技術
      • SPF, DKIM, DMARC, ARC 認証
  • 参照機能
    • POP over TLS(995/tcp)
    • IMAP over TLS(993/tcp)
  • Webmail
    • Roundcube
      • 各種 Plugin機能
        • フィルタリング (managesieve)
        • パスワード変更 (password)
  • アカウント管理
    • phpldapadmin
      • メールアドレス追加/削除/停止
      • パスワード変更
    • マルチドメイン対応可能
    • Thunderbird の Autoconfig に対応
    • MTA-STS に対応
    • SMTP/POP/IMAP の辞書アタックへの ratelimit
    • メールアーカイブ機能

構築するメールサーバはシングル構成です。全ての機能を1台で提供します。

インストールアプリケーション

  • certbot
  • clamd
  • rspamd
  • redis
  • 389-ds
  • dovecot
  • postfix
  • nginx
  • php-fpm
  • mysql
  • phpldapadmin
  • roundcube

メールシステム通信図メールシステム通信図

セットアップ手順は次の投稿からです。

CentOS Stream 8 でセットアップする場合

基本的には CentOS8 のセットアップ手順で問題ないが、下記の2点のみ読み替える必要があります。

01.メールサーバ構築 – 準備編[さくらのクラウド/CentOS8] の

# dnf config-manager --set-enabled PowerTool
を
# dnf config-manager --set-enabled powertools
に変更

03.メールサーバ構築 – ユーザDB編[さくらのクラウド/CentOS8] の

# sed -ri "s/;(root_dn).*/\1=${ROOT_DN}/;s/;(root_password).*/\1=${ROOT_PASSWORD}/" ${WORKDIR}/389ds
を
# sed -ri "s/;(root_password).*/\1=${ROOT_PASSWORD}\nroot_dn=${ROOT_DN}/" ${WORKDIR}/389ds
に変更

dovecot で public mailbox を使用する

#-- 変数に必要な値を代入
DOMAIN=masdon.life

#-- /etc/dovecot/local.conf に設定を追加する
vi /etc/dovecot/local.conf

#- mail_plugins に acl を追加
mail_plugins = $mail_plugins zlib acl

#- plugin に acl = vfile を追加
plugin {
  ...
  ...
  acl = vfile
}

#- protocol imap の mail_plugins に imap_acl を追加
protocol imap {
  mail_plugins = $mail_plugins imap_acl
  ...
}

#- namespace の設定を追加
namespace inbox {
  type = private
  separator = /
  prefix =
  inbox = yes
}

#- _Public/Test01 と _Public/Test02 を作成する
namespace {
  type = public
  separator = /
  prefix = _Public/
  location = maildir:/var/dovecot/public/%Ld
  subscriptions = yes
  list = yes
  mailbox Test01 {
    auto = subscribe
  }
  mailbox Test02 {
    auto = subscribe
  }
}

#-- dovecot を reload
systemctl reload dovecot

#-- _Public を該当ドメイン全てのユーザで使用できるよう acl を設定
echo "anyone eilprwts" > /var/dovecot/public/${DOMAIN}/dovecot-acl
chown dovecot. /var/dovecot/public/${DOMAIN}/dovecot-acl
chmod 600 /var/dovecot/public/${DOMAIN}/dovecot-acl

#-- 該当ドメインのメールアカウントを設定したメーラーで _Public/Test01と_Public/Test02 フォルダを購読しアクセスすると MBOXが作成される
#-- 作成されたMBOXは、上位フォルダの ACL が設定される
ls -ld /var/dovecot/public/${DOMAIN}/.Test0{1,2}
drwx------ 5 dovecot dovecot 146  2月 17 21:45 /var/dovecot/public/masdon.life/.Test01
drwx------ 5 dovecot dovecot 146  2月 17 21:09 /var/dovecot/public/masdon.life/.Test02

#-- _Public/Test01 を該当ドメイン全てのユーザで使用できるよう acl を設定
echo "anyone eilprwts" > /var/dovecot/public/${DOMAIN}/.Test01/dovecot-acl
chown dovecot. /var/dovecot/public/${DOMAIN}/.Test01/dovecot-acl
chmod 600 /var/dovecot/public/${DOMAIN}/.Test01/dovecot-acl

#-- _Public/Test02 をadminユーザのみで使用できるよう acl を設定
echo "user=admin@${DOMAIN} eilprwts" > /var/dovecot/public/${DOMAIN}/.Test02/dovecot-acl
chown dovecot. /var/dovecot/public/${DOMAIN}/.Test02/dovecot-acl
chmod 600 /var/dovecot/public/${DOMAIN}/.Test02/dovecot-acl

#-- _Public/Test02 にコマンドで acl に特定のユーザの権限を追加
doveadm acl add -u admin@${DOMAIN} _Public/Test02 user=ma3ki@${DOMAIN} expunge insert lookup post read write write-deleted write-seen

#-- _Public/Test02 にコマンドで acl に gr1 というグループの権限を追加
doveadm acl add -u admin@${DOMAIN} _Public/Test02 group=gr1 expunge insert lookup post read write write-deleted write-seen

#-- 権限の確認
doveadm acl get -u admin@${DOMAIN} _Public/Test01
ID     Global Rights
anyone        expunge insert lookup post read write write-deleted write-seen

doveadm acl get -u admin@${DOMAIN} _Public/Test02
ID                        Global Rights
user=admin@masdon.life        expunge insert lookup post read write write-deleted write-seen
user=ma3ki@masdon.life        expunge insert lookup post read write write-deleted write-seen

#-- ACL のフラグの意味 https://doc.dovecot.org/settings/plugin/acl/