RHEL 7 系 OS で Postfix の chroot 環境をセットアップ


メール送受信、転送などに使われる (RHEL 7 派生では標準の) ソフトウェア、Postfix は、chroot 環境で動かすことで Postfix が攻撃を受けたときの影響を最小限に抑えることができます。これをセットアップする手順には日本語で書かれたものもあるのですが、やや古い上に RHEL 7 系との相性がいまひとつだったので、RHEL 7 / CentOS 7 完全対応版として改めて手順を書いてみることにしました。

大まかな手順は以下の通りです。

  1. /etc/postfix/master.cf の編集
  2. /etc/postfix/chroot-update の配置とパッチ
  3. Postfix サービスの再起動

/etc/postfix/master.cf の編集

master.cf にはそのプロセスを chroot 環境下で実行するか否かのフラグが記されています (コメント部分のみ抜粋)。

/etc/postfix/master.cf
# ==========================================================================
# service type  private unpriv  chroot  wakeup  maxproc command + args
#               (yes)   (yes)   (yes)   (never) (100)
# ==========================================================================

カラム 5 ($5) が chroot するか否かを示すフラグ (RHEL 7 系のデフォルトは n ― chroot 化しない) で、カラム 8 ($8) 以降が起動する内部コマンド名とその引数です。

原則として、内部コマンド名が次のもの 以外 であれば、chroot 化することが可能です1

  • local
  • pipe
  • proxymap
  • spawn
  • virtual

例えば、qmgr についてはオリジナル版から次のように書き換えます (関連部分のみ抜粋)。

/etc/postfix/master.cf
# ==========================================================================
# service type  private unpriv  chroot  wakeup  maxproc command + args
#               (yes)   (yes)   (yes)   (never) (100)
# ==========================================================================
qmgr      unix  n       -       y       300     1       qmgr

/etc/postfix/chroot-update の配置とパッチ

RHEL 7 派生の環境では、サービス開始時に /etc/postfix/chroot-update というシェルスクリプトを配置しておけば、chroot 環境のセットアップ用コマンドとして Postfix 開始前に実行されます。
このために使えそうなファイルとして /usr/share/doc/postfix-2.10.1/examples/chroot-setup/LINUX2 がプリインストールされていますが、これを直接コピーして使うことには 2 つの大きな罠 (+ 1 つの小さな問題) があります。

まず、SELinux と干渉する可能性があるということ。もうひとつは外部からメールを受け取る Postfix サーバーにおいて名前解決 (IP アドレスからの逆引き) がうまくいかず、次のように送信元ホストがすべて不明な扱いになったり、逆引きできないサーバーを拒否している設定2 では全てのメールを受け取れなかったりするというものです。

どこかのメールファイルの例
Received: from example.com (unknown [1.2.3.4])

これらの原因は下の詳細セクションに譲るとして、問題を解決した chroot-update ファイルは以下の通りです。

/etc/postfix/chroot-update
#! /bin/sh

# LINUX2 - shell script to set up a Postfix chroot jail for Linux
# Tested on SuSE Linux 5.3 (libc5) and 7.0 (glibc2.1)

# Other testers reported as working:
#
# 2001-01-15 Debian sid (unstable)
#            Christian Kurz <[email protected]>

# Copyright (c) 2000 - 2001 by Matthias Andree
# Copyright (c) 2015 by Tsukasa OI
# Redistributable unter the MIT-style license that follows:
# Abstract: "do whatever you want except hold somebody liable or change
# the copyright information".

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.

# 2000-09-29
# v0.1: initial release

# 2000-12-05
# v0.2: copy libdb.* for libnss_db.so
#       remove /etc/localtime in case it's a broken symlink
#       restrict find to maxdepth 1 (faster)

# Revision 1.4  2001/01/15 09:36:35  emma
# add note it was successfully tested on Debian sid
#
# 20060101 /lib64 support by Keith Owens.
#

# 2015-12-18
# modification for RHEL 7

CP="cp -p"

cond_copy() {
  # find files as per pattern in $1
  # if any, copy to directory $2
  dir=`dirname "$1"`
  pat=`basename "$1"`
  lr=`find "$dir/" -maxdepth 1 -name "$pat"`
  if test ! -d "$2" ; then exit 1 ; fi
  if test "x$lr" != "x" ; then $CP $1 "$2" ; fi
} 

set -e
umask 022

POSTFIX_DIR=${POSTFIX_DIR-/var/spool/postfix}
cd ${POSTFIX_DIR}

mkdir -p etc lib usr/lib/zoneinfo
test -d /lib64 && mkdir -p lib64

# find localtime (SuSE 5.3 does not have /etc/localtime)
lt=/etc/localtime
if test ! -f $lt ; then lt=/usr/lib/zoneinfo/localtime ; fi
if test ! -f $lt ; then lt=/usr/share/zoneinfo/localtime ; fi
if test ! -f $lt ; then echo "cannot find localtime" ; exit 1 ; fi
rm -f etc/localtime

