POP廃止されるんでGmailやめました。無限エイリアスメール受信システムをセルフホストする

以前、無償版G Suiteが廃止されるのでAWS SESとDockerでメール送受信システムを作ったという記事でメール受信システムを作って運用してきました。 これは、手前にSESとLambdaを置くことで、メールを振り分けて、振り分けた結果をlmtpdeliverを使ってHTTPS化したLMTPを使ってDovecotに配信し、それをGmailのPOPで受信させる仕組みになっていました。 この仕組みでG Suiteと変わりなくGmailのインターフェイスでメールを使うことができていたのですが、GmailのPOP3の廃止によってGmailがPOPメールを受信しなくなるため、この仕組みを使い続けることができなくなりました。

いろいろ検討した結果、中継に使用していたDovecotをメインサーバーに昇格させることにしました。 あらかじめ知識がある方に、こういうこともできるよという参考情報としてみていただけると幸いです。

メール受信システムに求めたもの

僕がこのメール受信システムで求めたのは以下のような内容でした。

求める仕組みに対しては Fastmail を選択すればフルマネージドなシステムを得ることもできましたが月775円かかること、iCloud+は最安のプランであれば月150円でしたが、ドメインに対するキャッチオールアドレスは設定できるものの+エイリアスが実現できませんでした。

また、振り分けに関してはある程度サーバーでできた方が良いのですが、最近Appleのメールの受信トレイには「プロモーション」や「トランザクション」といった、Gmailの「プロモーション」や「新着」といった受信トレイレベルでの自動振り分けがあり、これがあることからもGmailの代替として使えそうと思っています。

結果、自作の道を進むことにしました。

これまでのシステムからの改良ポイント

これまでの仕組みは、SESで受けたメールを、GCEのAlways Free枠で作ったVMで動かしているDovecotで一時蓄積、それをGmailのPOPで受信する仕組みでした。

SESは受信状況をDymanoDBに管理情報として書き込んだうえで、別のLambdaがGCE上で動いているlmtpdeliverに向けて送信し、LMTPでDovecotに配信されます。

今回はこれまでの仕組みから、末端にいたGmailがいなくなり、Dovecotに直接メールを蓄積して使用する形態にしました。

しかし、あくまでこのDovecotは中継として使用する前提で設計されており、可用性等の部分で強化が必要となりました。また検索や振り分けの機能を持たせる必要もあります。

  • Dovecotで受信したメールをロストすることができないので、メールを保持しているストレージをバックアップと暗号化をする
  • Webメールインターフェイスを設ける(Roundcubeを採用)
  • 振り分けをManageSieveで設定できる

専用ストレージの準備

これまでDovecotにはs3qlで作ったストレージをマウントしていましたが、検索等で使用するのであれば速度が十分でないことや、破損耐性に難があると考えました。

そこで、まずはGCEのVMにメール専用ストレージを追加することにしました。ストレージはいろいろ検討した結果、単純にPersistent Diskを使うことにしました。 (オブジェクトストレージ系はDovecot側で追加ライセンスが必要になり、Filestoreは費用が高くオーバースペック、個人的な使用のため、受信が止まらなければ一時的なダウンがあってもそこまで問題ないという考えです) 受信に関してはSES+Lambdaで受信していて、AWS側に送信失敗状態で蓄積しておけば、復旧後に再送させることが可能です。

バランス永続ディスク(Balanced Persistent Disk)の10GBを作成して、これまで使っていたDovecotVMにアタッチしました。暗号化キーに関しては、一応顧客管理の鍵をCloud KMSで作成しました。 また、スナップショットを週1回作成することにし、2週分のスナップショットを持つことにしました。1〜2週間程度であれば、SES+Lambda側から再送することで状態を元に戻せるだろうという考えです。 今回の仕組みではここが一番費用がかかる部分で、100円〜200円/月のコストアップになります。

Dovecotのバージョンアップ

Dovecotのイメージもこれを期にバージョンアップすることにしました。Dovecotを2.4系に上げることで、組み込みのfts-flatcurveによる検索機能が使えるようになり、振り分けに使うManageSieveも含まれています。2.4系のイメージからはrootless環境で動くようになっていたため、これまでの設定を生かそうとするとうまくいかなさそうだったため、一度ゼロの状態から設定をやり直しました。(メールデータを蓄積していなかったのでデータが一度飛んでも問題なかった)

docker composeで構築しているので、以下のような設定にしています。

  • MAILDATA_DIR は、専用ストレージのパスを .env で与えています。
  • tls ディレクトリには、Let's Encryptで取得したTLS証明書とキーをtls.crt, tls.keyとして保存しています
