Running a webserver in Kubernetes is easy, but a mailserver is more challenging. Most of the challenging things has todo with your ip infrastructure, ingress and loadbalancer within Kubernetes. Here are things I learned when I was deploying a mailserver based on Postfix on Kubernetes with metallb and nginx ingress. If you have a different setup, things could apply or not.

Prevent mail loops on your secondary MX

Your postfix MX pod is running maybe on a private ip RFC1918, this is fine. But Postfix needs to know it’s public ip. This is to prevent mail looping if yor primary MX is down. You need to define your public incoming ip in the main.cf so if you use loadbalancing you need to define the public ip for the LoadBalancer that would be the same IP as defined in your DNS MX record, not your outgoing ip.

proxy_interfaces = 178.242.244.145

Ingress Proxy and external IP

If you want to run your mailserver behind for example a nginx or haproxy ingress proxy, because you don’t have enough public ip’s or want to use one ip for multiple services. Postfix wont know the Public IP of the incoming client connection. And that could be a problem for things like RBL lookups.

To let Postfix know what the external public ip is from the connecting client. We need to use the externalTrafficPolicy for the ingress service that is behind the LoadBalancer. If this is possible also depends on the type of LoadBalancer you use. I am using metallb and that is working.

externalTrafficPolicy: Local

Now the ingress proxy knows the external ip of the connecting client. But this ingress proxy needs to pass this information down the the Postfix Pod. For this we can use in nginx of haproxy ingress the tcp-services configmap. This is brcuase ingress normaly does only http and not TCP.

apiVersion: v1
kind: ConfigMap
metadata:
  name: tcp-services
  namespace: ingress-nginx
data:
  "25": "mail/mail-postfix:25::PROXY"

And in postfix we need to tell that connection from the ingress proxy is using the PROXY protocol. haproxy protocol definition here also works with nginx, haproxy is just a standard.

postscreen_upstream_proxy_protocol=haproxy

helo/ehlo Banners

Your incoming and outgoing mails doesn’t need to be to or from same ip for the same pod, maybe ingress is via a LoadBalancer and ingress proxy, and outgoing will be from the node where the pod is running. So the A and PTR DNS records are different. Mail system can be strict for the correct usage with A and PTR records and whats is defined in the banners. So we need to define those in the main.cf from Postfix

smtpd_banner = {{ LOADBALANCER_INGRESS_NAME }} ESMTP $mail_name
smtp_helo_name = {{ NODE_NAME }}

You can hardcode the names, better is to put in in the environment variable from the pod. So now when the pod starts, it get the hostname of the node (needs to be a FQDN). And this varible you can apply in your mail config. And if your DNS records are correctly setup the helo for outgoing connections should contain the hostname of the node running the pod.

        env:
          - name: NODE_NAME
            valueFrom:
              fieldRef:
                fieldPath: spec.nodeName

RBL List

For fighting spam maybe you use RBL lists. The problem is that DNS servers will be limited in the amount of requests that they can do. So when you use your provider DNS and other people will also use this DNS server you will hit a max pretty easy. Better is to use your own resolver for this.

Note: If you are running a commercial server you should be paying or donating to use this lists in general.

One way is to install an Unbound resolver on your nodes (the nodes where CoreDNS is running) and point the config of CoreDNS in Kubernetes for the RBL lists to this Unbound resolver. Now the request for the RBL lists domains will go trough Unbound on the node.

apiVersion: v1
kind: ConfigMap
metadata:
  name: coredns
  namespace: kube-system
data:
  Corefile: |-
    .:53 {
        errors
        health {
            lameduck 5s
        }
        ready
        kubernetes cluster.local in-addr.arpa ip6.arpa {
            pods insecure
            fallthrough in-addr.arpa ip6.arpa
            ttl 30
        }
        prometheus :9153
        forward . /etc/resolv.conf
        cache 30
        loop
        reload
        loadbalance
    }
    zen.spamhaus.org:53 {
       errors
       cache 30
       forward . 172.17.0.1:5300
    }
    multi.uribl.com:53 {
       errors
       cache 30
       forward . 172.17.0.1:5300
    }
    dnswl.org:53 {
       errors
       cache 30
       forward . 172.17.0.1:5300
    }