11. メールサーバ構築(5) – nginx/php-fpm [さくらのVPS/CentOS7]

・nginxの用途

  1. メールプロキシーサーバ(SMTP-Submission/SMTPS/POPS/IMAPS)
  2. メール認証サーバ(HTTP)
  3. Webサーバ(HTTPS)

・nginx のインストール

#-- 変数に必要な値を代入
DOMAIN=masdon.life
VERSION=1.17.0
IPV4=$(ip addr show eth0 | awk '/inet /{print $2}' | sed 's#/.*##')
IPV6=$(ip addr show eth0 | awk '/inet6 /&&/global/{print $2}' | sed 's#/.*##')

#-- nginx ユーザ/グループの作成と起動スクリプトを用意する為、OS標準の nginx をインストールしてアンインストール
yum install -y nginx
cp -p /usr/lib/systemd/system/nginx.service /var/tmp
yum remove -y nginx

#-- ldapに登録したメールアカウントで basic 認証ができるように nginx-auth-ldap をダウンロード
mkdir ~/work/git
git clone https://github.com/kvspb/nginx-auth-ldap.git ~/work/git/nginx-auth-ldap

#-- nginx の source のダウンロード (TLS1.3 に対応するため、source からインストールする)
cd ~/work/src
curl -L -O https://nginx.org/download/nginx-${VERSION}.tar.gz
tar xvzf nginx-${VERSION}.tar.gz && cd nginx-${VERSION}