services:
  dovecot:
    image: dovecot/dovecot:2.4.1
    ports:
      - "995:31995" # POP3(Gmailから使用、今後GmailのPOP廃止の際に停止する)
      - "993:31993" # IMAPS 
      - "587:31587" # Submission
    volumes:
      - type: bind
        source: "${MAILDATA_DIR}/etc/dovecot/conf.d"
        target: /etc/dovecot/conf.d
      - type: bind
        source: "${MAILDATA_DIR}/etc/dovecot/tls"
        target: /etc/dovecot/ssl
      - type: bind
        source: "${MAILDATA_DIR}/vmail"
        target: /srv/vmail
    restart: always

  lmtpdeliver:
    build: ./lmtpdeliver
    environment:
      LMTP_SERVER: "dovecot:24"
    restart: always

Dovecotの詳細な設定

dovecotの設定は、公式イメージでは /etc/dovecot/conf.d/ に入れて差分を設定していきます。

10-auth.conf

dovecot_config_version = 2.4.1

passdb passwd-file {
  passwd_file_path = /etc/dovecot/conf.d/users
  passdb_default_password_scheme = sha512-crypt
}

userdb passwd-file {
  passwd_file_path = /etc/dovecot/conf.d/users
}

10-lmtp.conf (lmtpdeliverに使用する)

service lmtp {
  inet_listener lmtp {
    port = 24
  }
}

10-pop3.conf

protocols {
  pop3 = yes
}

service pop3-login {
  inet_listener pop3s {
    port = 31990
    ssl = yes
  }
}

20-submission.conf

submission_relay_host = <<SESのSMTPサーバー>>
submission_relay_port = 587
submission_relay_user = <<SESのユーザーID>>
submission_relay_password = <<SESのパスワード>>
submission_relay_ssl = starttls

30-fts.conf

dovecot_config_version = 2.4.1

mail_plugins {
  fts = yes
  fts_flatcurve = yes
}

fts flatcurve {
  # substring_search = no   # まずはOFF推奨(容量/負荷を抑える)
}

# autoindex
fts_autoindex = yes
fts_autoindex_max_recent_msgs = 1000

また、users にuserdb形式のユーザー設定を設置しました。

Roundcubeの設定

Roundcubeをdocker-composeに追加します。データベースはsqliteを使うことで、コストの高いSQLインスタンスを使わずにすみます。

  roundcube:
    image: roundcube/roundcubemail:1.6.x-fpm-alpine-nonroot
    environment:
      - ROUNDCUBEMAIL_DEFAULT_HOST=ssl://dovecot
      - ROUNDCUBEMAIL_DEFAULT_PORT=31993
      - ROUNDCUBEMAIL_SMTP_SERVER=tls://dovecot
      - ROUNDCUBEMAIL_SMTP_PORT=31587
      - ROUNDCUBEMAIL_PLUGINS=managesieve,archive,zipdownload
      - ROUNDCUBEMAIL_SKIN=elastic
      - ROUNDCUBEMAIL_INSTALL_PLUGINS=1
      - ROUNDCUBEMAIL_DB_TYPE=sqlite
    volumes:
      - "${MAILDATA_DIR}/roundcube/html:/var/www/html:rw"
      - "${MAILDATA_DIR}/roundcube/db:/var/roundcube/db"

  proxy:
    image: nginx
    volumes:
      - type: bind
        source: "${MAILDATA_DIR}/etc/nginx"
        target: /etc/nginx
      - type: bind
        source: ${LETSENCRYPT_CERT_PATH}
        target: /etc/letsencrypt
      - "${MAILDATA_DIR}/roundcube/html:/srv/roundcube/html:ro"
    ports:
      - "5443:443"  # Roundcube向け
      - "5980:5980" # lmtpdeliver向け

起動すると /var/www/html にroundcubeのPHPが展開されるので、その中にある設定ファイル config/config.inc.php にRoundcubeの設定を追加できます。 DovecotSSLになっていますが、コンテナ名で接続すると証明書のホスト名と一致せず認証に失敗するため、ホスト名を指定して接続する設定を加える必要があります。また、ManageSieveが使えるようにポートを指定します。

<?php
    // ...省略
    include(__DIR__ . '/config.docker.inc.php');


    $config['managesieve_host'] = 'ssl://dovecot:34190';

    // TLS検証オプション(IMAP/SMTP/ManageSieve 共通)
    $conn_opts = [
      'ssl' => [
        'verify_peer'       => true,
        'verify_peer_name'  => true,
        'allow_self_signed' => false,
        'peer_name'         => 'example.com', // TLS証明書のホスト名を入れる
        'cafile'            => '/etc/ssl/certs/ca-certificates.crt',
      ],
    ];
    $config['imap_conn_options']       = $conn_opts;
    $config['smtp_conn_options']       = $conn_opts;
    $config['managesieve_conn_options']= $conn_opts;

