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 をセットアップします。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)