diff --git a/apps/base/bind-internal/authoritative/unkin-net/a/kustomization.yaml b/apps/base/bind-internal/authoritative/unkin-net/a/kustomization.yaml index 5f7821d..0b2c906 100644 --- a/apps/base/bind-internal/authoritative/unkin-net/a/kustomization.yaml +++ b/apps/base/bind-internal/authoritative/unkin-net/a/kustomization.yaml @@ -16,5 +16,6 @@ resources: - identity.yaml - lb1.yaml - logviewer.yaml + - mail.yaml - vlogs.yaml - watchstate.yaml diff --git a/apps/base/bind-internal/authoritative/unkin-net/a/mail.yaml b/apps/base/bind-internal/authoritative/unkin-net/a/mail.yaml new file mode 100644 index 0000000..5aff86b --- /dev/null +++ b/apps/base/bind-internal/authoritative/unkin-net/a/mail.yaml @@ -0,0 +1,14 @@ +--- +apiVersion: bind.unkin.net/v1alpha1 +kind: DNSRecord +metadata: + name: mail-dns-internal + namespace: bind-internal +spec: + zoneRef: unkin-net + name: mail + type: A + ttl: 600 + values: + # stalwart-lb PureLB VIP (IMAPS, submission, HTTPS). + - 198.18.200.18 diff --git a/apps/base/cert-manager/certificate_wildcard-main-unkin-net.yaml b/apps/base/cert-manager/certificate_wildcard-main-unkin-net.yaml index bc34a2b..8bed1a2 100644 --- a/apps/base/cert-manager/certificate_wildcard-main-unkin-net.yaml +++ b/apps/base/cert-manager/certificate_wildcard-main-unkin-net.yaml @@ -1,6 +1,6 @@ --- # Let's Encrypt *.main.unkin.net wildcard for the haproxy edge (pve, arr stack, -# jellyfin, stalwart webadmin/autoconfig). DNS-01 needs the delegated +# jellyfin, stalwart webadmin/autoconfig) and the mail smarthost. DNS-01 needs the delegated # _acme-challenge.main.unkin.net CNAME in the public unkin.net zone. # _acme-challenge.main.unkin.net. CNAME _acme-challenge.main.acme.unkin.net. apiVersion: cert-manager.io/v1 @@ -13,9 +13,9 @@ spec: secretTemplate: annotations: reflector.v1.k8s.emberstack.com/reflection-allowed: "true" - reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces: "haproxy" + reflector.v1.k8s.emberstack.com/reflection-allowed-namespaces: "haproxy,mail" reflector.v1.k8s.emberstack.com/reflection-auto-enabled: "true" - reflector.v1.k8s.emberstack.com/reflection-auto-namespaces: "haproxy" + reflector.v1.k8s.emberstack.com/reflection-auto-namespaces: "haproxy,mail" privateKey: size: 4096 dnsNames: diff --git a/apps/base/mail/kustomization.yaml b/apps/base/mail/kustomization.yaml index bdeda69..c712c55 100644 --- a/apps/base/mail/kustomization.yaml +++ b/apps/base/mail/kustomization.yaml @@ -8,6 +8,10 @@ resources: - gateway.yaml - httproute.yaml - namespace.yaml + - smarthost-networkpolicy.yaml + - smarthost-pdb.yaml + - smarthost-service.yaml + - smarthost-statefulset.yaml - stalwart-apply-job.yaml - stalwart-apply-rbac.yaml - stalwart-blobs-bucket.yaml @@ -19,3 +23,17 @@ resources: - vaultauth.yaml - vaultstaticsecret.yaml - vmpodscrape.yaml + +configMapGenerator: + - name: smarthost-config + namespace: mail + options: + disableNameSuffixHash: true + files: + - smarthost/main.cf + - smarthost/master.cf + - smarthost/relay_domains + - smarthost/transport + - smarthost/gateways.cidr + - smarthost/mynetworks.cidr + - smarthost/tls_policy diff --git a/apps/base/mail/smarthost-networkpolicy.yaml b/apps/base/mail/smarthost-networkpolicy.yaml new file mode 100644 index 0000000..05f4c67 --- /dev/null +++ b/apps/base/mail/smarthost-networkpolicy.yaml @@ -0,0 +1,78 @@ +--- +# Gateways (public IPs, see smarthost/gateways.cidr), lab networks (incl. node IPs for kubelet probes) and opted-in pods in any namespace. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: smarthost + namespace: mail +spec: + podSelector: + matchLabels: + app.kubernetes.io/name: smarthost + policyTypes: + - Ingress + ingress: + - from: + - ipBlock: + cidr: 192.0.2.1/32 + - ipBlock: + cidr: 192.0.2.2/32 + - ipBlock: + cidr: 10.10.12.200/32 + - ipBlock: + cidr: 198.18.1.10/32 + - ipBlock: + cidr: 198.18.2.160/27 + - ipBlock: + cidr: 198.18.2.192/27 + - ipBlock: + cidr: 198.18.13.0/24 + - ipBlock: + cidr: 198.18.14.0/24 + - ipBlock: + cidr: 198.18.15.0/24 + - ipBlock: + cidr: 198.18.16.0/24 + - ipBlock: + cidr: 198.18.17.0/24 + - ipBlock: + cidr: 198.18.18.0/24 + - ipBlock: + cidr: 198.18.19.0/24 + - ipBlock: + cidr: 198.18.20.0/24 + - ipBlock: + cidr: 198.18.21.0/24 + - ipBlock: + cidr: 198.18.22.0/24 + - ipBlock: + cidr: 198.18.23.0/24 + - ipBlock: + cidr: 198.18.25.0/24 + - ipBlock: + cidr: 198.18.26.0/24 + - ipBlock: + cidr: 198.18.27.0/24 + - ipBlock: + cidr: 198.18.28.0/24 + - ipBlock: + cidr: 198.18.29.0/24 + - ipBlock: + cidr: 198.18.64.0/24 + - ipBlock: + cidr: 198.18.65.0/24 + - ipBlock: + cidr: 198.18.66.0/24 + - ipBlock: + cidr: 198.18.67.0/24 + - ipBlock: + cidr: 198.18.68.0/24 + - ipBlock: + cidr: 198.18.69.0/24 + - namespaceSelector: {} + podSelector: + matchLabels: + mail.unkin.net/smarthost-client: "true" + ports: + - port: smtp + protocol: TCP diff --git a/apps/base/mail/smarthost-pdb.yaml b/apps/base/mail/smarthost-pdb.yaml new file mode 100644 index 0000000..272d9cf --- /dev/null +++ b/apps/base/mail/smarthost-pdb.yaml @@ -0,0 +1,11 @@ +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: smarthost + namespace: mail +spec: + maxUnavailable: 1 + selector: + matchLabels: + app.kubernetes.io/name: smarthost diff --git a/apps/base/mail/smarthost-service.yaml b/apps/base/mail/smarthost-service.yaml new file mode 100644 index 0000000..aabcec7 --- /dev/null +++ b/apps/base/mail/smarthost-service.yaml @@ -0,0 +1,33 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: smarthost + namespace: mail +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: smarthost + ports: + - name: smtp + port: 25 + targetPort: smtp +--- +# Gateways and lab networks; Local keeps client IPs for the postfix CIDR checks. +apiVersion: v1 +kind: Service +metadata: + name: smarthost-lb + namespace: mail + annotations: + purelb.io/addresses: 198.18.200.16 + purelb.io/service-group: common +spec: + type: LoadBalancer + externalTrafficPolicy: Local + selector: + app.kubernetes.io/name: smarthost + ports: + - name: smtp + port: 25 + targetPort: smtp diff --git a/apps/base/mail/smarthost-statefulset.yaml b/apps/base/mail/smarthost-statefulset.yaml new file mode 100644 index 0000000..2bd4527 --- /dev/null +++ b/apps/base/mail/smarthost-statefulset.yaml @@ -0,0 +1,117 @@ +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: smarthost + namespace: mail + annotations: + configmap.reloader.stakater.com/reload: "smarthost-config" +spec: + serviceName: smarthost + replicas: 2 + selector: + matchLabels: + app.kubernetes.io/name: smarthost + template: + metadata: + labels: + app.kubernetes.io/name: smarthost + spec: + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: DoNotSchedule + labelSelector: + matchLabels: + app.kubernetes.io/name: smarthost + securityContext: + seccompProfile: + type: RuntimeDefault + containers: + - name: postfix + image: artifactapi.k8s.syd1.au.unkin.net/docker-internal/postfix:0.1.0 + imagePullPolicy: IfNotPresent + ports: + - containerPort: 25 + name: smtp + protocol: TCP + readinessProbe: + tcpSocket: + port: smtp + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + exec: + command: ["postfix", "status"] + initialDelaySeconds: 30 + periodSeconds: 30 + # postfix master runs as root and drops to the postfix user per service. + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + add: + - CHOWN + - DAC_OVERRIDE + - FOWNER + - SETUID + - SETGID + - NET_BIND_SERVICE + resources: + requests: + cpu: 50m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + volumeMounts: + - name: spool + mountPath: /var/spool/postfix + - name: tls + mountPath: /etc/postfix/tls + readOnly: true + - name: config + mountPath: /etc/postfix/main.cf + subPath: main.cf + readOnly: true + - name: config + mountPath: /etc/postfix/master.cf + subPath: master.cf + readOnly: true + - name: config + mountPath: /etc/postfix/relay_domains + subPath: relay_domains + readOnly: true + - name: config + mountPath: /etc/postfix/transport + subPath: transport + readOnly: true + - name: config + mountPath: /etc/postfix/gateways.cidr + subPath: gateways.cidr + readOnly: true + - name: config + mountPath: /etc/postfix/mynetworks.cidr + subPath: mynetworks.cidr + readOnly: true + - name: config + mountPath: /etc/postfix/tls_policy + subPath: tls_policy + readOnly: true + volumes: + - name: config + configMap: + name: smarthost-config + - name: tls + secret: + secretName: wildcard-main-unkin-net-tls + volumeClaimTemplates: + - metadata: + name: spool + spec: + accessModes: [ReadWriteOnce] + storageClassName: cephrbd-fast-delete + resources: + requests: + storage: 50Gi diff --git a/apps/base/mail/smarthost/gateways.cidr b/apps/base/mail/smarthost/gateways.cidr new file mode 100644 index 0000000..c3d1c3b --- /dev/null +++ b/apps/base/mail/smarthost/gateways.cidr @@ -0,0 +1,3 @@ +# Gateway (mta1/mta2) public IPs; TEST-NET placeholders until the BinaryLane servers exist. +192.0.2.1/32 reject_plaintext_session, permit +192.0.2.2/32 reject_plaintext_session, permit diff --git a/apps/base/mail/smarthost/main.cf b/apps/base/mail/smarthost/main.cf new file mode 100644 index 0000000..5f85dd2 --- /dev/null +++ b/apps/base/mail/smarthost/main.cf @@ -0,0 +1,42 @@ +compatibility_level = 3.5 +myhostname = mail-in.main.unkin.net +myorigin = unkin.net +mydestination = +alias_maps = +local_recipient_maps = +inet_interfaces = all +inet_protocols = ipv4 +maillog_file = /dev/stdout +message_size_limit = 52428800 + +# Relay-anywhere clients; gateways must stay out. +mynetworks = cidr:/etc/postfix/mynetworks.cidr + +relay_domains = texthash:/etc/postfix/relay_domains +# Default minus relay_domains: subdomains are not delivered and would loop back out via the gateways. +parent_domain_matches_subdomains = debug_peer_list,fast_flush_domains,mynetworks,permit_mx_backup_networks,qmqpd_authorized_clients,smtpd_access_maps +transport_maps = texthash:/etc/postfix/transport +relayhost = [mta1.edge.unkin.net]:25 +smtp_fallback_relay = [mta2.edge.unkin.net]:25 +smtp_tls_policy_maps = texthash:/etc/postfix/tls_policy +smtp_tls_CAfile = /etc/pki/tls/certs/ca-bundle.crt +smtpd_client_restrictions = permit_mynetworks, check_client_access cidr:/etc/postfix/gateways.cidr, reject +smtpd_relay_restrictions = permit_mynetworks, reject_unauth_destination +smtpd_recipient_restrictions = permit_mynetworks, reject_unverified_recipient +unverified_recipient_reject_code = 550 +unverified_recipient_tempfail_action = defer_if_permit +address_verify_negative_refresh_time = 5m +address_verify_negative_expire_time = 1h +address_verify_positive_refresh_time = 1h +address_verify_positive_expire_time = 1d + +milter_default_action = tempfail +smtpd_milters = inet:rspamd.mail.svc:11332 +smtpd_authorized_xforward_hosts = cidr:/etc/postfix/gateways.cidr + +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_received_header = yes +smtp_tls_security_level = may +lmtp_tls_security_level = none diff --git a/apps/base/mail/smarthost/master.cf b/apps/base/mail/smarthost/master.cf new file mode 100644 index 0000000..94ed1f7 --- /dev/null +++ b/apps/base/mail/smarthost/master.cf @@ -0,0 +1,27 @@ +# Gateways (inbound to relay_domains) and mynetworks (relay anywhere); all via the rspamd milter. +25 inet n - n - - smtpd +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 + -o syslog_name=postfix/$service_name +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/mail/smarthost/mynetworks.cidr b/apps/base/mail/smarthost/mynetworks.cidr new file mode 100644 index 0000000..c9f5e9d --- /dev/null +++ b/apps/base/mail/smarthost/mynetworks.cidr @@ -0,0 +1,31 @@ +# Pod CIDR (narrowed by smarthost-networkpolicy.yaml) plus the lab networks from bind-internal acl-main/acl-common/acl-nomad-jobs. +# Keep in sync with the NetworkPolicy. +10.42.0.0/16 OK +10.10.12.200/32 OK +198.18.1.10/32 OK +198.18.2.160/27 OK +198.18.2.192/27 OK +198.18.21.160/27 OK +198.18.21.192/27 OK +198.18.13.0/24 OK +198.18.14.0/24 OK +198.18.15.0/24 OK +198.18.16.0/24 OK +198.18.17.0/24 OK +198.18.18.0/24 OK +198.18.19.0/24 OK +198.18.20.0/24 OK +198.18.21.0/24 OK +198.18.22.0/24 OK +198.18.23.0/24 OK +198.18.25.0/24 OK +198.18.26.0/24 OK +198.18.27.0/24 OK +198.18.28.0/24 OK +198.18.29.0/24 OK +198.18.64.0/24 OK +198.18.65.0/24 OK +198.18.66.0/24 OK +198.18.67.0/24 OK +198.18.68.0/24 OK +198.18.69.0/24 OK diff --git a/apps/base/mail/smarthost/relay_domains b/apps/base/mail/smarthost/relay_domains new file mode 100644 index 0000000..59818b2 --- /dev/null +++ b/apps/base/mail/smarthost/relay_domains @@ -0,0 +1 @@ +unkin.net OK diff --git a/apps/base/mail/smarthost/tls_policy b/apps/base/mail/smarthost/tls_policy new file mode 100644 index 0000000..dd1481a --- /dev/null +++ b/apps/base/mail/smarthost/tls_policy @@ -0,0 +1,2 @@ +[mta1.edge.unkin.net]:25 secure match=hostname +[mta2.edge.unkin.net]:25 secure match=hostname diff --git a/apps/base/mail/smarthost/transport b/apps/base/mail/smarthost/transport new file mode 100644 index 0000000..828306f --- /dev/null +++ b/apps/base/mail/smarthost/transport @@ -0,0 +1 @@ +unkin.net lmtp:[stalwart.mail.svc.cluster.local]:24