還記得大約一年半前
筆者我曾經寫了一篇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紀錄:這個應該大家都會設定,否則你的網址根本連不到伺服器
  • MX紀錄:指定信件要導去哪裡
  • PTR紀錄:做rDNS反向解析用的,可以從IP反向解析到domain,這個是為了避免被標記為垃圾郵件

這邊的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有兩種判斷一封信要怎麼處理的方式

  1. local domain:信件的收件人domain是本機的user
  2. 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的經驗加持
所以這次的過程來說整體還算順利
沒有像去年一樣卡了好幾天這樣,網域一下來馬上就搞好了
希望這篇可以給有需要的人一些參考囉

總瀏覽次數:載入中...更新於

請我喝[茶]~( ̄▽ ̄)~*

Miyago9267 微信支付

微信支付

Miyago9267 支付寶

支付寶

Miyago9267 buymeacoffee

buymeacoffee