還記得大約一年半前
筆者我曾經寫了一篇 Docker MailServer 架設與採坑紀錄
當時是直接在 Ubuntu 主機架設,並且持續使用到現在
然而,最近我們公司突然有業務需要自己自架 MailServer
正好,公司 GKE cluster 可以讓我玩
於是我就嘗試在 K8s 上架設 MailServer
過程中當然也是遇到一些坑
這篇文章就來分享一下在 K8s 上架設 MailServer 的經驗吧
或許是因為 Docker MailServer 的專案逐步成熟了
雖然或多或少還是有一些小麻煩
但整體來說比起我之前純用 Docker 架設時要順利的多
# 前提
老樣子,先來確認一下我們手上有的資源
默認各位都會設定 K8s,也默認都會設定 GCP(或各家雲服務設定)
當然你也可以選擇自己部 Cluster 不用雲服務
不過網路設定等的環境設定就得自己處理了
# 環境
- 系統: GKE (Google Kubernetes Engine)
- Kubernetes: v1.27+
- DNS: Cloudflare
- SSL: acme (Cert Manager)
# GCP 防火牆設定
如果你的 VPC 設定是對的,默認各種 Port 應該會是開的
不過保險還是列一下我們需要哪些 Port
以及其對應的協定
ports: | |
- "25:25" # SMTP | |
- "143:143" # IMAP | |
- "465:465" # ESMTP | |
- "587:587" # ESMTP | |
- "993:993" # IMAPS |
如果需要 POP3 的話也要開 110 和 995
# DNS
跟用 Docker 自架一樣,想指向成功同樣需要設定 DNS
一樣進到你的 Cloudflare Dashboard 或是其他 DNS 服務商的後台
不過這裡只是先提示,和之前不同
使用 GCP 的服務的話會需要等 K8s 的 LoadBalancer 建立好之後
才能拿到 IP 來設定 DNS,所以先跳過,等等再回來處理
# Docker MailServer in Kubernetes
同樣的,確定好手邊的資源以及確定要做什麼了
接下來就是開始架設 MailServer 了
我們這次同樣是使用的是現成的 Docker 專案 Docker MailServer
# 部屬 K8s deployment
Docker MailServer 官方其實已經有提供 K8s 的範例檔案了
不過這個文檔並不是官方維護的
並且對於 GKE 的兼容性也不算完美
所以我們還是要自己調整一下
保險起見我還是分享一下我的設定
apiVersion: apps/v1 | |
kind: Deployment | |
metadata: | |
name: mailserver | |
namespace: mail | |
labels: | |
app: mailserver | |
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 privileged ports" | |
ignore-check.kube-linter.io/no-read-only-root-fs: "Mailserver requires a writable root filesystem" | |
spec: | |
replicas: 1 | |
selector: | |
matchLabels: | |
app: mailserver | |
template: | |
metadata: | |
labels: | |
app: mailserver | |
annotations: | |
container.apparmor.security.beta.kubernetes.io/mailserver: runtime/default | |
spec: | |
hostname: mail | |
containers: | |
- name: mailserver | |
image: ghcr.io/docker-mailserver/docker-mailserver:latest | |
imagePullPolicy: IfNotPresent | |
securityContext: | |
allowPrivilegeEscalation: true | |
readOnlyRootFilesystem: false | |
runAsUser: 0 | |
runAsGroup: 0 | |
runAsNonRoot: false | |
privileged: false | |
capabilities: | |
add: | |
- CHOWN | |
- FOWNER | |
- MKNOD | |
- SETGID | |
- SETUID | |
- DAC_OVERRIDE | |
- NET_ADMIN | |
- NET_RAW | |
- NET_BIND_SERVICE | |
- SYS_CHROOT | |
- KILL | |
drop: | |
- ALL | |
seccompProfile: | |
type: RuntimeDefault | |
resources: | |
limits: | |
memory: 1.5Gi | |
cpu: 1000m | |
requests: | |
memory: 500Mi | |
cpu: 250m | |
envFrom: | |
- configMapRef: | |
name: mailserver-environment | |
volumeMounts: | |
- name: mail-data | |
mountPath: /var/mail | |
- name: mail-state | |
mountPath: /var/mail-state | |
- name: mail-config | |
mountPath: /tmp/docker-mailserver | |
- name: letsencrypt | |
mountPath: /etc/letsencrypt | |
readOnly: true | |
ports: | |
- name: smtp | |
containerPort: 25 | |
protocol: TCP | |
- name: submissions | |
containerPort: 465 | |
protocol: TCP | |
- name: submission | |
containerPort: 587 | |
protocol: TCP | |
- name: imap | |
containerPort: 143 | |
protocol: TCP | |
- name: imaps | |
containerPort: 993 | |
protocol: TCP | |
volumes: | |
- name: mail-data | |
persistentVolumeClaim: | |
claimName: mailserver-data | |
- name: mail-state | |
persistentVolumeClaim: | |
claimName: mailserver-state | |
- name: mail-config | |
persistentVolumeClaim: | |
claimName: mailserver-config | |
- name: letsencrypt | |
secret: | |
secretName: mailserver-tls | |
items: | |
- key: tls.crt | |
path: tls.crt | |
- key: tls.key | |
path: tls.key |
apiVersion: v1 | |
kind: Service | |
metadata: | |
name: mailserver | |
namespace: mail | |
labels: | |
app: mailserver | |
spec: | |
externalTrafficPolicy: Local | |
type: LoadBalancer # 跟 GCP LB 要外部 IP,如果你要改 Ingress 固定也行 | |
selector: | |
app: mailserver | |
ports: | |
- name: smtp | |
port: 25 | |
targetPort: smtp | |
protocol: TCP | |
- name: submissions | |
port: 465 | |
targetPort: submissions | |
protocol: TCP | |
- name: submission | |
port: 587 | |
targetPort: submission | |
protocol: TCP | |
- name: imaps | |
port: 993 | |
targetPort: imaps | |
protocol: TCP |
apiVersion: v1 | |
kind: PersistentVolumeClaim | |
metadata: | |
name: mailserver-data | |
namespace: mail | |
spec: | |
storageClassName: standard-rwo | |
accessModes: | |
- ReadWriteOnce | |
resources: | |
requests: | |
storage: 25Gi | |
--- | |
apiVersion: v1 | |
kind: PersistentVolumeClaim | |
metadata: | |
name: mailserver-state | |
namespace: mail | |
spec: | |
storageClassName: standard-rwo | |
accessModes: | |
- ReadWriteOnce | |
resources: | |
requests: | |
storage: 5Gi | |
--- | |
apiVersion: v1 | |
kind: PersistentVolumeClaim | |
metadata: | |
name: mailserver-config | |
namespace: mail | |
spec: | |
storageClassName: standard-rwo | |
accessModes: | |
- ReadWriteOnce | |
resources: | |
requests: | |
storage: 1Gi |
apiVersion: cert-manager.io/v1 | |
kind: Certificate | |
metadata: | |
name: mailserver-cert | |
namespace: mail | |
spec: | |
secretName: mailserver-tls | |
isCA: false | |
privateKey: | |
algorithm: RSA | |
encoding: PKCS1 | |
size: 2048 | |
dnsNames: | |
- example.com # 改成你的網域 | |
- mail.example.com # 改成你的網域 | |
issuerRef: | |
kind: ClusterIssuer | |
name: letsencrypt-prod |
apiVersion: kustomize.config.k8s.io/v1beta1 | |
kind: Kustomization | |
namespace: mail | |
resources: | |
- deployment.yaml | |
- service.yaml | |
- persistentVolumn.yaml | |
- certificate.yaml | |
configMapGenerator: | |
- name: mailserver-environment | |
envs: | |
- config.env | |
generatorOptions: | |
disableNameSuffixHash: true |
基本上是抄官方的 Mailserver.env
完成後,就可以使用 kubectl apply -k . 來部屬你的 K8s 了
# 拿到 IP
這時候你如果去看你的 Pod
應該會發現他卡著 Pending 起不來
這是因為他沒有辦法順利的去進行 Issuer 的驗證
所以會一直卡在 Waiting for certificate to be issued 的狀態
緊接著,很重要的一步
我們要去拿到 LoadBalancer 的 IP
趁 Pod 不注意,輸入 kubectl -n mail get svc -o wide 可以抓到 External-IP
拿到 IP 之後,填入 DNS 的 A 紀錄裡面
接著等 DNS 生效,Cert Manager 就會順利的幫我們取得 SSL 證書
# 創建帳號 && DKIM
和直接使用 Docker 開啟一樣
使用 K8s 架設同樣是需要 exec 進去容器內創建帳號的
不然你的 Postfix 可能會哭給你看然後噴錯不斷重啟
kubectl exec -it <mail-pod-name> -- bash | |
setup email add |
# 連線設定
# DNS 驗證設定
- A 紀錄:這個應該大家都會設定,否則你的網址根本連不到伺服器
- [mail.]example.com -> 你的伺服器 IP
- MX 紀錄:指定信件要導去哪裡
- example.com -> [mail.]example.com
- PTR 紀錄:做 rDNS 反向解析用的,可以從 IP 反向解析到 domain,這個是為了避免被標記為垃圾郵件
- 你的伺服器 IP -> [mail.]example.com
這邊的 example.com 請自行替換成你自己的網域,當然有引號的部分也可以選用 subdomain
例如 mail.example.com,只是筆者想要用主網域因此沒有選用
❯ dig +short TXT miyago9267.com | |
"google-site-verification=eWCbTPIIpTueaDXP5h1AjIB83xZTKmMAfgsSL9IgGs8" | |
"v=spf1 a mx ip4:我的IP -all" | |
❯ dig +short MX miyago9267.com | |
10 miyago9267.com. | |
❯ dig +short A miyago9267.com | |
我的IP |
# DKIM
DKIM 是一種防止郵件被偽造的驗證技術
可以將信件的標頭和內文都進行加密以及生成數位簽章
我們需要在 dns 設定裡面加入這個
否則會無法驗證信件導致無法收發信
而 docker mailserver 裡有預設提供 opendkim 來進行金鑰的生成
kubectl exec -it <mail-pod-name> -- bash | |
setup config dkim |
我們的 K8s 有設定 PVC,因此可以直接讀取靜態檔來使用
生成完之後會在 /tmp/docker-mailserver/opendkim/keys/你的網域/ 裡面
複製並組合起來,貼到你的 DNS TXT 紀錄裡面
# SPF
SPF 是一種驗證發信人身份的技術
一開始我們前置作業所設定的 TXT 就是這個
TXT紀錄: example.com -> v=spf1 ip4:你的External-IP -all | |
# dig 結果如下 | |
❯ dig +short TXT example.com | |
"v=spf1 a mx ip4:你的External-IP -all" |
# DMARC
DMARC 也是一種郵件驗證技術
主要是 SPF 和 DKIM 的補充
並且可以指定如何處理未經驗證或驗證失敗的信件
這裡有一個現成的 DMARC 產生器可以用
❯ dig +short TXT _dmarc.example.com | |
"v=DMARC1; p=none; rua=mailto:你的備援信箱地址; ruf=mailto:你的備援信箱地址; sp=none; ri=86400" |
rua 和 ruf 要調整好,不然出錯的時候很難 debug
# 容器文件設定
跟直接用舊版的 Docker MailServer 相比
使用新版,並且掛在 K8s 底下後基本上他都幫你調好了
我有調整的只有這個,為了讓 Postfix 不要把我們的網址
當成本機 mydestination 的 domain/hostname 來做 local recipient 檢查
講的有點抽象
道理可以想像成下面這樣
Postfix 有兩種判斷一封信要怎麼處理的方式
- local domain:信件的收件人 domain 是本機的 user
- virtual domain:信件的收件人 domain 是虛擬 domain
然而,前者的優先級高於後者
如果我們的 mydestination 裡面有包含我們的 domain
那麼 Postfix 就會認為這個 domain 是 local user
然而現在我們同名的 user 並沒有在本機裡面存在
就會導致信件無法送達,並且報錯
而要解決這個問題很簡單
實際上就是把 mydestination 改成只包含 localhost.$mydomain, localhost
只需要一段去 patch 它,讓他只留下這兩個就可以了
當然你也可以用 $myhostname 也可以啦,只是就像上面講的
要自己去創帳號調整了,就比較難無限亂開帳號了
❯ kubectl -n mail exec -it "$POD" -- sh -lc ' | |
cat > /tmp/docker-mailserver/postfix-main.cf <<EOF | |
mydestination = localhost.\$mydomain, localhost | |
EOF | |
' | |
❯ kubectl -n mail rollout restart deploy/mailserver |
# 測試
最後就是測試我們的功能有沒有成功了
# swaks
可以使用 swaks 這個工具來發信測試能不能收
swaks \ | |
--to user@example.com \ | |
--from test@example.com \ | |
--server mail.example.com \ | |
--header "Subject: swaks test" \ | |
--body "Hello, this is a test mail." |
# openssl
可以使用 openssl 來測試加密的連線是否正常
以前說我沒什麼研究
現在... 還是沒什麼研究
不過比較知道他是評斷憑證握手是否正常的東西了
openssl s_client -connect localhost:995 -crlf | |
openssl s_client -connect localhost:587 -crlf |
基本上 SMTP 跟 IMAP 的連線都可以通了
就代表我們的 MailServer 應該是成功架設完成啦
# 結語
入坑了 K8s 之後,發現這東西是真好管理
(撇除掉很難自架以外,但我用 GKE 所以ㄏㄏ)
加上 Docker MailServer 的官方修了以前我需要額外設定的不少 bug
而且我有去年架過 MailServer 的經驗加持
所以這次的過程來說整體還算順利
沒有像去年一樣卡了好幾天這樣,網域一下來馬上就搞好了
希望這篇可以給有需要的人一些參考囉
