'fix'
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
Rikuoh Tsujitani 2023-08-26 11:56:45 +09:00
parent 86d2f3a718
commit ee199ce548
3 changed files with 135 additions and 18 deletions

View file

@ -1,18 +1,18 @@
---
title: "MailuでWeb UI付きのメールサーバを所有する"
date: 2023-08-25T19:40:24+09:00
date: 2023-08-26T11:40:24+09:00
draft: true
tags: ['tech']
---
初夏の辺りには[こんなこと]()を言っていたが、せっかくVPSの計算資源が余っているのでやはり自前でメールサーバを建てることにした。数多の艱難辛苦に見舞われた10年前と今では状況がずいぶん違う。今やDockerがあり、優れたOSSがあり、これまでに培ってきたトラブルシューティングの知見がある。躓いたらいつでもコンテナを破棄してやり直せばいい。なにか建てるたびにディレクトリの至るところに引っかき傷を残していた10年前とは違う
初夏の頃には[こんなこと](https://riq0h.jp/2023/05/05/213838/)を言っていたが、VPSの計算資源が余っているのでやはり自前でメールサーバを建てた。なにしろ数多の艱難辛苦に見舞われた10年前と今では状況がずいぶん違う。今やDockerがあり、優れたOSSがあり、これまでに培ってきたトラブルシューティングの知見がある。躓いたらいつでもコンテナを破棄してやり直せばいい。なにかを建てるたびどこかに引っかき傷を残す恐れはもうない
[Mailu]()というOSSがある。メールサーバに必要な構成が統合されていて全部よしなにやってくれる上にWeb UIまで付いてくるすごいやつだ。こんなのがあるんだったらVPSを契約している身でわざわざ外部の事業者に金を払っている場合ではない。そうして、僕は意気揚々とサーバの構築に乗り出したのだった。**結論から言うと、考えが甘かった。** 相変わらずメールサーバは手強い。本稿はMailUを利用したメールサーバの構築について記す。
[Mailu](https://github.com/Mailu/Mailu)というOSSがある。メールサーバに必要な構成が統合されていて全部よしなにやってくれる上にWeb UIまで付いてくるすごいやつだ。こんなのがあるんだったらVPSを契約している身でわざわざ他所に金を払っている場合ではない。こうして、僕は意気揚々とサーバの構築に乗り出したのだった。**結論から言うと、考えが甘かった。** 相変わらずメールサーバは手強い。本稿はMailuを利用したメールサーバの構築について記す。
## ファイルの取得と編集
`docker`および`docker-compose`はすでに導入されているものとする。専用のユーザでホームディレクトリ直下に作業フォルダを作成する。以降は`docker-compose.yml`の雛形をコピペして持ってくる形が一般的だが、Mailuの場合は[セットアップウィザード]を使うとユーザの意図に適った設定ファイルを出力してくれる。当然、これらのファイルはdocker-compose.ymlと`mailu.env`ファイルなので後からでも編集できる。ただし、動作検証を済ませるまではWebメールクライアントを有効にした方がよいと思われる
`docker`および`docker-compose`はすでに導入されているものとする。専用のユーザでホームディレクトリ直下に作業フォルダを作成する。以降は`docker-compose.yml`の雛形をコピペして持ってくる形が一般的だが、Mailuの場合は[セットアップユーティリティ](https://setup.mailu.io/2.0/)を使うとユーザの意図に適った設定ファイルを出力してくれる。これらのファイルは`docker-compose.yml`と`mailu.env`ファイルなので後からでも編集できる。ただし、動作検証を済ませるまでは使う予定がなくともWebメールクライアントを有効にした方がよい。
重要なポイントとして、ポート設定の`80:80`と`443:443`の左側は必ず他の番号に変更しなければらない。メールサーバ以外のサービスを一切立ち上げていないのならともかく、これらの内向きポートは確実に専有されている。僕は`7900:80`、`8443:443`にした。メールサーバなのにHTTP/HTTPSポートが必要なはタイトル通りWeb UIが備わっているためだ。この修正に際して`mailu.env`に以下の追記が求められる。
重要なポイントとして、ポート設定の`80:80`と`443:443`の左側は必ず他の番号に変更しなければらない。メールサーバ以外のサービスを一切立ち上げていないのならともかく、これらの内向きポートは確実に専有されている。僕は`7900:80`、`8443:443`にした。メールサーバなのにHTTP/HTTPSポートが必要な理由はタイトル通りWeb UIが備わっているためだ。この修正に際して`mailu.env`に以下の追記が求められる。
```env
REAL_IP_HEADER=X-Real-IP
@ -20,27 +20,144 @@ REAL_IP_HEADER=X-Real-IP
REAL_IP_FROM=あんたのグローバルIP
```
ウィザードの"Choose how you wish to handle security"の欄はいまいちピンと来ないかもしれない。しかし最終的には大抵`letsencrypt`か`mail-letsencrypt`を選択することになる。いや、SSL証明書ならもう持っているけど ……そう思うのも無理はないが、その方が明らかに楽できる。とはいえ、下手に連続施行して取得規制に引っかかっては困るので準備が整うまでは`cert`を選択することをおすすめする
ユーティリティの"Choose how you wish to handle security"の欄はいまいちピンと来ないかもしれない。しかし最終的には大抵`letsencrypt`か`mail-letsencrypt`を選択することになる。SSL証明書をすでに持っているかどうかは関係がない。理由は後述する。セットアップの段階ではむやみな連続試行による取得規制を避けたいので`cert`を選択しておく
セットアップウィザードの甲斐あって他に修正すべき箇所はあまりない。**今は。**もし皆さんがCloudflareなどのCDNを間に挟んで**いなければ**ここからの話はかなりスムーズになる。だが、今どきそんな人いるか それこそ10年前とは違う。今やなんでもCDNの時代だ。なんでもCDNの上に乗っているものだからオリジンサーバが文字通り雲隠れしたように見える。ところがメールサーバはそれが通用しない。だから面倒くさいんだ。
自動生成の甲斐あって他に修正すべき箇所はあまりない。もし皆さんがCloudflareなどのCDNを間に挟んで**いなければ**ここからの話はかなりスムーズになる。……だが、今どきそんな人いるか それこそ10年前とは違う。今CDNの時代だ。なんでもCDNの上に乗っているものだからオリジンサーバが文字通り雲隠れしたように見える。ところがメールサーバはそれが通用しない。だから面倒くさいんだ。
## 特殊なDNSの設定
Cloudflareの設定を例にとる。メールサーバに用いるドメインを選択して**DNS → レコード**からレコードを設定する。通常であればサーバのIPアドレスをAレコードでドメインと紐づけて、MXレコードにドメインを登録しておしまいだが、メールサーバの場合は「プロキシ」を有効にしては**ならない。** 適用後の表示が「DNSのみ」になるように設定する。
## DNSレコードの設定
Cloudflareの設定を例にとる。メールサーバに用いるドメインを選択して**DNS → レコード**からレコードを設定する。通常であればサーバのIPアドレスをAレコードでドメインと紐づけて、MXレコードに当該のドメインを登録しておしまいだが、メールサーバの場合は「プロキシ」を有効にしては**ならない。** 適用後の表示が「DNSのみ」になるように設定する。
つまり、メールサーバに用いるドメインはCloudflareの恩恵を受けられない。それがなにを意味するのか 彼らのSSL証明書を使えないのだ。僕がこのブログで散々おすすめしているCloudflareのオリジンサーバ証明書はもちろん、エッジ証明書すらもらえない。そう、同じドメイン下なら使える証明書がメールサーバに振ったサブドメインに限っては使えないので、別の証明書を新たに発行しなければならないのである
これはあえて有効にした状態でMXレコードを引いてみると事情がよく判る。CDNを挟んだ過程でなにかがおかしくなってしまうのか、サブドメイン部分が不正な値に変換されて出力されてしまうようだ。この状態ではメールの送受信に支障が生じたり、メールクライアントから接続できないなどの不具合に見舞われる。公式のドキュメントでも[やるなと書いてある。](https://developers.cloudflare.com/support/other-languages/%E6%97%A5%E6%9C%AC%E8%AA%9E/cloudflare%E3%81%AE%E4%BD%BF%E7%94%A8%E6%99%82%E3%81%AB%E3%83%A1%E3%83%BC%E3%83%AB%E3%81%8C%E9%85%8D%E4%BF%A1%E4%B8%8D%E8%83%BD%E3%81%AB%E3%81%AA%E3%82%8B/)以下に実例を示す
もちろんすでに持っているSSL証明書がCDNのものではなく、それこそLet's encryptとかZero SSLなら使い回せる。しかし、本稿で紹介するのはDockerを利用した構築方法だ。Dockerの内部で動いているサーバから外部の証明書を直接参照する方法は用意されていない。公式の手段では特定のディレクトリにコピペする形をとる。
```zsh
$ dig mx mystech.ink
...中略...
;; ANSWER SECTION:
mystech.ink. 300 IN MX 10 mx.mystech.ink. #正常なら設定した通りのMXレコードが引ける
とはいうものの、大半の人が使っている無料のSSL証明書は有効期限が3ヶ月しかない。3ヶ月ごとにいちいち更新した証明書をコピペするのか あるいは別途、スクリプトを書いてcronに実行させる ウーン、どうにも洗練さに欠ける。僕たちは最先端のOSSでちょっぱやのメールサーバを建てているはずじゃなかったのか
$ dig mx mystech.ink
...中略...
;; ANSWER SECTION:
mystech.ink. 300 IN MX 10 _dc_24782934.mystech.ink. #プロキシが有効だと不正な値に変換される
```
そこで、前述した`letsencrypt`が顔をのぞかせる。これはメールサーバが初回起動時に設定したドメイン用の証明書を自動で取得して、更新も実行してくれる設定なのだ。こいつに任せておけばこの問題はDockerコンテナの内部で完結する。よし、話はこれで終わりか 後は起動して寝るだけか 残念、もう一つある。
なるほどでは「DNSのみ」にすれば万事解決か、というとそうでもない。証明書の問題が残る。Cloudflareの恩恵を受けられるのなら自動的にエッジ証明書があてがわれて、望むならオリジンサーバ証明書も無料で取得できるが今回はそれが使えない。したがって、メールサーバ用に個別のSSL証明書を用意しなければならないのである。
このOSSはWeb UIを備えている。つまりブラウザで管理画面に接続できる。どのURLで なにも工夫していなければメールサーバのドメインと同じはずだ。そうすると、厄介な話になる。Let's encryptは80番ポートを使って証明書を取得する。たとえHTTPSでしか接続を受け付けたくなくとも開けておかなければならない。にも拘らずCloudflareの恩恵を受けていないので、エッジ証明書は存在しない。とんでもなくガバい管理画面の出来上がりだ。
すでに持っているSSL証明書がCloudflareやCDNのものでなく、自前で取得したLet's encryptとかZero SSLなら通常は使い回せる。しかし、本稿で紹介するのはDockerを利用した構築方法だ。Dockerコンテナの内部で動いているサーバから外部の証明書を直接参照する方法は用意されていない。公式の手段では特定のディレクトリにコピペする方式を採っている
あれ、`letsencrypt`の設定が自動で取得してくれるのでは いいや、そいつはDockerコンテナの中にあるのでnginxのリバースプロキシで指定できない。じゃあ……一体どうするんだ。ブラウザ用の証明書も別にとれってのか 一体何枚の証明書がいるんだ 全然便利でもなんでもないじゃないか
とはいえ、大半の人が使っている無料のSSL証明書は有効期限が3ヶ月しかない。3ヶ月ごとにいちいち更新した証明書を貼り直したり、わざわざスクリプトを書いてcronに実行させるのは洗練されているとは言いがたい。僕たちは最先端の統合スイートOSSでちょっぱやのメールサーバを建てているのではなかったのか
実はすばらしい解決策がある。メールサーバ用のサブドメインと、管理画面用のサブドメインを別々に用意すればいい。hogafuga.comというサイトがあったとすると、Aレコードをmail.hogefuga.comに振ってこっちを管理画面用にする。そして、メールサーバ用にはmx.hogefuga.comなどを割り当てて、MXレコードを振る
そこで、前述した`letsencrypt`が顔を覗かせる。これはメールサーバが初回起動時に決めたドメイン用の証明書を自動で取得して、更新作業も実行してくれる設定なのだ。こいつに任せておけば証明書の問題はDockerコンテナの内部で完結する。このOSSが単純なメールサーバであれば話はこれで終わりだった
この時、`letsencrypt`の設定で自動取得させるドメインの対象は`mx.hogefuga.com`の方だ。`mail.hogefuga.com`はただのWebフロントエンドに過ぎないので堂々とCloudflareのプロキシを有効にできる。こうすると`mx.hogefuga.com`はメールクライアントで設定する際にしか用いられないのでWebの方の心配をする必要がなくなり、`mail.hogefuga.com`はCloudflareの恩恵がさんさんと降り注いで勝手にSSL化される寸法となる
だが、僕たちはWeb UI付きのメールサーバを建てようとしている。つまり、Web経由で管理画面なりWebメールクライアントに接続する。`letsencrypt`の自動取得機能は80番ポートで通信を行う。ここで懸念となるのは、管理画面やWebメールクライアントに用いるサブドメインと、メールサーバのサブドメインが同じだった時に、80番ポートが露出したWebサーバを公開してしまうことだ
簡単そうに言ったがけどな、この解決策を探し当てるまでに2日もかかったよ僕は。もしかしてこれって割と当たり前のメソッドだったりする
通常、80番ポートへのアクセスはCDNかリバースプロキシでリダイレクト処理を行うが、Cloudflareの恩恵を受けられない上に`letsencrypt`の設定で証明書周りを済ませたい僕たちにこれらの手段は使えない。では、どうすべきか。Web UI自体を使わない手もあるが、それならわざわざMailuのようなOSSを選ぶ理由はない。面倒な管理をGUIで完結させたいからWeb UI付きのメールサーバを選んでいるんじゃないか。
幸いにも解決策はある。管理画面用のサブドメインと、メールサーバ用のサブドメインをそれぞれ別に設ければいい。`hogefuga.com`というドメインを例にとると、管理画面用に`mail.hogefuga.com`、メールドメイン用に`mx.hogefuga.com`をそれぞれ作る。単なるWebフロントエンドに過ぎない前者はプロキシを有効化してCDNのSSLを証明書を得られるし、後者は安全に80番ポートを開けて`letsencrypt`の設定を使えるようになる。以上を踏まえた例を下記に示す。
```dns
hogefuga.com IN A 111.111.111.111 #メールアドレスの後ろ半分となるルートドメイン。プロキシ化する。
mail.hogefuga.com IN A 111.111.111.111 #管理画面用のAレコード。プロキシ化する。
mx.hogefuga.com IN A 111.111.111.111 #メールサーバ用のAレコード。プロキシ化しない。
hogefuga.com IN MX mx.hogefuga.com #メールサーバ用のMXレコード。プロキシ化しない。
```
この例では管理画面に接続する時は`mail.hogefuga.com`を使って、メールクライアント上でドメインを指定する際には`mx.hogefuga.com`を用いる形となる。`mailu.env`ファイルの方もこれに合わせた形に修正する。ユーティリティから再度作成し直しても構わない。
```env
# Main mail domain
DOMAIN=あんたのドメイン
# Hostnames for this server, separated with comas
HOSTNAMES=mx.あんたのドメイン
...中略...
# Website name
SITENAME=mail.あんたのドメイン
# Linked Website URL
WEBSITE=https://mail.あんたのドメイン
```
## ポートとリバースプロキシの設定
予めポートの開放を済ませる。公式のドキュメントではメールプロコトルに関するすべてのポートを開放するように書かれているが、ほとんどのユースケースではSSL接続のIMAPしか利用しないと考えられる。よって、ポートの開放も最低限のみ行う。特に25番ポートはよく攻撃の標的にされるのでできれば閉じておきたい。
```zsh
$ ufw allow 80
$ ufw allow 443
$ ufw allow 993
$ ufw allow 465
$ ufw reload
```
続いて、リバースプロキシの設定を書く。先に書いたように管理画面ないしはWebメールクライアント用と、メールサーバ用の2つのファイルを用意する。
```conf
#Webメールクライアント用
server {
server_name mail.あんたのドメイン;
location / {
proxy_pass https://localhost:8443;
proxy_set_header Host $host;
proxy_set_header Connection $http_connection;
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
listen 443 ssl http2;
ssl_certificate /etc/ssl/certs/あんたのドメイン.pem;
ssl_certificate_key /etc/ssl/private/あんたのドメイン.key;
}
```
CloudflareのSSL/TLS設定で「フル厳密」を用いる環境でなければオリジンサーバの証明書は除いても問題ない。自動で付与されるエッジ証明書でも機能する。なぜか判らないがMailuは内向きでもHTTPSで通信しているため、`proxy_pass`をその通りに記す必要がある。
```
#メールサーバ用
server {
listen 80;
server_name mx.mystech.ink;
location / {
proxy_pass http://localhost:7900;
proxy_set_header Host $host;
proxy_set_header Connection $http_connection;
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
```
メールサーバ用は`letsencrypt`のために80番ポートが欲しいだけなので簡素な設定で済む。2つのファイルを作成したら`systemctl restart nginx`でnginxを再起動する。
## 初回起動と管理画面の設定
いよいよ初回起動に入る。`docker-compose up`でバックエンドではなくログ垂れ流しで起動する。文字列の中でSSL証明書が取得されたっぽい英文を見つけたら、ひとまず成功と見ていいだろう。Ctrl+cでサーバを停止させた後に`docker-compose up -d`で立ち上げ直して、アドミンユーザの作成を行う。
```zsh
$ docker-compose exec admin flask mailu admin ユーザ名 ドメイン 'パスワード'
```
上記の操作で`ユーザ名@ドメイン`をID、`パスワード`をパスワードとするアドミンアカウントが作成される。ここでようやく管理画面用のURLにアクセスして諸々の設定を済ませる過程に入る。**メールドメイン → ドメイン名を追加**からメールアドレスの後ろ半分にしたいルートドメインを記入する。
![](/img/207.png)
次に「管理」カテゴリの赤丸で囲ったアイコンからユーザを作成する。ここで作成するユーザの名前がメールアドレスの前半分になる。最後に「操作」カテゴリの赤丸で囲ったアイコンから右上の「鍵を再生成」を押す。すると、SPF/DKIM/DMARCの設定に必要な情報が表示されるのでそれぞれをDNSレコードに登録しに行く。これらは現代のメールにおいて必須のセキュリティであるため、必ず登録しなければならない。
## 送受信の確認
管理画面からWebメールに移動して実際に送受信を行う。向こうからのメールがこちらに届いて、その逆にも問題がなければメッセージのソースからSPF/DKIM/DMARCの認証が行えているか確認する。すべてがクリアなら以下のように承認される。
![](/img/208.png)
さらに同様の作業を普段使っているメールクライアントからも行えるかチェックしておく。ログイン情報は管理画面の「クライアント設定」から見ることができる。いずれの場合でも問題がなければメールサーバの構築作業は完了である。
## スパムの検証
MailuにはRspamdというかなり強力なスパムメールフィルタが入っている。大抵はデフォルト設定でよしなにやってくれるが、万が一届くべきメールが届いていない気がする時には管理画面の「スパムメール対策」から検証できる。「History」を覗くとこれまでにフィルタされたメールの一覧が表示される。たまに見ると面白い。適正なメールがスパム判定されすぎている場合は管理画面の「設定」でスパムフィルターの閾値を上げると改善する。

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB