UNIXユーザーネームサービス障害時の宛先存在確認問題と対策 - Postfix Advent Calendar 2014 - ダメ出し Blog

2014-12-19(Fri) [postfix] [更新履歴]

Postfix Advent Calendar 2014 の 19日目の記事です。 今回も安定の 5日遅れです。毎度毎度すみません。

今回は、/etc/passwd に加え、LDAP サーバーやデータベースシステムを UNIX ユーザー情報源として利用している場合に発生する問題と、その回避方法について紹介します。

Postfix ローカルユーザー = UNIX ユーザー

Postfix の標準の設定 (かつデフォルト) では、mydestination パラメーターで示されるドメインのユーザー(ローカルユーザー)は、 Postfix 稼動ホスト OS のユーザーになります。 Postfix は UNIX 系の OS 向けに実装されているため、 Postfix ローカルユーザー = UNIX ユーザーと言えます。

一方、UNIX のユーザーは、様々なサービスで維持・管理することができます。 もっとも代表的なものは /etc/passwd ファイルに保持される、旧来からの passwd(5) 情報です。 これ以外にも、ネームサービススイッチと呼ばれる仕組み (nsswitch.conf(5)) によって、LDAP サーバーやデータベースシステムなどを利用することができます。

ネームサービス障害時の Postfix の挙動

/etc/passwd の障害

/etc/passwd を読み込みを阻害して障害をシミュレートしてみましょう。

# chmod go-r /etc/passwd

適当なクライアントから /etc/passwd に存在するユーザー passwduser 宛にメールを送ってみます。以下の例では telnet コマンドで試行しています。

$ telnet mail.example.jp 25
Trying 10.0.0.1...
Connected to mail.example.jp.
Escape character is '^]'.
220 mail.example.jp ESMTP Postfix
mail from:<>
250 2.1.0 Ok
rcpt to:<passwduser@example.jp>
451 4.3.0 <passwduser@example.jp>: Temporary lookup failure
quit
221 2.0.0 Bye
Connection closed by foreign host.

宛先アドレス passwduser@example.jp が拒否されましたが、 応答コードが 4XX なので一時エラー扱いです。 これなら、障害復旧後に再送してもらえることを期待できます。

このときのログは次のようになります。

Dec 24 00:26:45 mail postfix/proxymap[26447]: warning: cannot access UNIX password database: Connection refused
Dec 24 00:26:45 mail postfix/smtpd[26446]: NOQUEUE: reject: RCPT from mua.example.jp[10.0.0.4]: 451 4.3.0 <passwduser@example.jp>: Temporary lookup failure; from=<> to=<passwduser@example.jp> proto=SMTP

ネームサービスサーバーの障害

ネームサービスに LDAP サーバーを利用している状態であると仮定します。 LDAP サービスを停止した状態で、LDAP サーバー上に存在するユーザー ldapuser 宛にメールを送ってみます。

$ telnet mail.example.jp 25
Trying 10.0.0.1...
Connected to mail.example.jp.
Escape character is '^]'.
220 mail.example.jp ESMTP Postfix
mail from:<>
250 2.1.0 Ok
rcpt to:<ldapuser@example.jp>
550 5.1.1 <ldapuser@example.jp>: Recipient address rejected: User unknown in local recipient table
quit
221 2.0.0 Bye
Connection closed by foreign host.

これも宛先アドレス ldapuser@example.jp が拒否されましたが、 今度の場合は応答コードが 5XX なので恒久エラー扱いです。 送信元が MTA であれば、再試行はせずに、即座にバウンスしてしまいます! このようにネームサービス (LDAP サーバー) の一時的な障害にもかかわらず、 恒久エラーとなってしまう問題が起きてしまいます。

このときのログは次のようになります。

Dec 24 00:48:19 mail postfix/smtpd[27862]: NOQUEUE: reject: RCPT from mua.example.jp[10.0.0.4]: 550 5.1.1 <ldapuser@example.jple.jp>: Recipient address rejected: User unknown in local recipient table; from=<> to=<ldapuser@example.jple.jp> proto=SMTP

ネームサービスに SSS (nss_sss) や PADL nss-pam-ldapd (nss_ldap) を利用している場合は、その直接のバックエンドである sssdnslcd の障害時にも同様の結果になります。

