docker-mailserver/docs/content/advanced/kubernetes.md
polarathene 021e942c4c docs(refactor): Large refactor + additions + fixes
Consistency pass, formatting cleanup and fixes, introduce admonitions, add front-matter.

---

docs: Add front-matter

---

docs: Fix and format links

- Some links were invalid (eg files moved or renamed)
- Some were valid but had invalid section headers (content removed or migrated)
- Some use `http://` instead of `https://` when the website supports a secure connection.
- Some already used the `[name][reference]` convention but often with a number that wasn't as useful for maintenance.
- All referenced docs needed URLs replaced. Opted for the `[name][reference]` approach to group them all clearly at the bottom of the doc, especially with the relative URLs and in some cases many duplicate entries.
- All `tomav` references from the original repo prior to switch to an organization have been corrected.
- Minor cosmetic changes to the `name` part of the URL, such as for referencing issues to be consistent.
- Some small changes to text body, usually due to duplicate URL reference that was unnecessary (open relay, youtous)
- Switched other links to use the `[name][reference]` format when there was a large group of URLs such as wikipedia or kubernetes. Github repos that reference projects related to `docker-mailserver` also got placed here so they're noticed better by maintainers. This also helped quite a bit with `mermaid` external links that are very long.
- There was a Github Wiki supported syntax in use `[[name | link]]` for `fetchmail` page that isn't compatible by default with MkDocs (needs a plugin), converted to `[name][reference]` instead since it's a relative link.

---

docs: Update commit link for LDAP override script

Logic moved to another file, keeping the permalink commit reference so it's unaffected by any changes in the file referenced in future.

---

docs: Heading corrections

Consistency pass. Helps with the Table of Contents (top-right UI) aka Document Outline.
docs: codefence cleanup

---

docs: misc cleanup

---

docs: Add Admonitions

Switches `<details>` usage for collapsible admonitions (`???`) while other text content is switched to the visually more distinct admoniton (`!!!` or `???+`) style.

This does affect editor syntax highlighting a bit and markdown linting as it's custom non-standard markdown syntax.
2021-03-25 11:49:24 +13:00

14 KiB

title
Kubernetes

Deployment Example

There is nothing much in deploying mailserver to Kubernetes itself. The things are pretty same as in docker-compose.yml, but with Kubernetes syntax.

apiVersion: v1
kind: Namespace
metadata:
  name: mailserver
---
kind: ConfigMap
apiVersion: v1
metadata:
  name: mailserver.env.config
  namespace: mailserver
  labels:
    app: mailserver
data:
  OVERRIDE_HOSTNAME: example.com
  ENABLE_FETCHMAIL: "0"
  FETCHMAIL_POLL: "120"
  ENABLE_SPAMASSASSIN: "0"
  ENABLE_CLAMAV: "0"
  ENABLE_FAIL2BAN: "0"
  ENABLE_POSTGREY: "0"
  ONE_DIR: "1"
  DMS_DEBUG: "0"

---
kind: ConfigMap
apiVersion: v1
metadata:
  name: mailserver.config
  namespace: mailserver
  labels:
    app: mailserver
data:
  postfix-accounts.cf: |
    user1@example.com|{SHA512-CRYPT}$6$2YpW1nYtPBs2yLYS$z.5PGH1OEzsHHNhl3gJrc3D.YMZkvKw/vp.r5WIiwya6z7P/CQ9GDEJDr2G2V0cAfjDFeAQPUoopsuWPXLk3u1    

  postfix-virtual.cf: |
    alias1@example.com user1@dexample.com    

  #dovecot.cf: |
  #  service stats {
  #    unix_listener stats-reader {
  #      group = docker
  #      mode = 0666
  #    }
  #    unix_listener stats-writer {
  #      group = docker
  #      mode = 0666
  #    }
  #  }

  SigningTable: |
    *@example.com mail._domainkey.example.com    

  KeyTable: |
    mail._domainkey.example.com example.com:mail:/etc/opendkim/keys/example.com-mail.key    

  TrustedHosts: |
    127.0.0.1
    localhost    

  #user-patches.sh: |
  #  #!/bin/bash

  #fetchmail.cf: |

---
kind: Secret
apiVersion: v1
metadata:
  name: mailserver.opendkim.keys
  namespace: mailserver
  labels:
    app: mailserver
type: Opaque
data:
  example.com-mail.key: 'base64-encoded-DKIM-key'

---
kind: Service
apiVersion: v1
metadata:
  name: mailserver
  namespace: mailserver
  labels:
    app: mailserver