RoundcubeのコンテナはFastCGIと動作するため、別途HTTPSサーバーを用意します。 元々、TLSを終端するためにnginxをproxyとして設置していましたのでこちらを使用します。

events {
    worker_connections  16;
}

http {
    include /etc/nginx/mime.types;

    server {
        listen 443 ssl;
        ssl_certificate     <<SSL_path>>;
        ssl_certificate_key <<SSL_path>>;
        server_name <<ServerName>>;

        root /srv/roundcube/html;
        index index.php;
 
        location ~* \.(?:css|js|png|jpg|jpeg|gif|ico|svg|ttf|woff|woff2)$ {
            try_files $uri =404;
            access_log off;
            expires 7d;
        }

        location / {
            try_files $uri $uri/ /index.php?$query_string;
        }

        location ~ \.php$ {
            include fastcgi_params;

            fastcgi_param SCRIPT_FILENAME /var/www/html$fastcgi_script_name;

            fastcgi_param SCRIPT_NAME     $fastcgi_script_name;
            fastcgi_param PATH_INFO       $fastcgi_path_info;
            fastcgi_param HTTPS           on;

            fastcgi_pass roundcube:9000;
            fastcgi_read_timeout 120s;
            fastcgi_intercept_errors on;
        }

        location ~ ^/(config|temp|logs|vendor)/ { deny all; }
    }

    server {
        listen 5980 ssl;
        client_max_body_size 500M;
        ssl_certificate     <<SSL_path>>;
        ssl_certificate_key <<SSL_path>>;
        server_name <<ServerName>>;
        location / {
            auth_basic "Restricted";
            auth_basic_user_file <<userfile_path>>;
        }
        location /delivery {
            auth_basic "Restricted";
            auth_basic_user_file <<userfile_path>>;
            proxy_pass http://lmtpdeliver:8080/delivery;
            proxy_redirect off;
        }
    }
}

GmailのPOP設定の変更

GmailのPOPサポートがあるうちは並行運用として使い続けるのですが、GmailからPOPでアクセスされた場合はメールを削除する設定になっていました。

今後Dovecotにもメールを残していくので、メールを削除する設定を解除しました。

他のメールサーバーからのメールを受信する

GmailのPOPでは、DovecotだけでなくプロバイダのメールもPOP受信させていました。これも使えなくなるため、Cloud Run Jobsでfetchmailを実行して、Lambdaが使っているlmtpdeliverに受信したメールをPOSTするようにしました。

以下のプロジェクトにまとめています。

github.com

https系のサービスをCloud Runの裏側に隠す

しばらく使っていると、nginxに /.git/credentials とか /+CSCOL+/ だとかの攻撃狙いのアクセスが多くあることがわかりました。404を返すだけなので特に問題はないものの、気味が悪いのでCloud Runを手前に置くことで、RoundcubeにはIdentity-Aware Proxy認証を、lmtpdeliverにはIAM認証をかけることにしました。

Cloud Runでnginxを動かすには、Dockerfileとdefault.conf.templateを用意してデプロイするだけで動きます。default.conf.templateでデプロイしておくことで、環境変数がenvsubstで展開されます。これを環境変数や認証設定をあわせてデプロイしておくことで、

Dockerfile

FROM nginx:stable-alpine
COPY default.conf.template /etc/nginx/templates/default.conf.template

default.conf.template

server {
    listen ${PORT};
    location / {
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header Host ${MAIL_PROXY_DOMAIN};
        proxy_ssl_name ${MAIL_PROXY_DOMAIN};
        proxy_pass https://${MAIL_PROXY_HOST}/;
    }
}

また、Direct VPC Egressの設定を加えたので、プライベートネットワーク経由でGCE VMへアクセスできるようになりました。 これで、GCE VMHTTPSサービスをインターネットから直接アクセスできないようにファイアウォールを調整しました。

かかっている費用

実際にかかっている費用をみると、このシステムを導入した後の費用は月200円程度のアップで収まりました。具体的に上がったのはメール用のPersistent Diskとそのスナップショットにかかる費用のみで、Cloud Runをフロントエンドに置く仕組みは今月に入ってから入れましたが、現段階では無料枠に収まっています。

最後に

最終的な構成を図に起こすとこんな感じ。

今回の移行は10月から順次構築してきましたが、Cloud Runをフロントに置いた今後は、Gmailのメールを移動させたりして本格的な利用を開始していきます。 SPAMフィルタはまだ入っていませんが、無限エイリアスを使ってメールの送信元を細かく管理し、必要に応じてSPAMが届くようなメールアドレスは捨てる(Lambdaの段階で配信しないようにフィルタする)ことで、きちっと管理ができている(SPAMが届かない状態を保てている)ので今後検討していこうと思っています。

長期的な使用感についてはレポートしていければ良いと思っています。