hosting mail lolipop heteml ムームードメイン MRE

メールシステムのリバースプロキシに Nginx を使っているのでご紹介

hosting mail lolipop heteml ムームードメイン MRE

こんにちは。最近、ピストのチェーンを 和泉チエン TOUGH GUARD に替えて、ご機嫌な原口です。

ホスティング事業部の MRE(Messaging Reliability Engineering *ペパボの造語です)というチームで、 SRE ような取り組みを、DNS やメールなどのメッセージングサービスに対して実施しています。

今回は、弊社のホスティングサービスで提供しているメールシステムについてご紹介いたします。

メールシステム構成

弊社のホスティングサービスで提供しているメールシステムは、運用効率化やメールサーバー リプレイス時のダウンタイム削減のため、リバースプロキシを導入しています。

このリバースプロキシについては、過去、dovecot や Courier-IMAP などを利用していましたが、 現在は Nginx に変更しています。メールシステムで Nginx を利用している例は少ないと思うので、 簡単にご紹介させていただきます。

メールシステム構成図

ngx_mail_core_module

Nginx を HTTP のリバースプロキシとして利用することは一般的ですが、MAIL についても 標準でモジュール ngx_mail_core_module が提供されています。

Nginx の設定

POPの設定であれば、以下のような感じです。

mail {
    # mailのproxy機能を使う
    proxy on;
    # 認証サーバーのURI
    auth_http http://127.0.0.1:9000/api/auth/mail/;
    # 認証情報(XCLIENT)をバックエンドに渡す
    xclient on;
    pop3_auth plain cram-md5;
    server {
        listen   110;
        protocol pop3;
    }
}

Nginx が POPプロトコルをしゃべり、メールクライアントと認証情報のやりとりを行います。

その後、その認証情報をまとめて auth_http で指定した認証サーバーに送り、 認証サーバーから、認証成功の status や upstream が返されると、認証以降の通信がプロキシされます。

認証サーバー(HTTP authentication server)

弊社では、認証サーバーを Go で書いており、以下のような感じで、リクエストとレスポンスを 取り扱っています。

認証情報を HTTP のヘッダーで受け取り、認証結果もヘッダーで返します(なぜ、ヘッダーで やりとりしないといけないのか、不満はありますが…)。ヘッダーの詳細については、 ngx_mail_auth_http_module をご参照ください。

// リクエストを受け取る処理
func NewAuthRequest(r *http.Request) *AuthRequest {
	loginAttempt, _ := strconv.Atoi(r.Header.Get("Auth-Login-Attempt"))
	a := &AuthRequest{
		SessionID:    xid.New().String(),
		Method:       r.Header.Get("Auth-Method"),
		Protocol:     r.Header.Get("Auth-Protocol"),
		AuthUser:     r.Header.Get("Auth-User"),
		Pass:         r.Header.Get("Auth-Pass"),
		Salt:         r.Header.Get("Auth-Salt"),
		LoginAttempt: loginAttempt,
		Client: client{
			IP:   net.ParseIP(r.Header.Get("Client-Ip")),
			Host: r.Header.Get("Client-Host"),
		},
	}
	if r.Header.Get("Auth-Ssl") == "on" {
		a.SSLCipher = r.Header.Get("Auth-Ssl-Cipher")
		a.SSLProtocol = r.Header.Get("Auth-Ssl-Protocol")
	}
	return a
}
// レスポンスを組み立てる処理
func (a *AuthResponse) SetHeaders(w http.ResponseWriter) {
	w.Header().Set("Auth-Status", a.Status)
	if a.Status == AuthSuccess {
		w.Header().Set("Auth-User", a.Address)
		w.Header().Set("Auth-Server", a.Server)
		w.Header().Set("Auth-Port", strconv.Itoa(a.Port))
		if a.Pass != "" {
			w.Header().Set("Auth-Pass", a.Pass)
		}
	} else if a.Status == AuthFailure {
		if a.Wait > 0 {
			w.Header().Set("Auth-Wait", strconv.Itoa(a.Wait))
		}
	} else if a.Status == AuthError {
		w.Header().Set("Auth-Error-Code", a.ErrorCode)
	}
}

Nginx の選定理由

リバースプロキシを Nginx に変更した理由は、やはり、Nginx の使用に慣れており、信頼もあったことが 大きかったと思います。またリソース面でもイベント駆動型の Nginx は優秀でした。

2021年8月、弊社サービスのロリポップ!において、POP・IMAP が長時間使用できなくなる 電気通信事故 を 起こしてしまいました(ユーザーの皆様にはご迷惑をおかけし、本当に申し訳ございませんでした)。

この時の原因は、Nginx 以前に使用していた Courier-IMAP のプロセス増加によるメモリ枯渇でした。 現在の Nginx では、半分以下のメモリ使用量で、余裕がある状態でプロキシができています。

まとめ

メールシステムにおいて、Nginx をリバースプロキシとして事例は少ないと思い、紹介させていただきました。 何百万というメールアドレスをホスティングしている環境で利用しても、軽々とさばく Nginx には、 本当に助かっています。

追伸

珍しい構成と思っていたのに、ChatGPT からは一般的と言われてしまいました…

chatgpt に聞いた結果