spec:
  selector:
    app: mailserver
  ports:
    - name: smtp
      port: 25
      targetPort: smtp
    - name: smtp-secure
      port: 465
      targetPort: smtp-secure
    - name: smtp-auth
      port: 587
      targetPort: smtp-auth
    - name: imap
      port: 143
      targetPort: imap
    - name: imap-secure
      port: 993
      targetPort: imap-secure
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mailserver
  namespace: mailserver
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mailserver
  template:
    metadata:
      labels:
        app: mailserver
        role: mail
        tier: backend
    spec:
      #nodeSelector:
      #  kubernetes.io/hostname: local.k8s
      #initContainers:
      #- name: init-myservice
      #  image: busybox
      #  command: ["/bin/sh", "-c", "cp /tmp/user-patches.sh /tmp/files"]
      #  volumeMounts:
      #    - name: config
      #      subPath: user-patches.sh
      #      mountPath: /tmp/user-patches.sh
      #      readOnly: true
      #    - name: tmp-files
      #      mountPath: /tmp/files
      containers:
      - name: docker-mailserver
        image: tvial/docker-mailserver:latest
        imagePullPolicy: Always
        volumeMounts:
          - name: config
            subPath: postfix-accounts.cf
            mountPath: /tmp/docker-mailserver/postfix-accounts.cf
            readOnly: true
          #- name: config
          #  subPath: postfix-main.cf
          #  mountPath: /tmp/docker-mailserver/postfix-main.cf
          #  readOnly: true
          - name: config
            subPath: postfix-virtual.cf
            mountPath: /tmp/docker-mailserver/postfix-virtual.cf
            readOnly: true
          - name: config
            subPath: fetchmail.cf
            mountPath: /tmp/docker-mailserver/fetchmail.cf
            readOnly: true
          - name: config
            subPath: dovecot.cf
            mountPath: /tmp/docker-mailserver/dovecot.cf
            readOnly: true
          #- name: config
          #  subPath: user1.example.com.dovecot.sieve
          #  mountPath: /tmp/docker-mailserver/user1@example.com.dovecot.sieve
          #  readOnly: true
          #- name: tmp-files
          #  subPath: user-patches.sh
          #  mountPath: /tmp/docker-mailserver/user-patches.sh
          - name: config
            subPath: SigningTable
            mountPath: /tmp/docker-mailserver/opendkim/SigningTable
            readOnly: true
          - name: config
            subPath: KeyTable
            mountPath: /tmp/docker-mailserver/opendkim/KeyTable
            readOnly: true
          - name: config
            subPath: TrustedHosts
            mountPath: /tmp/docker-mailserver/opendkim/TrustedHosts
            readOnly: true
          - name: opendkim-keys
            mountPath: /tmp/docker-mailserver/opendkim/keys
            readOnly: true
          - name: data
            mountPath: /var/mail
            subPath: data
          - name: data
            mountPath: /var/mail-state
            subPath: state
          - name: data
            mountPath: /var/log/mail
            subPath: log
        ports:
          - name: smtp
            containerPort: 25
            protocol: TCP
          - name: smtp-secure
            containerPort: 465
            protocol: TCP
          - name: smtp-auth
            containerPort: 587
          - name: imap
            containerPort: 143
            protocol: TCP
          - name: imap-secure
            containerPort: 993
            protocol: TCP
        envFrom:
          - configMapRef:
              name: mailserver.env.config
      volumes:
        - name: config
          configMap:
            name: mailserver.config
        - name: opendkim-keys
          secret:
            secretName: mailserver.opendkim.keys
        - name: data
          persistentVolumeClaim:
            claimName: mail-storage
        - name: tmp-files
          emptyDir: {}

!!! note Any sensitive data (keys, etc) should be deployed via Secrets. Other configuration just fits well into ConfigMaps.

!!! note Make sure that Pod is assigned to specific Node in case you're using volume for data directly with hostPath. Otherwise Pod can be rescheduled on a different Node and previous data won't be found. Except the case when you're using some shared filesystem on your Nodes.

Exposing to the Outside World

The hard part with Kubernetes is to expose deployed mailserver to outside world. Kubernetes provides multiple ways for doing that. Each has its downsides and complexity.

The major problem with exposing mailserver to outside world in Kubernetes is to preserve real client IP. Real client IP is required by mailserver for performing IP-based SPF checks and spam checks.

Preserving real client IP is relatively non-trivial in Kubernetes and most exposing ways do not provide it. So, it's up to you to decide which exposing way suits better your needs in a price of complexity.

If you do not require SPF checks for incoming mails you may disable them in Postfix configuration by dropping following line (which removes check_policy_service unix:private/policyd-spf option):

kind: ConfigMap
apiVersion: v1
metadata:
  name: mailserver.config
  labels:
    app: mailserver
data:
  postfix-main.cf: |
    smtpd_recipient_restrictions = permit_sasl_authenticated, permit_mynetworks, reject_unauth_destination, reject_unauth_pipelining, reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname, reject_unknown_recipient_domain, reject_rbl_client zen.spamhaus.org, reject_rbl_client bl.spamcop.net    
# ...

---

kind: Deployment
apiVersion: extensions/v1beta1
metadata:
  name: mailserver
# ...
    volumeMounts:
      - name: config
        subPath: postfix-main.cf
        mountPath: /tmp/docker-mailserver/postfix-main.cf
        readOnly: true

External IPs Service

