還記得大約一年半前
筆者我曾經寫了一篇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的經驗加持
所以這次的過程來說整體還算順利
沒有像去年一樣卡了好幾天這樣,網域一下來馬上就搞好了
希望這篇可以給有需要的人一些參考囉
