k8sで自宅メールサーバを建てたい
Table of Contents
ベアメタルk8s上に、docker-mailserverを使っておうちメールサーバをたてることにした
モチベーション
- 容量を気にせず使えるメールサーバがほしい
- gmail等のフリーメールも便利だが、電子メールという技術の仕組み上、無料枠を永久に使い潰していくのが精神衛生上よくないので。
- 運用中のサービスから気軽に利用できるメールサーバがほしい
- misskeyからのメール送信用。ないとパスワードの再設定等ができない。
- 死活監視のアラート用。discordのwebhookへの通知とかでも良いが、電子メールという枯れたプロトコルであれば対応しているサービスが多いので、通知の統一がしやすそう。
前提
こちらのコンテナを使っていく。
docker-mailserverには、メールの送受信を行うためのPostfix/Dovecotの他にも、ClamAVというフリーのアンチウイルスソフトやfail2ban等の不正アクセス防止ツールが含まれている。
Advanced | Kubernetes - Docker Mailserver
k8sへのデプロイに際して上記ドキュメントを参照した。上記リンク内の注にもあるとおり、k8sは正式にサポートされているものではないので注意が必要。
構成
手順
ConfigMapの設定
apiVersion: v1
kind: ConfigMap
metadata:
name: mailserver.environment
namespace: nginx
immutable: false
data:
TLS_LEVEL: modern
POSTSCREEN_ACTION: drop
OVERRIDE_HOSTNAME: mail.sample.com # - ①
FAIL2BAN_BLOCKTYPE: drop
POSTMASTER_ADDRESS: xxx@sample.com
UPDATE_CHECK_INTERVAL: 10d
POSTFIX_INET_PROTOCOLS: ipv4
ONE_DIR: '1'
ENABLE_CLAMAV: '0'
ENABLE_POSTGREY: '0'
ENABLE_FAIL2BAN: '1'
AMAVIS_LOGLEVEL: '-1'
SPOOF_PROTECTION: '1'
MOVE_SPAM_TO_JUNK: '1'
ENABLE_UPDATE_CHECK: '1'
ENABLE_SPAMASSASSIN: '1'
SUPERVISOR_LOGLEVEL: warn
SPAMASSASSIN_SPAM_TO_INBOX: '1'
RELAY_HOST: 'smtp.ocn.ne.jp' # - ②
RELAY_PORT: '587'
RELAY_USER: xxx@xxx.ocn.ne.jp # - ③
RELAY_PASSWORD: xxx
SSL_TYPE: manual # - ④
SSL_CERT_PATH: /secrets/ssl/rsa/tls.crt
SSL_KEY_PATH: /secrets/ssl/rsa/tls.key
---
kind: PersistentVolume # - ⑤
apiVersion: v1
metadata:
name: pv-mail-config
namespace: nginx
spec:
storageClassName: mail-config
capacity:
storage: 5Mi
accessModes:
- ReadWriteOnce
nfs:
server: {NFSのIP}
path: /k8s/mail/config
---
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: pvc-mail-config
namespace: nginx
spec:
storageClassName: mail-config
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Mi
①: ここをsample.com
とすると、ユーザ作成済なのに以下のエラーが出る
postfix/smtpd[2150]: NOQUEUE: reject: RCPT from mail-oo1-f43.google.com[209.85.161.43]: 550 5.1.1 <xxx@sample.com>: Recipient address rejected: User unknown in local recipient table; from=<hogehoge@gmail.com> to=<xxx@sample.com> proto=ESMTP helo=<mail-oo1-f43.google.com>
これはOVERRIDE_HOSTNAMEがメールのドメインと一致していると出るエラーぽいので、mail.sample.com
に変えることで解決。
↓参考にしたページ
Postfixの『Recipient address rejected: User unknown in local recipient table』で15分ほどハマった話 - DENET 技術ブログ
②: P25B対策のため、OCNのSMTPサーバでリレーを行うための設定。
以下プロバイダがOCNの人しか関係ない話。古い文献だと、smtp.xxx.ocn.ne.jp
(xxx
にはblue
やmarble
などが入る)が正しいと書いてある場合もあるが、現状はsmtp.ocn.ne.jp
が正。
③: SMTP-AUTHに使うユーザ。プロバイダに通知されているものを入力。@以降も入れる。
④: 既にingress-nginxでのhttps通信に利用している証明書と秘密鍵を利用する。
cert-managerによる証明書取得&自動更新等はこちらの記事で扱っているので省略。
⑤: 参考ページでは以下のようにアカウント情報をConfigMapで決め打ちしていたが、これだとアカウントを動的に追加することができないので、PVを作成する。
---
apiVersion: v1
kind: ConfigMap
metadata:
name: mailserver.files
data:
postfix-accounts.cf: |
test@example.com|{SHA512-CRYPT}$6$someHashValueHere
other@example.com|{SHA512-CRYPT}$6$someOtherHashValueHere
ちなみに、手動でユーザを追加するにはコンテナ内で以下コマンドを実行する必要がある。
setup email add user@example.com password
setup email list # メールアドレスのリストを確認
ユーザなしの状態でコンテナを起動すると、以下のように120秒以内にメールアドレスを作るように言われるので、コンテナ内で上記コマンドを叩けば良い。
4nm1tsu@ubuntu1:~/manifests/mail$ kubectl logs -n nginx mailserver-b5d69757f-vc6s2 -f
[ INF ] Welcome to docker-mailserver 12.1.0
[ INF ] Checking configuration
[ INF ] Configuring mail server
[ WARNING ] You need at least one mail account to start Dovecot (120s left for account creation before shutdown)
[ WARNING ] You need at least one mail account to start Dovecot (110s left for account creation before shutdown)
[ WARNING ] You need at least one mail account to start Dovecot (100s left for account creation before shutdown)
[ WARNING ] You need at least one mail account to start Dovecot (90s left for account creation before shutdown)
[ WARNING ] You need at least one mail account to start Dovecot (80s left for account creation before shutdown)
[ WARNING ] You need at least one mail account to start Dovecot (70s left for account creation before shutdown)
[ INF ] Starting daemons
[ INF ] mail.sample.com is up and running
データ用PV作成
メールのデータ保存用にPVを作成する。
apiVersion: v1
kind: PersistentVolume
metadata:
name: mail-pv
spec:
capacity:
storage: 20Gi
persistentVolumeReclaimPolicy: Retain
accessModes:
- ReadWriteOnce
storageClassName: mail-nfs
nfs:
server: {NFSのIP}
path: "/k8s/mail"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mail-pvc
namespace: nginx
annotations:
volume.beta.kubernetes.io/persistent-volume: mail-pv
spec:
storageClassName: mail-nfs
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
サービス公開
MetalLB経由で公開する。
参考ページの例から大きく変更した部分はハイライトしている。
apiVersion: v1
kind: Service
metadata:
name: mailserver
namespace: nginx
labels:
app: mailserver
annotations:
metallb.universe.tf/loadBalancerIPs: {公開用IP(MetalLBのIPアドレスプールから選択)}
spec:
type: LoadBalancer
externalTrafficPolicy: Local #
selector:
app: mailserver
ports:
# Transfer
- name: transfer
port: 25
targetPort: transfer
protocol: TCP
# ESMTP with implicit TLS
- name: esmtp-implicit
port: 465
targetPort: esmtp-implicit
protocol: TCP
# ESMTP with explicit TLS (STARTTLS)
- name: esmtp-explicit
port: 587
targetPort: esmtp-explicit
protocol: TCP
# IMAPS with implicit TLS
- name: imap-implicit
port: 993
targetPort: imap-implicit
protocol: TCP
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mailserver
namespace: nginx
annotations:
ignore-check.kube-linter.io/run-as-non-root: >-
'mailserver' needs to run as root
ignore-check.kube-linter.io/privileged-ports: >-
'mailserver' needs privilegdes ports
ignore-check.kube-linter.io/no-read-only-root-fs: >-
There are too many files written to make The
root FS read-only
spec:
replicas: 1
selector:
matchLabels:
app: mailserver
template:
metadata:
labels:
app: mailserver
annotations:
container.apparmor.security.beta.kubernetes.io/mailserver: runtime/default
spec:
hostNetwork: true #
hostname: mail
containers:
- name: mailserver
image: ghcr.io/docker-mailserver/docker-mailserver:latest
imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
runAsUser: 0
runAsGroup: 0
runAsNonRoot: false
privileged: false
capabilities:
add:
# file permission capabilities
- CHOWN
- FOWNER
- MKNOD
- SETGID
- SETUID
- DAC_OVERRIDE
# network capabilities
- NET_ADMIN # needed for F2B
- NET_RAW # needed for F2B
- NET_BIND_SERVICE
# miscellaneous capabilities
- SYS_CHROOT
- KILL
drop: [ALL]
seccompProfile:
type: RuntimeDefault
# You want to tune this to your needs. If you disable ClamAV,
# you can use less RAM and CPU. This becomes important in
# case you're low on resources and Kubernetes refuses to
# schedule new pods.
resources:
limits:
memory: 4Gi
cpu: 1500m
requests:
memory: 2Gi
cpu: 600m
volumeMounts:
- name: files
mountPath: /tmp/docker-mailserver
readOnly: false
# PVCs
- name: data
mountPath: /var/mail
subPath: data
readOnly: false
- name: data
mountPath: /var/mail-state
subPath: state
readOnly: false
- name: data
mountPath: /var/log/mail
subPath: log
readOnly: false
# certificates
- name: certificates-rsa
mountPath: /secrets/ssl/rsa/
readOnly: true
# other
- name: tmp-files
mountPath: /tmp
readOnly: false
ports:
- name: transfer
containerPort: 25
protocol: TCP
- name: esmtp-implicit
containerPort: 465
protocol: TCP
- name: esmtp-explicit
containerPort: 587
- name: imap-implicit
containerPort: 993
protocol: TCP
envFrom:
- configMapRef:
name: mailserver.environment
restartPolicy: Always
volumes:
# configuration files
- name: files
persistentVolumeClaim:
claimName: pvc-mail-config
# PVCs
- name: data
persistentVolumeClaim:
claimName: mail-pvc
# certificates
- name: certificates-rsa
secret:
secretName: letsencrypt-cert
items:
- key: tls.key
path: tls.key
- key: tls.crt
path: tls.crt
# other
- name: tmp-files
emptyDir: {}
ルータのポートフォワーディング設定
以下のポートへの通信を、メールのサービスを公開しているIPへと転送する。
- 25(SMTP)
- 465(SMTP over SSL)
- 587(submission)
- 993(IMAP3)
MUAからアクセス
クライアントはthunderbirdを利用した。
たとえばno-reply
ユーザでログインしたい場合、以下のように入力すればメールサーバへアクセスできる。
受信は問題なくできたが、送信時にいくつかトラブルが出た。
sender address rejected: (in reply to RCPT TO command)と言われる
いつからなのかは分からないが、OCNのリレーサーバにメールをリレーするとき、メールのenvelope-fromをOCNのメールアドレスにしないといけなくなったらしい。
大学時代(2018年ごろ)にメールサーバを建てていたときは少なくともその必要はなかったはずなので、知らないうちに仕様が変わっていたようだ。公式のアナウンスはなさそうだった。
OCNメールサーバへのリレー設定 ~Sub-missionポート編~
自宅サーバのメールがOCNでリレーしてもらえなくなった - お茶漬けぶろぐ
上記ブログを参考に、/etc/postfix/main.cf
内に以下を追記、およびOCNメールアドレスを記載したsender_maps
ファイルを追加することで解決した。
...
local_header_rewrite_clients = permit_mynetworks
sender_canonical_classes = envelope_sender
sender_canonical_maps = regexp:/etc/postfix/sender_maps
/^.*$/ xxx@xxx.ocn.ne.jp
これらはコンテナを再起動すると消えてしまうので、以下のようなマニフェストファイルを作っておき、Deployment
からコンテナにマウントしておく。
apiVersion: v1
kind: ConfigMap
metadata:
name: config
namespace: nginx
data:
sender-maps: |
/^.*$/ xxx@xxx.ocn.ne.jp
postfix-main.cf: |
local_header_rewrite_clients = permit_mynetworks
sender_canonical_classes = envelope_sender
sender_canonical_maps = regexp:/etc/postfix/sender-maps
OP25Bに引っかかる
OCNのリレーサーバを指定しているのに、何故か以下のエラーが出る。
Jul 2 06:58:31 ubuntu3 postfix/smtp[848]: connect to gmail-smtp-in.l.google.com[64.233.189.26]:25: Connection timed out
コンテナに入って、Postfixの設定を見てみる。
...
sender_dependent_relayhost_maps = texthash:/etc/postfix/relayhost_map
...
@sample.com [smtp.ocn.ne.jp]:587
どうやら環境変数RELAY_HOST
とRELAY_PORT
で指定したリレーサーバの情報が、relayhost_map
としてホスト毎のリレーサーバ情報として書き出されているようだ。
恐らくOP25Bに引っかかっていたのはrelayhost_map
のdbファイルが生成されていないことが原因だろう。
そもそも、ホストごとにリレーサーバの情報を設定する必要はなく、全てsmtp.ocn.ne.jp
にリレーしたい。
改めてmain.cf
を調べると、relayhost
という項目が空欄になっているのがわかった。
...
relayhost =
...
どうやらこの項目は、docker-mailserver
ではDEFAULT_RELAY_HOST
環境変数経由で設定できるらしい1。
そのため、以下のように環境変数を追加することで、きちんとリレーサーバを経由するようになった。
...
DEFAULT_RELAY_HOST: 'smtp.ocn.ne.jp:587'
...
色々試す
送信専用アドレスを作成
サービスからこのメールサーバを利用する上で、送信専用のメールアドレスがほしい。
先程のConfigMapにno-reply
宛の外部からのメールを捨てる設定を追加する。
apiVersion: v1
kind: ConfigMap
metadata:
name: config
namespace: nginx
data:
access: |
no-reply@sample.com DISCARD DEV-NULL
sender-maps: |
/^.*$/ xxx@xxx.ocn.ne.jp
postfix-main.cf: |
local_header_rewrite_clients = permit_mynetworks
sender_canonical_classes = envelope_sender
sender_canonical_maps = regexp:/etc/postfix/sender-maps
smtpd_recipient_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination, check_policy_service unix:private/policyd-spf, reject_unauth_pipelining, reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname, reject_unknown_recipient_domain, check_policy_service inet:localhost:65265, check_recipient_access hash:/etc/postfix/access
設定ファイルをマウント後、dbを作成するのも忘れずに
spec:
hostNetwork: true #
hostname: mail
containers:
- name: mailserver
image: ghcr.io/docker-mailserver/docker-mailserver:latest
imagePullPolicy: IfNotPresent
lifecycle:
postStart:
exec:
command: ["postmap", "/etc/postfix/access"]
Postfix で no-reply を実装する | 雑廉堂の雑記帳
misskeyのメールサーバ設定を行う
「コントロールパネル」→「メールサーバー」から、以下のように事前に作成したアカウントを入力すれば、ユーザ登録時やパスワード変更時にこのアドレスが利用されるようになる。
動作確認は「配信テスト」から可能。
grafanaからメールでアラート通知する
grafanaでメールを利用する場合、grafana.ini
にsmtpサーバの設定を行う必要がある。
helm経由でインストールしている場合2は、grafana.ini
を設定するためのパラメータが用意されているので、以下のように設定する。
...
grafana:
enabled: true
#namespaceOverride: ""
deploymentStrategy:
type: Recreate
grafana.ini:
smtp:
enabled: true
host: "{メールサーバのホスト}:465"
user: "grafana@sample.com"
password: "xxx"
from_address: "grafana@sample.com" # 必須
from_name: "Grafana"
persistence:
enabled: true
...
# values.yamlの再適用
helm upgrade -f values.yaml kube-prometheus-stack prometheus-community/kube-prometheus-stack
これでメールによるアラート通知を行えるようになった。
通知先は「Alerting」→「Contact points」から変更可能だ。「Test」で通知のテストも出来る。
チュートリアル - Grafana Eメール通知設定[ステップバイステップ]
余裕があったらやっておきたいこと
DKIM設定
建てたメールサーバからGmailのアドレス宛にメールを送信すると、メールアドレスの隣に「リレーサーバのドメイン」と「経由」という文字が表示されていた。
Gmailでは送信元のドメインとFromのアドレスのドメインが一致しない場合にこの表示がされる仕様らしい。 そしてこの挙動はDKIMで送信ドメイン認証を行うことで回避することができる。
因みにDKIMとは、「送信メールサーバ側でメールに電子署名を追加して送信し、受信メールサーバ側でメールを受信後、公開鍵をDNSサーバに問い合わせて電子署名の検証を行うことで、正しいメールサーバから配信されていることを確認する仕組み」のこと。
幸いdocker-mailserverでは簡単にこの設定ができる仕組みが用意されている。
DKIM, DMARC & SPF - Docker Mailserver
事前にメールアドレスを作成している前提で、以下のコマンドを実行する。
setup config dkim
すると、
/tmp/docker-mailserver/opendkim/keys/{ドメイン}
配下にmail.txt
が生成されるので、そのファイルに記載されている内容をDNSサーバのTXTレコードに追加する。
メールサーバを再起動すると、設定が反映されていることを確認できた。
fail2ban設定
運用を始めてからしばらく経つと、運用開始直後に比べて、メールの配信が遅れたり、明らかに動作が不安定になり始めた。
ログを見てみると、複数IPからログイン試行を行う某国からの不審なアクセスが目立ったので、以下の設定を参考にfail2ban設定を厳しくすることにした。
docker-mailserver/config-examples/fail2ban-jail.cf at master · docker-mailserver/docker-mailserver
apiVersion: v1
kind: ConfigMap
metadata:
name: config
namespace: nginx
data:
...
fail2ban-jail.cf: |
[DEFAULT]
# "bantime" is the number of seconds that a host is banned.
bantime = -1
# A host is banned if it has generated "maxretry" during the last "findtime"
# seconds.
findtime = 1w
# "maxretry" is the number of failures before a host get banned.
maxretry = 3
# "ignoreip" can be a list of IP addresses, CIDR masks or DNS hosts. Fail2ban
# will not ban a host which matches an address in this list. Several addresses
# can be defined using space (and/or comma) separator.
ignoreip = 127.0.0.1/8 4nm1tsu.com
# default ban action
# nftables-multiport: block IP only on affected port
# nftables-allports: block IP on all ports
banaction = nftables-allports
[dovecot]
enabled = true
[postfix]
enabled = true
# For a reference on why this mode was chose, see
# https://github.com/docker-mailserver/docker-mailserver/issues/3256#issuecomment-1511188760
mode = aggressive
[postfix-sasl]
enabled = true
# This jail is used for manual bans.
# To ban an IP address use: setup.sh fail2ban ban <IP>
[custom]
enabled = true
bantime = -1
port = smtp,pop3,pop3s,imap,imaps,submission,submissions,sieve
ポイントはpostfixのmodeをaggressive
にしているところ。これがextra
だと見逃してしまう通信があった。
そして攻撃元のIPが頻繁に変わるので、1週間で3回攻撃を観測したIPを永久BANという厳しめの設定にしている。
この設定のおかげもあってか、設定から1ヶ月ほどで合計360弱のIPをBANすることができ、当初の問題であるメールサーバの遅延もなくなった。
おわりに
おうちk8sクラスタに遊び道具が増えていい感じになった。
お願いだからOCNは自宅鯖主向けにも情報を公開してくれ。