The simplest way is to expose mailserver as a Service with external IPs.

kind: Service
apiVersion: v1
metadata:
  name: mailserver
  labels:
    app: mailserver
spec:
  selector:
    app: mailserver
  ports:
    - name: smtp
      port: 25
      targetPort: smtp
# ...
  externalIPs:
    - 80.11.12.10

Downsides

  • Real client IP is not preserved, so SPF check of incoming mail will fail.

  • Requirement to specify exposed IPs explicitly.

Proxy port to Service

The Proxy Pod helps to avoid necessity of specifying external IPs explicitly. This comes in price of complexity: you must deploy Proxy Pod on each Node you want to expose mailserver on.

Downsides

  • Real client IP is not preserved, so SPF check of incoming mail will fail.

Bind to concrete Node and use host network

The simplest way to preserve real client IP is to use hostPort and hostNetwork: true in the mailserver Pod. This comes in price of availability: you can talk to mailserver from outside world only via IPs of Node where mailserver is deployed.

kind: Deployment
apiVersion: extensions/v1beta1
metadata:
  name: mailserver
# ...
    spec:
      hostNetwork: true
# ...
      containers:
# ...
          ports:
            - name: smtp
              containerPort: 25
              hostPort: 25
            - name: smtp-auth
              containerPort: 587
              hostPort: 587
            - name: imap-secure
              containerPort: 993
              hostPort: 993
# ...

Downsides

  • Not possible to access mailserver via other cluster Nodes, only via the one mailserver deployed at.
  • Every Port within the Container is exposed on the Host side, regardless of what the ports section in the Configuration defines.

Proxy Port to Service via PROXY Protocol

This way is ideologically the same as using Proxy Pod, but instead of a separate proxy pod, you configure your ingress to proxy TCP traffic to the mailserver pod using the PROXY protocol, which preserves the real client IP.

Configure your Ingress

With an NGINX ingress controller, set externalTrafficPolicy: Local for its service, and add the following to the TCP services config map (as described here):

25:  "mailserver/mailserver:25::PROXY"
465: "mailserver/mailserver:465::PROXY"
587: "mailserver/mailserver:587::PROXY"
993: "mailserver/mailserver:993::PROXY"

With HAProxy, the configuration should look similar to the above. If you know what it actually looks like, add an example here. 😃

Configure the Mailserver

Then, configure both Postfix and Dovecot to expect the PROXY protocol:

kind: ConfigMap
apiVersion: v1
metadata:
  name: mailserver.config
  labels:
    app: mailserver
data:
  postfix-main.cf: |
    postscreen_upstream_proxy_protocol = haproxy    
  postfix-master.cf: |
    submission/inet/smtpd_upstream_proxy_protocol=haproxy
    smtps/inet/smtpd_upstream_proxy_protocol=haproxy    
  dovecot.cf: |
    # Assuming your ingress controller is bound to 10.0.0.0/8
    haproxy_trusted_networks = 10.0.0.0/8, 127.0.0.0/8
    service imap-login {
      inet_listener imaps {
        haproxy = yes
      }
    }    
# ...
---

kind: Deployment
apiVersion: extensions/v1beta1
metadata:
  name: mailserver
spec:
  template:
    spec:
      containers:
        - name: docker-mailserver
          volumeMounts:
            - name: config
              subPath: postfix-main.cf
              mountPath: /tmp/docker-mailserver/postfix-main.cf
              readOnly: true
            - name: config
              subPath: postfix-master.cf
              mountPath: /tmp/docker-mailserver/postfix-master.cf
              readOnly: true
            - name: config
              subPath: dovecot.cf
              mountPath: /tmp/docker-mailserver/dovecot.cf
              readOnly: true

Downsides

  • Not possible to access mailserver via inner cluster Kubernetes DNS, as PROXY protocol is required for incoming connections.

Let's Encrypt Certificates

Kube-Lego may be used for a role of Let's Encrypt client. It works with Kubernetes Ingress Resources and automatically issues/manages certificates/keys for exposed services via Ingresses.

kind: Ingress
apiVersion: extensions/v1beta1
metadata:
  name: mailserver
  labels:
    app: mailserver
  annotations:
    kubernetes.io/tls-acme: 'true'
spec:
  rules:
    - host: example.com
      http:
        paths:
          - path: /
            backend:
              serviceName: default-backend
              servicePort: 80
  tls:
    - secretName: mailserver.tls
      hosts:
        - example.com

Now, you can use Let's Encrypt cert and key from mailserver.tls Secret in your Pod spec:

# ...
env:
  - name: SSL_TYPE
    value: 'manual'
  - name: SSL_CERT_PATH
    value: '/etc/ssl/mailserver/tls.crt'
  - name: SSL_KEY_PATH
    value: '/etc/ssl/mailserver/tls.key'
# ...
volumeMounts:
  - name: tls
    mountPath: /etc/ssl/mailserver
    readOnly: true
# ...
volumes:
  - name: tls
    secret:
      secretName: mailserver.tls