気付いたらrootメールが溢れてる

そんなことになる前にやれるべきことはやっておこう。


    犯人はだれ

    Linuxサーバをテキトーに作ってテキトーに運用してるとやらかしがちなのがrootメールの始末だろう。ターミナルログインしてrootにsuしたら毎回You have mailと言われるアレである。mailコマンドを打ってもよくわからんメールが大量に溜まっておりいちいち読むのも削除するのも面倒だからと放置しがちだが、気を抜くと何年かして忘れた頃に突如ディスクが溢れて、システム丸ごと制御不能ということも稀にあるからあなどれない。それはまあ自業自得なのだが、ミニマルな普通のRHEL/CentOS(Linux)ならrootメールの生成元は基本的に以下のふたつだけだ。

    • crond
    • logwatch

    たったふたつしかないのだからサーバセットアップ時に忘れずに対処しておくに越したことはない。あとからnagiosやらchkrootkitやらを動かしだすと当然メール生成元が増えてゆくわけだしその前に、である。とりあえず手堅いのは/etc/aliasesを編集してrootメールを外部のまともなメールアドレスに丸ごと転送してしまうことだ。


    aliasesで転送する

    /etc/aliasesの末尾を見ると次のようになっている。

    # trap decode to catch security attacks
    decode:         root
    
    # Person who should get root's mail
    #root:          marc
    

    見れば分かる通り root: USERIDを追記すれば、 root宛のメールは指定のローカルアカウントに転送され、 rootのスプールには残らなくなる。従って普段コンソールログインに使用するアカウントを指定してやれば、 rootにならなければメールが来ていることに気付かない、という事態は避けられる。

    /etc/aliasesを編集したら忘れずに newaliasesを実行してMTAを更新しよう。

    当然、USERIDの代わりに外部メールアドレスを記述すれば、 rootメールはすべてそこに送られる・・・はずなのだが、システムによってはうまく動かない。その秘密が直前にある decode: rootだ。コメントにある通りセキュリティ上の理由で rootメールが外部に送信されないよう制限をかけている。故にこの制限をコメントアウトすれば意図通りあらゆる rootメールが外部メールアドレスへ全転送されるようになる。

    # trap decode to catch security attacks
    #decode:        root
    
    # Person who should get root's mail
    root:           log@example.com  
    

    言うまでもなくこの設定で万一あれやこれやをされるとたいへん面白くない事態になる。セキュリティ設定は無闇に外すべきではないのが筋であるから、いったん普段使いのログインアカウントに転送し、そちらで外部メールアドレスへ再転送するように設定するのが正しい。

    # trap decode to catch security attacks
    decode:         root
    
    # Person who should get root's mail
    root:           charlie
    
    charlie:        log@outer.example.com  
    

    ログイン管理者が複数いるなら、カンマで区切って転送先を列挙すればめいめいに転送される。これは簡易的なメーリングリストの作成方法と同じだ。

    root:           charlie, dennis  
    

    当たり前だが自分自身に転送すればループメールになってしまう。外部転送しつつローカルにもメールを残したい場合は、バックスラッシュをつけたエイリアス名を記述する。

    X charlie:        charlie, log@outer.example.com
    
    O charlie:        \charlie, log@outer.example.com  
    

    aliasesで MailFromを書き換える

    ところで、当然ながら外部メールアドレスへの転送を行う場合は、リジェクトやらドロップやらでエラーメールが戻ってくる1ことになる。これをまた転送してしまえば当然無限ループになるので、回避するために転送メールの Fromをカスタマイズする手がある。各メール生成元で個別に MailFromを設定するのが正道だが、上記のエイリアスの仕組みを使えば該当アカウントを通過するすべてのメールのFromをもれなく上書きしてしまえる。

    charlie:          \charlie, "|/var/local/fw-charlie-to-log.sh"  
    owner-fw-charlie: \charlie  
    
    #!/bin/sh -
    /usr/lib/sendmail -f owner-fw-charlie@mydomain log@outer.example.com
    

    これは、charlieに届いたメールは自分自身のローカルに保存されるとともに、指定スクリプト2の標準入力に送る。スクリプトの方は Fromを charlieのエイリアス owner-fw-charlie3に書き換えつつ外部の log@outer.example.comに送信する。こうしておいていざリジェクトメールが帰ってくると、それは owner-fw-charlie宛に着信することになる。 owner-fw-charlieのエイリアス設定行では charlieへのローカル保存のみを指示しているので、再度の外部転送は発生せず、ループメールもここで止めつつリジェクトを捨てずに保存させることができる。

    この話はクラウドVPS等の運用でグローバルIPを持っておりかつ SMTPポートを開けてメール受信もする正規のサーバでのことだが、送信元サーバ自身でエラーメールを受け取らないような運用でも、同じように MailFromを書き換える価値はある。とは言えその場合は From偽装とやっていることは同じなので正しく着発信できループにもならないよう充分に気を払う必要がある。


    crondのメール送信を止める

    ここまでは各種ステータス&ジョブメールを捨てずに外部転送して受け取り&閲覧する前提の話4だったが、ここからはメール生成そのものを元から止めてしまう方法である。

    cronの出すジョブメールを止めるには、まず第一に cron実行されるジョブがstdoutもstderrも吐かないことが最重要だ。出力が何もなければ cronメールは生成されない。本来cronジョブのバックグラウンド終了結果を実行ユーザに返す仕組みなので、その中身は個々のジョブが責任を持っている・・・が、そうも言ってられない事態も時にはあるもので、そういう時は /etc/sysconfig/crondCRONDARGSを次のように修正し、 service crond restartする。

    # Settings for the CRON daemon.
    # CRONDARGS= :  any extra command-line startup arguments for crond
    CRONDARGS="-m off"  
    

    もう少し一般的な(汎用的な)メールの止め方としては、 /etc/crontabMAILTO=""を書く方法だろう。この方法は RHEL/CentOSに限らず Linux全般や *BSDにも通用する。そして編集後には crondを再起動(あるいはkill -HUP)しなければならないのだが、どういうわけか誰もが頻繁にプロセス再起動を忘れるのは定番のお約束である。5

    SHELL=/bin/bash  
    PATH=/sbin:/bin:/usr/sbin:/usr/bin  
    MAILTO=root  
    MAILTO="log@outer.example.com"  
    MAILTO=""  
    HOME=/
    
    # For details see man 4 crontabs
    
    # Example of job definition:
    # .---------------- minute (0 - 59)
    # |  .------------- hour (0 - 23)
    # |  |  .---------- day of month (1 - 31)
    # |  |  |  .------- month (1 - 12) OR jan,feb,mar,apr ...
    # |  |  |  |  .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
    # |  |  |  |  |
    # *  *  *  *  * user-name command to be executed
    

    ところがこの方法には落とし穴がある。crontabファイルは /etcの下にあるものばかりではない。

    # /usr/bin/crontab -l
    MAILTO="root"  
    #* * * * * /etc/pound/poundchk.sh 1>/dev/null 2>&1
    * * * * * /etc/rc.d/secure_cleaning.pl
    */3 * * * * /etc/rc.d/dns_listen_flush.sh
    

    /var/spool/cron/ 以下には各ユーザ別の crontabファイルを作ることが出来る。 /etc/crontabに比べると6カラム目のユーザ指定がない6ことを除けば実質同じものだ。当然MAILTO環境変数も同様に指定できるので、こちらでもメール送信が設定されていないか確認したほうがよい。

    言うまでもなく先に上げた /etc/sysconfig/crondでメールを止める方法はメール送信が完全に止まるので、MAILTO環境変数を改めて指定してもジョブメールが出たりすることはない。


    logwatchのレポートメールを止める

    logwatchについては、もっと高級なシステム監視(ZABBIXやNagios)を採用しているなら不必要なことがほとんどなので、logwatchパッケージ自体を削除して最初からなかったことにしてしまうのが一番シンプルで確実だ。

    sudo yum remove logwatch  
    

    logwatchはperlやsendmail(postfix)くらいにしか依存していないはずなので、依存性により本パッケージ削除できないという事態は滅多にない。だが事情により既存パッケージを削除したくない・できない7等という運営上の理由があるならば、以下の設定で動作を止めることができる。

    echo "DailyReport=No" >> /usr/share/logwatch/default.conf/logwatch.conf  
    

    これは/etc/cron.daily/0logwatchが次のように書かれているためだ。

    #!/bin/bash
    
    DailyReport=`grep -e "^[[:space:]]*DailyReport[[:space:]]*=[[:space:]]*" /usr/share/logwatch/default.conf/logwatch.conf | head -n1 | sed -e "s|^\s*DailyReport\s*=\s*||"`
    
    if [ "$DailyReport" != "No" ] && [ "$DailyReport" != "no" ]  
    then  
        logwatch
    fi  
    

    /etc/cron.daily/0logwatchを削除したり実行パーミッションを落とせば良くないか?という発想は甘い。yum updateの際に欠損ファイルが復旧されたり、パーミッションが元通りに修復されてしまう事態がおこるため、こういうものは設定ファイルで動作を止めるのが正義である。


    溢れたrootメールを消す・証拠を残す

    以上の予防処置を講じるまでもなく rootメールが溢れたあとでは、まあだいたいシステム(のディスク残量)が終わっているのでこうするしかない。

    # > /var/spool/mail/root 
    

    ゼロバイト消去をするか、rm -fで削除するかは好みではあるが、いずれにせよ早急にディスク容量を回復する必要がある状況のはずだ。悠長に問題ファイルのバックアップを保存して・・・などという余裕はないだろう。そもそもファイルコピーするだけのリソース余地なぞ残っていまい。

    だが障害報告書に貼り付けるだけの最小限の状況証拠は残しておきたい場合はどうするか。かろうじてリモートターミナル8が繋がっているなら、ターミナルアプリのログ機能を利用して記録を取ることができる。要するにスプールファイルを catでコンソールに流して保存したりスクリーンショットを撮影したりする。

    もっとも真正直に catで全部流すと何時間掛かるか分かったものではないので、 head と tailで最初と最後の数メールぶん程度を記録すれば充分だろう。少なくとも何時からメールが溜まり始め、何時までシステムが動いていたかの問題期間を特定することは出来るはずだ。


    Postfixのドロップメールを消す

    rootメールを外部送信するようにしていながら送信先がなかったり間違っていた場合、maildropやbounceディレクトリが溢れることになる。「それ」が溢れていることが自明9であるならば、敢えてディレクトリの中を lsしようとしたり、rm -fしようとしたりしてはいけない。10

    # rm -f /var/spool/postfix/maildrop/*
    Too many arguments  
    

    この場合は該当ディレクトリごと rm -frして改めてディレクトリを作りなおすか、 find|xargsで消すか、今時の findなら -deleteオプションが備わっているので、それを使って消す。

    # rm -fr /var/spool/postfix/maildrop
    # mkdir /var/spool/postfix/maildrop
    # chown postfix:postdrop /var/spool/postfix/maildrop
    # chmod 730 /var/spool/postfix/maildrop
    
    # find /var/spool/postfix/maildrop/ -type f -print | xargs rm -f
    
    # find /var/spool/postfix/maildrop/ -type f -delete
    

    1. rootで外部送信しない理由は、これが攻撃手段に使えるからだ。

    2. スクリプトには当然Sendmail/Postfixから実行できるオーナーとパーミッションが付いていること。

    3. ここで使うエイリアス名は、ローカルに実在するアカウント名であってはならないし、/etc/aliasesで他用途に使っているエイリアス名であってもならない。

    4. 万一悪意ある攻撃を受けたり乗っ取られて勝手にあれこれされたりした場合、各種メールやsyslogを外部転送してあると事後調査が捗る。

    5. crondは起動/再起動直後の1分間は実際には動作しないので、テストジョブは少なくとも2〜3分先に仕掛ける必要がある。この3分というのが結構長いので、虫取りを何回か繰り返しているとたいがい再起動してたかどうかも忘れてしまう。(SEの頭の方に虫が湧く)

    6. この文法の違いを忘れて cronが正しく動かなかった話はよくある。文脈によってはどちらの crontabを扱っているつもりなのか再確認しないと危ない。通常 crontabコマンドはユーザレベルの定期ジョブ用で、システムタスク用には使わないだろう。だがこれを用いる最大の利点は crondの再起動が不要なことである。

    7. 設計仕様書に何故か含まれていて、要らないのに消すのは面倒という案件とか。

    8. sshdはとうに死んでいて手に負えないことが大多数だろうが、その状況でもシリアルコンソールには何の影響もなく使えるのが普通である。

    9. だいたい dfと duコマンドで何処がディスクを食い潰しているかを絞り込む過程で見つけることになる。

    10. どうしても lsしたい場合は -U(ソートしない)オプションを使わなければならない。これを忘れてswapフリーズに陥ると手に負えなくなる。そうなったプロセスは kill -KILL でも止められないのだ。

    RECENT LINKS