Postfix のコードを読んでみる

Postfix がローカルユーザーの存在を確認する際に参照するテーブルは何でしょうか。 それは local_recipient_maps パラメーターの値が示しています。 デフォルトなら次のような設定になっています。

# postconf local_recipient_maps
local_recipient_maps = proxy:unix:passwd.byname $alias_maps

proxy (proxymap(8)) を介して unix テーブルの passwd.byname でローカルユーザーの確認をしていることがわかります。 そこで Postfix のソースコードから src/util/dict_unix.c を参照すると、 以下の部分が該当することがわかります。

static const char *dict_unix_getpwnam(DICT *dict, const char *key)
{
    …省略…
    if ((pwd = getpwnam(key)) == 0) {
        if (sanity_checked == 0) {
            sanity_checked = 1;
            errno = 0;
            if (getpwuid(0) == 0) {
                msg_warn("cannot access UNIX password database: %m");
                dict->error = DICT_ERR_RETRY;
            }
        }
        return (0);
    } else {
        …省略…
    }
    …省略…

UNIX ユーザー情報をユーザー名で索く関数 getpwnam(3) が失敗 (0 を返す) したとき、さらに UID 番号 0 で getpwuid(3) 関数も試行し、 それも失敗した場合だけ dict->error = DICT_ERR_RETRY; するようになっています。

これにより、/etc/passwd のアクセス障害時は両方とも失敗して /etc/passwd の障害と認識されるため一時エラーとなり、 そのほかのネームサービスの障害のときは後者は失敗せず、 ユーザーが存在しないと認識されてしまい恒久エラーとなります。

すべてのネームサービス障害を一時エラーにする方法

Postfix にネームサービスサーバーを直接参照させる

今回の例のようにネームサービスに LDAP サーバーを利用しているのであれば、 ldap_table(5) でローカルユーザーの確認をさせればよいです。 ldap_table(5) なら LDAP サーバーが利用できない場合は一時エラーになります。

local_recipient_maps =
  ldap:$config_directory/local_recipient.ldap.cf
  $alias_maps

参考までに local_recipient.ldap.cf の例も載せておきましょう。

server_host = ldaps://ldap.example.jp/
version = 3
search_base = ou=users,dc=example,dc=jp
scope = sub
query_filter = (&(objectClass=posixAccount)(uid=%s))
result_attribute = uid

Postfix を改修する

確認はしていませんが、 Postfix のソースコードの src/util/dict_unix.c を次のように変更することにより、 すべてのネームサービス障害を一時エラーにできるのではないかと思われます。

static const char *dict_unix_getpwnam(DICT *dict, const char *key)
{
    …省略…
    if ((pwd = getpwnam(key)) == 0) {
        if (sanity_checked == 0) {
            sanity_checked = 1;
            errno = 0;
            getpwnam(":");
            if (errno != 0 && errno != ENOENT) {
                msg_warn("cannot access UNIX password database: %m");
                dict->error = DICT_ERR_RETRY;
            }
        }
        return (0);
    } else {
        …省略…
    }
    …省略…

ただし、ネームサービス障害時の getpwnam(3) による errno 値に標準はなく、 C ライブラリーやネームサービスモジュールの実装に依存している模様です。 この変更では対応できないケースがあるかもしれません。


ところで、12月25日はクリスマスな上に、 OSS 界隈で地味に活躍されているふみやすさんの誕生日ですね!
http://www.amazon.co.jp/registry/wishlist/27M7TV8CEEF6G?sort=priority

逆に、あなたの書いた OSS や Blog や Advent Calendar が気に入ったら何か送りたく なってしまうかもしれないので、プロフィールや Web サイトに あなたの Amazon 欲しいものリストの URL を貼っておいてくださいね!


私が勤める OSSTech っていう某弊社で社員募集しているようです。 人材紹介会社を介さなければ、入社後に 20万円のボーナス! 「ふみやすっていう人に紹介された」と言ってもらえると私にもボーナス!! → https://www.osstech.co.jp/recruit/


よろしければ、これまで参加した/参加予定のほかの Advent Calendar もどうぞ。