From b09cd1628d7d4f20099ad19f436ff3ba0ed35021 Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Sun, 24 May 2026 00:43:56 +1000 Subject: [PATCH] feat(postfix): deploy postfix MTA and rspamd spam filter - mailgateway namespace with Deployment + HPA (2-6 replicas) - rspamd Deployment + HPA (2-6 replicas) with milter interface - postfix configured to relay inbound mail to stalwart via transport maps - rspamd milter on port 11332 for spam scanning and DKIM signing - DKIM keys stored in Vault at kubernetes/namespace/mailgateway/default/dkim-keys - TLS cert via cert-manager (vault-issuer) for mail.main.unkin.net - rspamd web UI exposed via Traefik Gateway at rspamd.k8s.syd1.au.unkin.net - postfix external LoadBalancer service for inbound MX on port 25 - Add full main.cf and master.cf as ConfigMap resources mounted via subPath - main.cf: relay-only gateway config, texthash: transport maps, rspamd milter - master.cf: standard smtp + submission (587, TLS required) + internal processes - MAILNAME/MY_NETWORKS/MY_DESTINATION env vars kept in sync with main.cf - LOG_TO_STDOUT=1 for k8s log collection --- apps/base/mailgateway/certificate.yaml | 18 ++++ apps/base/mailgateway/gateway.yaml | 37 ++++++++ apps/base/mailgateway/httproute.yaml | 16 ++++ apps/base/mailgateway/kustomization.yaml | 32 +++++++ apps/base/mailgateway/namespace.yaml | 5 ++ apps/base/mailgateway/postfix-deployment.yaml | 85 +++++++++++++++++++ apps/base/mailgateway/postfix-hpa.yaml | 38 +++++++++ .../mailgateway/resources/postfix/main.cf | 47 ++++++++++ .../mailgateway/resources/postfix/master.cf | 42 +++++++++ .../mailgateway/resources/postfix/transport | 2 + .../rspamd/local.d/dkim_signing.conf | 13 +++ .../rspamd/local.d/milter_headers.conf | 2 + .../resources/rspamd/local.d/worker-proxy.inc | 7 ++ apps/base/mailgateway/rspamd-deployment.yaml | 75 ++++++++++++++++ apps/base/mailgateway/rspamd-hpa.yaml | 38 +++++++++ apps/base/mailgateway/services.yaml | 61 +++++++++++++ apps/base/mailgateway/vaultauth.yaml | 18 ++++ apps/base/mailgateway/vaultstaticsecret.yaml | 17 ++++ .../au-syd1/mailgateway/kustomization.yaml | 6 ++ argocd/applicationsets/platform.yaml | 1 + 20 files changed, 560 insertions(+) create mode 100644 apps/base/mailgateway/certificate.yaml create mode 100644 apps/base/mailgateway/gateway.yaml create mode 100644 apps/base/mailgateway/httproute.yaml create mode 100644 apps/base/mailgateway/kustomization.yaml create mode 100644 apps/base/mailgateway/namespace.yaml create mode 100644 apps/base/mailgateway/postfix-deployment.yaml create mode 100644 apps/base/mailgateway/postfix-hpa.yaml create mode 100644 apps/base/mailgateway/resources/postfix/main.cf create mode 100644 apps/base/mailgateway/resources/postfix/master.cf create mode 100644 apps/base/mailgateway/resources/postfix/transport create mode 100644 apps/base/mailgateway/resources/rspamd/local.d/dkim_signing.conf create mode 100644 apps/base/mailgateway/resources/rspamd/local.d/milter_headers.conf create mode 100644 apps/base/mailgateway/resources/rspamd/local.d/worker-proxy.inc create mode 100644 apps/base/mailgateway/rspamd-deployment.yaml create mode 100644 apps/base/mailgateway/rspamd-hpa.yaml create mode 100644 apps/base/mailgateway/services.yaml create mode 100644 apps/base/mailgateway/vaultauth.yaml create mode 100644 apps/base/mailgateway/vaultstaticsecret.yaml create mode 100644 apps/overlays/au-syd1/mailgateway/kustomization.yaml diff --git a/apps/base/mailgateway/certificate.yaml b/apps/base/mailgateway/certificate.yaml new file mode 100644 index 0000000..88d2b7b --- /dev/null +++ b/apps/base/mailgateway/certificate.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: postfix-smtp-tls + namespace: mailgateway +spec: + secretName: postfix-smtp-tls + issuerRef: + name: vault-issuer + kind: ClusterIssuer + commonName: mail.main.unkin.net + dnsNames: + - mail.main.unkin.net + - smtp-in.main.unkin.net + privateKey: + size: 4096 + algorithm: RSA diff --git a/apps/base/mailgateway/gateway.yaml b/apps/base/mailgateway/gateway.yaml new file mode 100644 index 0000000..7ef9a4f --- /dev/null +++ b/apps/base/mailgateway/gateway.yaml @@ -0,0 +1,37 @@ +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + labels: + traefik.io/instance: internal + annotations: + cert-manager.io/cluster-issuer: vault-issuer + cert-manager.io/common-name: rspamd.k8s.syd1.au.unkin.net + cert-manager.io/private-key-size: "4096" + external-dns.alpha.kubernetes.io/hostname: rspamd.k8s.syd1.au.unkin.net + external-dns.alpha.kubernetes.io/target: 198.18.200.4 + name: rspamd + namespace: mailgateway +spec: + gatewayClassName: traefik-internal + listeners: + - allowedRoutes: + namespaces: + from: Same + hostname: rspamd.k8s.syd1.au.unkin.net + name: http + port: 80 + protocol: HTTP + - allowedRoutes: + namespaces: + from: Same + hostname: rspamd.k8s.syd1.au.unkin.net + name: https + port: 443 + protocol: HTTPS + tls: + certificateRefs: + - group: "" + kind: Secret + name: rspamd-tls + mode: Terminate diff --git a/apps/base/mailgateway/httproute.yaml b/apps/base/mailgateway/httproute.yaml new file mode 100644 index 0000000..26d024b --- /dev/null +++ b/apps/base/mailgateway/httproute.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: rspamd + namespace: mailgateway +spec: + parentRefs: + - name: rspamd + namespace: mailgateway + hostnames: + - rspamd.k8s.syd1.au.unkin.net + rules: + - backendRefs: + - name: rspamd + port: 11334 diff --git a/apps/base/mailgateway/kustomization.yaml b/apps/base/mailgateway/kustomization.yaml new file mode 100644 index 0000000..0c088c7 --- /dev/null +++ b/apps/base/mailgateway/kustomization.yaml @@ -0,0 +1,32 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - certificate.yaml + - gateway.yaml + - httproute.yaml + - namespace.yaml + - postfix-deployment.yaml + - postfix-hpa.yaml + - rspamd-deployment.yaml + - rspamd-hpa.yaml + - services.yaml + - vaultauth.yaml + - vaultstaticsecret.yaml + +configMapGenerator: + - name: postfix-config + files: + - main.cf=resources/postfix/main.cf + - master.cf=resources/postfix/master.cf + - transport=resources/postfix/transport + options: + disableNameSuffixHash: true + - name: rspamd-config + files: + - worker-proxy.inc=resources/rspamd/local.d/worker-proxy.inc + - dkim_signing.conf=resources/rspamd/local.d/dkim_signing.conf + - milter_headers.conf=resources/rspamd/local.d/milter_headers.conf + options: + disableNameSuffixHash: true diff --git a/apps/base/mailgateway/namespace.yaml b/apps/base/mailgateway/namespace.yaml new file mode 100644 index 0000000..86e63f3 --- /dev/null +++ b/apps/base/mailgateway/namespace.yaml @@ -0,0 +1,5 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: mailgateway diff --git a/apps/base/mailgateway/postfix-deployment.yaml b/apps/base/mailgateway/postfix-deployment.yaml new file mode 100644 index 0000000..73fd562 --- /dev/null +++ b/apps/base/mailgateway/postfix-deployment.yaml @@ -0,0 +1,85 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: postfix + namespace: mailgateway +spec: + selector: + matchLabels: + app: postfix + template: + metadata: + annotations: + reloader.stakater.com/auto: "true" + labels: + app: postfix + spec: + containers: + - name: postfix + image: tozd/postfix:alpine-322 + ports: + - containerPort: 25 + name: smtp + protocol: TCP + - containerPort: 587 + name: submission + protocol: TCP + env: + # Keep these in sync with main.cf so the tozd startup postconf calls are no-ops + - name: MAILNAME + value: "mail.main.unkin.net" + - name: MY_NETWORKS + value: "127.0.0.0/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16" + - name: MY_DESTINATION + value: "localhost.localdomain, localhost" + - name: LOG_TO_STDOUT + value: "1" + livenessProbe: + tcpSocket: + port: 25 + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + tcpSocket: + port: 25 + initialDelaySeconds: 15 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: "1" + memory: 512Mi + volumeMounts: + # Mount main.cf and master.cf from ConfigMap using subPath + - name: postfix-config + mountPath: /etc/postfix/main.cf + subPath: main.cf + - name: postfix-config + mountPath: /etc/postfix/master.cf + subPath: master.cf + - name: postfix-config + mountPath: /etc/postfix/transport + subPath: transport + # TLS cert from cert-manager Certificate resource + - name: postfix-tls + mountPath: /etc/postfix/tls + readOnly: true + # Persistent mail queue + - name: spool + mountPath: /var/spool/postfix + volumes: + - name: postfix-config + configMap: + name: postfix-config + - name: postfix-tls + secret: + secretName: postfix-smtp-tls + - name: spool + emptyDir: {} diff --git a/apps/base/mailgateway/postfix-hpa.yaml b/apps/base/mailgateway/postfix-hpa.yaml new file mode 100644 index 0000000..0210544 --- /dev/null +++ b/apps/base/mailgateway/postfix-hpa.yaml @@ -0,0 +1,38 @@ +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: postfix-hpa + namespace: mailgateway +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: postfix + minReplicas: 2 + maxReplicas: 6 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 + behavior: + scaleUp: + stabilizationWindowSeconds: 0 + selectPolicy: Max + policies: + - type: Percent + value: 100 + periodSeconds: 60 + - type: Pods + value: 4 + periodSeconds: 30 + scaleDown: + stabilizationWindowSeconds: 300 + selectPolicy: Min + policies: + - type: Percent + value: 30 + periodSeconds: 60 diff --git a/apps/base/mailgateway/resources/postfix/main.cf b/apps/base/mailgateway/resources/postfix/main.cf new file mode 100644 index 0000000..b5466f7 --- /dev/null +++ b/apps/base/mailgateway/resources/postfix/main.cf @@ -0,0 +1,47 @@ +# Basic identity — kept in sync with MAILNAME/MY_NETWORKS/MY_DESTINATION env vars +# so the tozd startup script's postconf calls are no-ops +myhostname = mail.main.unkin.net +myorigin = main.unkin.net +mydestination = localhost.localdomain, localhost +mynetworks = 127.0.0.0/8 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 +inet_protocols = ipv4 +inet_interfaces = all + +# No local delivery — we're a relay-only gateway +local_transport = error:no local delivery +alias_maps = +alias_database = + +# Relay inbound mail for these domains to Stalwart +# texthash: reads plain text without requiring postmap (Alpine has no hash/btree) +relay_domains = main.unkin.net unkin.net +transport_maps = texthash:/etc/postfix/transport + +# rspamd milter (same namespace — short DNS name resolves) +smtpd_milters = inet:rspamd:11332 +non_smtpd_milters = inet:rspamd:11332 +milter_default_action = accept +milter_protocol = 6 +milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen} + +# Inbound TLS (cert from cert-manager Certificate resource) +smtpd_use_tls = yes +smtpd_tls_security_level = may +smtpd_tls_cert_file = /etc/postfix/tls/tls.crt +smtpd_tls_key_file = /etc/postfix/tls/tls.key +smtpd_tls_loglevel = 1 + +# Outbound TLS (opportunistic) +smtp_tls_security_level = may +smtp_tls_loglevel = 1 + +# Message size limit (50 MiB) +message_size_limit = 52428800 +mailbox_size_limit = 0 + +# Queue retention +maximal_queue_lifetime = 5d +bounce_queue_lifetime = 1d + +# Log to stdout for k8s log collection +maillog_file = /dev/stdout diff --git a/apps/base/mailgateway/resources/postfix/master.cf b/apps/base/mailgateway/resources/postfix/master.cf new file mode 100644 index 0000000..471f9c8 --- /dev/null +++ b/apps/base/mailgateway/resources/postfix/master.cf @@ -0,0 +1,42 @@ +# ========================================================================== +# service type private unpriv chroot wakeup maxproc command + args +# (yes) (yes) (yes) (never) (100) +# ========================================================================== + +# SMTP inbound (port 25) — runs rspamd milter, relays to Stalwart via transport_maps +smtp inet n - n - - smtpd + +# Submission (port 587) — TLS required, relay from trusted mynetworks only +submission inet n - n - - smtpd + -o syslog_name=postfix/submission + -o smtpd_tls_security_level=encrypt + -o smtpd_sasl_auth_enable=no + -o smtpd_reject_unlisted_recipient=no + -o smtpd_relay_restrictions=permit_mynetworks,reject + -o milter_macro_daemon_name=ORIGINATING + +# Internal postfix processes +pickup unix n - n 60 1 pickup +cleanup unix n - n - 0 cleanup +qmgr unix n - n 300 1 qmgr +tlsmgr unix - - n 1000? 1 tlsmgr +rewrite unix - - n - - trivial-rewrite +bounce unix - - n - 0 bounce +defer unix - - n - 0 bounce +trace unix - - n - 0 bounce +verify unix - - n - 1 verify +flush unix n - n 1000? 0 flush +proxymap unix - - n - - proxymap +proxywrite unix - - n - 1 proxymap +smtp unix - - n - - smtp +relay unix - - n - - smtp +showq unix n - n - - showq +error unix - - n - - error +retry unix - - n - - error +discard unix - - n - - discard +local unix - n n - - local +virtual unix - n n - - virtual +lmtp unix - - n - - lmtp +anvil unix - - n - 1 anvil +scache unix - - n - 1 scache +postlog unix-dgram n - n - 1 postlogd diff --git a/apps/base/mailgateway/resources/postfix/transport b/apps/base/mailgateway/resources/postfix/transport new file mode 100644 index 0000000..df78b5f --- /dev/null +++ b/apps/base/mailgateway/resources/postfix/transport @@ -0,0 +1,2 @@ +main.unkin.net smtp:[stalwart.stalwart.svc.cluster.local]:25 +unkin.net smtp:[stalwart.stalwart.svc.cluster.local]:25 diff --git a/apps/base/mailgateway/resources/rspamd/local.d/dkim_signing.conf b/apps/base/mailgateway/resources/rspamd/local.d/dkim_signing.conf new file mode 100644 index 0000000..4f90d72 --- /dev/null +++ b/apps/base/mailgateway/resources/rspamd/local.d/dkim_signing.conf @@ -0,0 +1,13 @@ +enabled = true; +selector = "mail"; + +domain { + main.unkin.net { + privkey = "/etc/rspamd/dkim/private_key"; + selector = "mail"; + } + unkin.net { + privkey = "/etc/rspamd/dkim/private_key"; + selector = "mail"; + } +} diff --git a/apps/base/mailgateway/resources/rspamd/local.d/milter_headers.conf b/apps/base/mailgateway/resources/rspamd/local.d/milter_headers.conf new file mode 100644 index 0000000..01b8162 --- /dev/null +++ b/apps/base/mailgateway/resources/rspamd/local.d/milter_headers.conf @@ -0,0 +1,2 @@ +extended_spam_headers = true; +use = ["x-spam-status", "x-spam-score", "authentication-results"]; diff --git a/apps/base/mailgateway/resources/rspamd/local.d/worker-proxy.inc b/apps/base/mailgateway/resources/rspamd/local.d/worker-proxy.inc new file mode 100644 index 0000000..d797213 --- /dev/null +++ b/apps/base/mailgateway/resources/rspamd/local.d/worker-proxy.inc @@ -0,0 +1,7 @@ +milter = yes; +bind_socket = "*:11332"; + +upstream "local" { + default = yes; + self_scan = yes; +} diff --git a/apps/base/mailgateway/rspamd-deployment.yaml b/apps/base/mailgateway/rspamd-deployment.yaml new file mode 100644 index 0000000..3800779 --- /dev/null +++ b/apps/base/mailgateway/rspamd-deployment.yaml @@ -0,0 +1,75 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: rspamd + namespace: mailgateway +spec: + selector: + matchLabels: + app: rspamd + template: + metadata: + annotations: + reloader.stakater.com/auto: "true" + labels: + app: rspamd + spec: + securityContext: + runAsUser: 11333 + runAsGroup: 11333 + fsGroup: 11333 + containers: + - name: rspamd + image: rspamd/rspamd:4.0.1 + ports: + - containerPort: 11332 + name: milter + protocol: TCP + - containerPort: 11333 + name: worker + protocol: TCP + - containerPort: 11334 + name: controller + protocol: TCP + livenessProbe: + httpGet: + path: /ping + port: 11334 + initialDelaySeconds: 15 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /ping + port: 11334 + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: "1" + memory: 512Mi + volumeMounts: + - name: rspamd-config + mountPath: /etc/rspamd/local.d + readOnly: true + - name: dkim-keys + mountPath: /etc/rspamd/dkim + readOnly: true + - name: rspamd-data + mountPath: /var/lib/rspamd + volumes: + - name: rspamd-config + configMap: + name: rspamd-config + - name: dkim-keys + secret: + secretName: dkim-keys + - name: rspamd-data + emptyDir: {} diff --git a/apps/base/mailgateway/rspamd-hpa.yaml b/apps/base/mailgateway/rspamd-hpa.yaml new file mode 100644 index 0000000..bad2966 --- /dev/null +++ b/apps/base/mailgateway/rspamd-hpa.yaml @@ -0,0 +1,38 @@ +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: rspamd-hpa + namespace: mailgateway +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: rspamd + minReplicas: 2 + maxReplicas: 6 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 + behavior: + scaleUp: + stabilizationWindowSeconds: 0 + selectPolicy: Max + policies: + - type: Percent + value: 100 + periodSeconds: 30 + - type: Pods + value: 4 + periodSeconds: 30 + scaleDown: + stabilizationWindowSeconds: 300 + selectPolicy: Min + policies: + - type: Percent + value: 30 + periodSeconds: 60 diff --git a/apps/base/mailgateway/services.yaml b/apps/base/mailgateway/services.yaml new file mode 100644 index 0000000..3ccbcbf --- /dev/null +++ b/apps/base/mailgateway/services.yaml @@ -0,0 +1,61 @@ +--- +# Internal service for rspamd - used by postfix pods in the same namespace +apiVersion: v1 +kind: Service +metadata: + name: rspamd + namespace: mailgateway +spec: + selector: + app: rspamd + ports: + - name: milter + port: 11332 + targetPort: 11332 + protocol: TCP + - name: worker + port: 11333 + targetPort: 11333 + protocol: TCP + - name: controller + port: 11334 + targetPort: 11334 + protocol: TCP +--- +# Internal ClusterIP for postfix - used by stalwart for outbound relay +apiVersion: v1 +kind: Service +metadata: + name: postfix + namespace: mailgateway +spec: + selector: + app: postfix + ports: + - name: smtp + port: 25 + targetPort: 25 + protocol: TCP + - name: submission + port: 587 + targetPort: 587 + protocol: TCP +--- +# External LoadBalancer for inbound MX (internet → postfix) +apiVersion: v1 +kind: Service +metadata: + name: postfix-external + namespace: mailgateway + annotations: + external-dns.alpha.kubernetes.io/hostname: smtp-in.main.unkin.net +spec: + type: LoadBalancer + externalTrafficPolicy: Local + selector: + app: postfix + ports: + - name: smtp + port: 25 + targetPort: 25 + protocol: TCP diff --git a/apps/base/mailgateway/vaultauth.yaml b/apps/base/mailgateway/vaultauth.yaml new file mode 100644 index 0000000..593e2c2 --- /dev/null +++ b/apps/base/mailgateway/vaultauth.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultAuth +metadata: + name: default + namespace: mailgateway +spec: + allowedNamespaces: + - mailgateway + kubernetes: + audiences: + - vault + role: default + serviceAccount: default + tokenExpirationSeconds: 600 + method: kubernetes + mount: k8s/au/syd1 + vaultConnectionRef: vso-system/default diff --git a/apps/base/mailgateway/vaultstaticsecret.yaml b/apps/base/mailgateway/vaultstaticsecret.yaml new file mode 100644 index 0000000..c47cc4c --- /dev/null +++ b/apps/base/mailgateway/vaultstaticsecret.yaml @@ -0,0 +1,17 @@ +--- +apiVersion: secrets.hashicorp.com/v1beta1 +kind: VaultStaticSecret +metadata: + name: dkim-keys + namespace: mailgateway +spec: + destination: + create: true + name: dkim-keys + overwrite: true + hmacSecretData: true + mount: kv + path: kubernetes/namespace/mailgateway/default/dkim-keys + refreshAfter: 5m + type: kv-v2 + vaultAuthRef: default diff --git a/apps/overlays/au-syd1/mailgateway/kustomization.yaml b/apps/overlays/au-syd1/mailgateway/kustomization.yaml new file mode 100644 index 0000000..def9a9f --- /dev/null +++ b/apps/overlays/au-syd1/mailgateway/kustomization.yaml @@ -0,0 +1,6 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - ../../../base/mailgateway diff --git a/argocd/applicationsets/platform.yaml b/argocd/applicationsets/platform.yaml index ac5afac..e25b55e 100644 --- a/argocd/applicationsets/platform.yaml +++ b/argocd/applicationsets/platform.yaml @@ -21,6 +21,7 @@ spec: - path: apps/overlays/*/inteldeviceplugins-system - path: apps/overlays/*/jfrog - path: apps/overlays/*/node-feature-discovery + - path: apps/overlays/*/mailgateway - path: apps/overlays/*/puppet - path: apps/overlays/*/purelb - path: apps/overlays/*/reflector-system