#- build
./configure \
--prefix=/usr/local/nginx-${VERSION} \
--conf-path=/etc/nginx/nginx.conf \
--error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log \
--pid-path=/var/run/nginx.pid \
--lock-path=/var/run/nginx.lock \
--http-client-body-temp-path=/var/cache/nginx/client_temp \
--http-proxy-temp-path=/var/cache/nginx/proxy_temp \
--http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp \
--http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp \
--http-scgi-temp-path=/var/cache/nginx/scgi_temp \
--user=nginx \
--group=nginx \
--with-http_ssl_module \
--with-http_realip_module \
--with-http_addition_module \
--with-http_sub_module \
--with-http_dav_module \
--with-http_flv_module \
--with-http_mp4_module \
--with-http_gunzip_module \
--with-http_gzip_static_module \
--with-http_random_index_module \
--with-http_secure_link_module \
--with-http_stub_status_module \
--with-mail \
--with-mail_ssl_module \
--with-file-aio \
--with-cc-opt=-O2 \
--with-http_v2_module \
--add-module=../../git/nginx-auth-ldap \
--with-openssl=../openssl-1.1.1d \
--with-openssl-opt=enable-tls1_3 \
--with-stream \
--with-stream_ssl_module
make
make install
ln -s nginx-${VERSION} /usr/local/nginx
/usr/bin/cp -pf conf/* /etc/nginx/

mkdir /var/lib/nginx
chown nginx. /var/lib/nginx

#-- nginx の起動設定を修正
mv /var/tmp/nginx.service /usr/lib/systemd/system/nginx.service
sed -i -e 's#/usr/sbin/nginx#/usr/local/nginx/sbin/nginx#' \
       -e '/^PIDFile/a ExecStartPre=/usr/local/bin/ipv6upchk.sh nginx' /usr/lib/systemd/system/nginx.service

・php7.3とphp-fpm のインストール

#-- remi レポジトリの追加
rpm -Uvh https://rpms.remirepo.net/enterprise/remi-release-7.rpm

#-- php-fpm のインストール
yum install -y php73-php php73-php-{fpm,ldap,devel}

#-- php-fpm の設定
cp -p /etc/opt/remi/php73/php-fpm.d/www.conf{,.org}

sed -i "s/apache/nginx/" /etc/opt/remi/php73/php-fpm.d/www.conf
chown nginx /var/opt/remi/php73/log/php-fpm
chgrp nginx /var/opt/remi/php73/lib/php/*

cp -p /etc/opt/remi/php73/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/opt/remi/php73/php.ini

ln -s php73 /usr/bin/php
ln -s /var/opt/remi/php73/log/php-fpm /var/log/php-fpm

#-- php を update した場合、permission が apache になるのでその対策
cat <<_EOL_>> /etc/cron.d/${DOMAIN}-cron
* * * * * root chown nginx /var/opt/remi/php73/log/php-fpm >/dev/null 2>&1
* * * * * root chgrp nginx /var/opt/remi/php73/lib/php/{opcache,session,wsdlcache} >/dev/null 2>&1
_EOL_

・nginxの設定

#-- 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 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE AUTH=LOGIN;
  smtp_auth login plain;

  server {
    listen     ${IPV4}:587;
    listen     [${IPV6}]:587;
    protocol   smtp;
    starttls   on;
    xclient    on;
    resolver   127.0.0.1;
    auth_http_header PORT 587;
  }
  server {
    listen     ${IPV4}:465 ssl;
    listen     [${IPV6}]:465 ssl;
    protocol   smtp;
    xclient    on;
    resolver   127.0.0.1;
    auth_http_header PORT 465;
  }
#  server {
#    listen     ${IPV4}:110;
#    listen     [${IPV6}]:110;
#    protocol   pop3;
#    starttls   on;
#    auth_http_header PORT 110;
#  }
  server {
    listen     ${IPV4}:995 ssl;
    listen     [${IPV6}]:995 ssl;
    protocol   pop3;
    auth_http_header PORT 995;
  }
#  server {
#    listen     ${IPV4}:143;
#    listen     [${IPV6}]:143;
#    protocol   imap;
#    starttls   on;
#    auth_http_header PORT 143;
#  }
  server {
    listen     ${IPV4}:993 ssl;
    listen     [${IPV6}]:993 ssl;
    protocol   imap;
    auth_http_header PORT 993;
  }
}
_EOL_

cp -p /etc/nginx/nginx.conf{,.org}

#-- nginx.conf の作成
cat <<'_EOL_'> /etc/nginx/nginx.conf
user  nginx;
worker_processes  2;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

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

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

    limit_req_zone $binary_remote_addr zone=limit_req_by_ip:10m rate=1r/m;
    limit_req_log_level error;
    limit_req_status 503;

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

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    include /etc/nginx/default.d/ldap.conf;
    include /etc/nginx/conf.d/*.conf;

    client_max_body_size 10485760; # 10MB
}
include /etc/nginx/mail.conf;
_EOL_

#-- basic認証でldapを参照する設定
cat <<_EOL_> /etc/nginx/default.d/ldap.conf
auth_ldap_cache_enabled on;
auth_ldap_cache_expiration_time 600000;
auth_ldap_cache_size 100;

ldap_server ldap1 {
    url ldap://127.0.0.1/?mailroutingaddress?sub?(objectClass=*);
    satisfy any;
}
_EOL_

#-- TLSの設定
cat <<_EOL_> /etc/nginx/default.d/masdon.life_ssl.conf
ssl_protocols TLSv1.2 TLSv1.3 ;
ssl_ciphers EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH;
ssl_ecdh_curve prime256v1;
ssl_prefer_server_ciphers on;
ssl_session_timeout  5m;
ssl_certificate /etc/letsencrypt/live/masdon.life/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/masdon.life/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/masdon.life/chain.pem;
resolver          127.0.0.1 8.8.8.8 valid=300s;
resolver_timeout  10s;
_EOL_

#-- 127.0.0.1:80 は nginx mail proxy の認証で使用
#-- global は TLS証明書の更新時に使用
cat <<_EOL_> /etc/nginx/conf.d/http.conf
server {
  listen 127.0.0.1:80;
  server_name ${DOMAIN};
  index index.html, index.php;
  access_log /var/log/nginx/access_auth.log main;
  error_log  /var/log/nginx/error_auth.log  error;
  root /var/www/html/http_root;
  server_tokens off;
  charset     utf-8;

  location ~ \.php\$ {
    allow 127.0.0.1;
    fastcgi_pass 127.0.0.1:9000;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name;
    include fastcgi_params;
    deny all;
  }

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

server {
  listen     ${IPV4}:80;
  listen     [${IPV6}]:80;
  server_name _;
  index index.html, index.php;
  access_log /var/log/nginx/access.log main;
  error_log  /var/log/nginx/error.log  error;
  root /var/www/html/http_root;
  server_tokens off;
  charset     utf-8;

  location ^~ /.well-known/acme-challenge/ {}
  location ^~ /mail/ {}
  location /nginx_mail_proxy/ { deny all; }

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

mkdir -p /var/www/html/http{,s}_root /var/log/nginx /var/cache/nginx/client_temp /etc/nginx/conf.d/https.d
cp -p /usr/local/nginx-${VERSION}/html/50x.html /var/www/html/http_root/
cp -p /usr/local/nginx-${VERSION}/html/50x.html /var/www/html/https_root/
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;
  listen [::]:443 ssl http2 default_server;
  server_name masdon.life;
  include /etc/nginx/default.d/masdon.life_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 /var/www/html/https_root;
  server_tokens off;
  charset     utf-8;

  #-- for roundcube attache file
  client_max_body_size 20m;

  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 127.0.0.1:9000;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name;
    include fastcgi_params;
  }
}
_EOL_

・メールプロキシー認証スクリプトの作成

mkdir -p /var/www/html/http_root/nginx_mail_proxy

cat <<'_EOL_'> /var/www/html/http_root/nginx_mail_proxy/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');
$env['helo']    = getenv('HTTP_AUTH_SMTP_HELO');
$env['from']    = getenv('HTTP_AUTH_SMTP_FROM');
$env['to']      = getenv('HTTP_AUTH_SMTP_TO');

$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
$ldap = array(
  "host" => "127.0.0.1",
  "port" => 389,
  "basedn" => "",
  "filter" => "(mailRoutingAddress=" . $env['user'] . ")",
  "attribute" => "mailhost",
  "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['dn'] = 'uid=' . $spmra[0] . ',ou=People,' . $tmpdn[0];

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

// 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']) ;

// 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, passwd=%s', $log, $ldap['passwd']);
  // $log = sprintf('auth=failure, %s', $log);
  header('Content-type: text/html');
  header('Auth-Status: Invalid login') ;
}

_writelog($log);
exit;
?>
_EOL_

・nginxとphp-fpmの起動

#-- php-fpm の起動
systemctl daemon-reload
systemctl enable nginx php73-php-fpm
systemctl start nginx php73-php-fpm

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