# copy localtime and some other system files into the chroot's etc
$CP -f $lt /etc/services /etc/resolv.conf /etc/nsswitch.conf etc
$CP -f /etc/host.conf /etc/hosts /etc/passwd etc
ln -s -f /etc/localtime usr/lib/zoneinfo

# copy required libraries into the chroot
cond_copy '/lib/libnss_*.so*' lib
cond_copy '/lib/libresolv.so*' lib
cond_copy '/lib/libdb.so*' lib
if test -d /lib64; then
  cond_copy '/lib64/libnss_*.so*' lib64
  cond_copy '/lib64/libresolv.so*' lib64
  cond_copy '/lib64/libdb.so*' lib64
fi

# SELinux hack
restorecon -Fr ${POSTFIX_DIR}

# This is not necessary because the service is not running now.
# postfix reload

これを配置して実行可能状態にすれば準備完了です。あとは Postfix サービスを再起動すれば、Postfix が chroot 環境で動くようになるはずです。

オリジナル版に変更を加えたところは以下の通り:

  1. 末尾に restorecon コマンドの呼び出しを追加した
  2. cond_copy シェル関数内で find "$dir" となっている部分を find "$dir/" に置き換えた
  3. このスクリプト開始時点でサービスが開始されていない前提であるため、末尾の postfix reload を削った

変更点 1 : SELinux 問題の解決

SELinux は予期しないファイルへのアクセスを阻止する一方で、ファイルやその他のリソースに対して適切なラベルが設定されていなければありがちな SELinux 問題 (Permission denied もしくはそれに起因する様々なエラー) が発生することになります。restorecon は登録されたルールに基づき SELinux ラベルを付け直すコマンドで、これを -r オプション付きで使う3 ことにより、/var/spool/postfix (chroot ディレクトリ) 以下が適切にラベル付けされた状態になります。

restorecon はあくまで登録されたルールに従って SELinux ラベルを付け直すコマンドなので Postfix + chroot 環境の適切なラベルが登録されていなければ正常に動作しません。ですが幸いなことに、RHEL 7 派生 OS においては Postfix + chroot 環境に対応した適切なラベルが初期登録されています。

変更点 2 : 名前解決問題の修正

設定例の LINUX2 スクリプトをそのまま実行しただけだと、名前解決やデータベース操作のために必要な共有ライブラリが適切にコピーされません。そのため、Postfix は名前解決 (特に逆引き) に支障をきたします。

この問題の最大の原因は、RHEL 7 派生において /lib/lib64 がシンボリックリンク (いずれも /usr 以下を指し示す) であることです。また find コマンドは (末尾がスラッシュでない) 与えられたパスがシンボリックリンクであるとき、(リンクの示す先ではなく) そのリンク自体が与えられたものと解釈するため、検索してほしい /usr/lib/usr/lib64 以下のファイルを検索してくれません。

これを解決するために、find コマンドの引数末尾にスラッシュを追加しました。これにより、シンボリックリンクが示す先のディレクトリを強制的に示すようになり、指定されたファイルが適切に検索、コピーされるようになります。

変更点 3 : postfix reload の削除

設定例の LINUX2 スクリプトは、そもそも chroot-update とは全く異なる状況で動くことを想定しています。つまり、Postfix サービスは動作中で、なおかつセットアップ時に一度だけ実行する、というものです。

適切に設定できているならこのスクリプトを修正して実行、ないし手動で実行していれば問題は無いのですが、せっかくサービス起動時に自動でセットアップする機能があるのですから使ってしまいましょう。このために変更するべきところはオリジナル版末尾にある postfix reload だけです。これを削除もしくはコメント化することにより、systemd との統合はうまくいくはずです。

参考文献

  1. Postfixをchroot環境にする - コロの Linux サーバー構築
  2. Postfix manual - master(5) - Postfix documentation (man page)

  1. このうち local および virtual は非 chroot 環境で動かすことが事実上必須です。pipespawn は一応 chroot 環境で動かすこともできなくはありませんが、内部的に動かすコマンドを chroot 環境下にセットアップする必要があるため、よく分からなければ chroot 化しないのが賢明です。また proxymap は chroot 環境下で動かすことも可能ですが、そもそも chroot 環境の外に置いたデータベース類に対するアクセスを chroot 環境のコマンドに提供するのが proxymap の目的なので、動かす意味がありません (逆に、proxymap を使っていなければ chroot 化した副作用もありません)。 

  2. 個人的にはあまりオススメできない。 

  3. -F オプションは SELinux ラベル (コンテキスト) のうちタイプ部分以外も書き換えることを意味します。余程のことがなければ、このオプションがなくてもスクリプトは動作するはずです。