Nginxでsmtp proxyをしてはいけない

# 結論を先に
大規模な負荷分散などをする場合以外の一般的なメールサーバにおいては、労多くして益少なし。裏側におきたいのなら、PortNATなどをお勧めします。

### なぜ？
Nginxのsmtp proxyは送信用としては利用可能だが、**メール受信には利用できない**。(というより、利用すべきでない。自ドメイン宛でもダメ！)

え？これで終わっちゃダメ？
では、順を追って説明します。
# 環境

以下のように接続されている環境を想定しています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/192151/369138af-4390-e87c-2395-3f1923f385d0.png)

OSにはFreeBSD10.3を使用しています。
Mysqlのテーブル管理にはPostfixAdminを利用しています。

# Nginxのsmtp auth
## Nginx側の設定

簡単にnginx.confを書いてみます。
シンプルにするためsmtpのみ、sslなしで記述しています。

```nginx:nginx.conf
mail {
    auth_http  authserver/mailauth/auth.php ;
    proxy on; 
    proxy_pass_error_message on;
    smtp_capabilities PIPELINING 8BITMIME "SIZE 20480000" DSN;
    smtp_auth plain login;

  # SMTP
  server {
    listen      587;
    protocol    smtp;
    xclient     off;
    auth_http_header PORT 587;

    error_log       /var/log/nginx/mail-error.log;
  }
}
```

ここできもとなるのは、認証を行うこの部分

```
auth_http  authserver/mailauth/auth.php ;
```

## 認証：auth.php

冗長ですが、利用したい方がいるかもしれないので、必要のない部分も載せてあります。[^1]

```php:auth.php
<?php

// set $env from nginx
$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['method']  = getenv('HTTP_AUTH_METHOD');
$env['attempt'] = getenv('HTTP_AUTH_ATTEMPT');
$env['client']  = getenv('HTTP_CLIENT_IP');
$env['host']    = getenv('HTTP_CLIENT_HOST');
$env['port']    = getenv('HTTP_PORT');
// proxy port map
$portmap = array(
   "smtp" => 587,
   "pop3" => 110,
   "imap" => 143,
);
// protocol map
$protomap = array(
    "995" => "pops",
    "993" => "imaps",
    "110" => "pop",
    "143" => "imap",
    "587" => "smtp",
    "465" => "smtps",
);
// MySQL DB
$dbhost = "DBHOST";
$dbname = "postfix";
$dbuser = "postfix";
$dbpass = "DBPASSW0RD";
$dsn = "mysql:host=$dbhost;dbname=$dbname;charset=utf8";

$user = rtrim($env['user']) ;
$passwd = rtrim($env['passwd']) ;
$proxyhost = getenv('SERVER_ADDR');
$myaddr = getenv('SERVER_ADDR');
$proxyport = $portmap[$env['proto']];

// DB Access
try {
  $pdo = new PDO($DSN, $dbuser, $dbpass,array(PDO::ATTR_EMULATE_PREPARES => false));
} catch (PDOException $e) {
  $log = 'Dadabase connection failure. '.$e->getMessage();
  openlog("nginx-proxy-auth", LOG_PID , LOG_MAIL);
  syslog(LOG_INFO,"$log");
  closelog();
  exit($log);
}

$sql="SELECT password FROM mailbox WHERE username = '$user' AND active=1;";
$stmt = $pdo->query($sql);
$row = $stmt -> fetch();
$hashpass = $row["password"];

$cmd = "/usr/local/bin/doveadm pw -p '$passwd' -t '$hashpass' |  /usr/bin/sed -r 's/.*(verified))$/\\1/'";
$result = rtrim(shell_exec($cmd));

$log = sprintf('user=%s, client=%s, proto=%s', $user, $env['client'], $env['proto']);

if ( $result === 'verified' ) {
   $log = sprintf('proxy=successful, %s, connect=%s:%s', $log, $proxyhost, $proxyport);
   header('Content-type: text/html');
   header('Auth-Status: OK') ;
   header("Auth-Server: $myaddr") ;
   header("Auth-Port: $proxyport") ;
} else {
   $log = sprintf('proxy=failure, %s, passwd=%s', $log, $passwd);
   header('Content-type: text/html');
   header('Auth-Status: Invalid login') ;
}

// write syslog
openlog("nginx-proxy-auth", LOG_PID , LOG_MAIL);
syslog(LOG_INFO,"$log");
closelog();

?>
```
やっていることはシンプルで、Nginxから引き渡されたユーザ名やPasswordを使用してDBのPassと照合、マッチしていれば'Auth-Status: OK'とメールサーバをかえします。
ここでは、DBですがLDAPなどでも流れは一緒です。

# メールの送信

Postfixの設置が適正に行われていれば、これでSmtpAuthを利用した送信はできるはずです。

# Proxyの認証とメールサーバ

さて、前段で「**Postfixの設置が適正に**」と書きましたが、認証部分ソースを見ればわかりますが、認証時にNginx伝えられるのは認証の成否とメールサーバのアドレス、ポートのみとなります。
Nginxは伝えられたメールサーバに接続してメールの送受信を行うことになりますが、Postfix側は認証が適切に行われているかどうかを知りませんので、**Nginxからのメールは全て認証を経たものとして無条件に受け入れるよう設定**されます。

# メールの受信
メールの送信はできましたので、ここで受信側を考えます。
当然ほかのメールサーバはログインのための情報を持っていませんので、別ポートで認証なしで受信できるよう設定します。
通常の場合Postfixは認証を経ていなくても、自ドメイン宛のメールについては受け入れるという設定がされています。
しかしながら、今回のケースではNginx経由での接続が**適切な認証を経たものかをPostfix側で知ることはできません**。
よって、前段で書いた通りPostfixはこの別ポートからの接続を**認証を経たものとして無条件に受け入れます**。

# 結果

受信用ポートを通して接続された場合、Postfixは**認証を経たものとして無条件に受け入れる**ため、結果として自ドメイン**以外宛**のメールであっても受け入れてしまいます。

**オープンリレー**の出来上がりです。**orz**

# まとめ

nginxのsmtp proxyを実際に利用することを考えた場合、メール受信については、別途手立てを講じる必要があります。
ISPのように複数のサーバで負荷の分散を図らねばならないような環境でしたら、その労力も報われるでしょうが、一般的なサーバでは利用しない方が楽だと思います。


---
# おまけ
NginxでのUpstreamを使ったproxyをいかに記述してみました。

- [upstreamによるメールproxy](https://wiki.rookie-inc.com/serverapps/nginx/streammailproxy)

[^1]: 手抜きでSQL生成してます。このまま使っちゃダメだよ。