# services/mailu/helmrelease.yaml apiVersion: helm.toolkit.fluxcd.io/v2 kind: HelmRelease metadata: name: mailu namespace: mailu-mailserver spec: interval: 30m chart: spec: chart: mailu version: 2.1.2 sourceRef: kind: HelmRepository name: mailu namespace: flux-system install: remediation: { retries: 3 } timeout: 10m upgrade: remediation: retries: 3 remediateLastFailure: true cleanupOnFail: true timeout: 10m values: mailuVersion: "2024.06" domain: bstein.dev hostnames: [mail.bstein.dev] domains: - name: bstein.dev enabled: true dkim: enabled: true externalRelay: host: "[smtp.postmarkapp.com]:587" existingSecret: mailu-postmark-relay usernameKey: relay-password passwordKey: relay-password timezone: Etc/UTC subnet: 10.42.0.0/16 existingSecret: mailu-secret tls: outboundLevel: encrypt externalDatabase: enabled: true type: postgresql host: postgres-service.postgres.svc.cluster.local port: 5432 database: mailu username: mailu existingSecret: mailu-db-secret existingSecretUsernameKey: username existingSecretPasswordKey: password existingSecretDatabaseKey: database initialAccount: enabled: true username: test domain: bstein.dev existingSecret: mailu-initial-account-secret existingSecretPasswordKey: password persistence: accessModes: [ReadWriteMany] size: 100Gi storageClass: astreae single_pvc: true front: hostnames: [mail.bstein.dev] proxied: true hostPort: enabled: false https: enabled: false external: false forceHttps: false externalService: enabled: true type: LoadBalancer externalTrafficPolicy: Cluster ports: submission: true nodePorts: pop3: 30010 pop3s: 30011 imap: 30143 imaps: 30993 manageSieve: 30419 smtp: 30025 smtps: 30465 submission: 30587 logLevel: DEBUG nodeSelector: hardware: rpi4 admin: logLevel: DEBUG nodeSelector: hardware: rpi4 podLivenessProbe: enabled: true initialDelaySeconds: 30 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 6 successThreshold: 1 podReadinessProbe: enabled: true initialDelaySeconds: 20 periodSeconds: 10 timeoutSeconds: 5 failureThreshold: 6 successThreshold: 1 extraEnvVars: - name: FLASK_DEBUG value: "1" - name: ACCESSLOG value: /dev/stdout - name: ERRORLOG value: /dev/stderr - name: WEBROOT_REDIRECT value: "" - name: FORWARDED_ALLOW_IPS value: 127.0.0.1,10.42.0.0/16 - name: DNS_RESOLVERS value: 1.1.1.1,9.9.9.9 extraVolumes: - name: unbound-config configMap: name: mailu-unbound - name: unbound-run emptyDir: {} extraVolumeMounts: - name: unbound-run mountPath: /var/lib/unbound extraContainers: - name: unbound image: docker.io/alpine:3.20 command: ["/bin/sh", "-c"] args: - | while :; do printf "nameserver 10.43.0.10\n" > /etc/resolv.conf if apk add --no-cache unbound bind-tools; then break fi echo "apk failed, retrying" >&2 sleep 10 done cat >/etc/resolv.conf <<'EOF' search mailu-mailserver.svc.cluster.local svc.cluster.local cluster.local nameserver 127.0.0.1 EOF unbound-anchor -a /var/lib/unbound/root.key || true exec unbound -d -c /opt/unbound/etc/unbound/unbound.conf ports: - containerPort: 53 protocol: UDP - containerPort: 53 protocol: TCP volumeMounts: - name: unbound-config mountPath: /opt/unbound/etc/unbound - name: unbound-run mountPath: /var/lib/unbound dnsPolicy: None dnsConfig: nameservers: - 127.0.0.1 searches: - mailu-mailserver.svc.cluster.local - svc.cluster.local - cluster.local clamav: image: repository: clamav/clamav-debian tag: "1.4" logLevel: DEBUG nodeSelector: hardware: rpi5 resources: requests: cpu: 200m memory: 1Gi limits: cpu: 500m memory: 3Gi livenessProbe: enabled: false initialDelaySeconds: 300 periodSeconds: 30 timeoutSeconds: 5 failureThreshold: 6 successThreshold: 1 startupProbe: enabled: false initialDelaySeconds: 60 periodSeconds: 30 timeoutSeconds: 5 failureThreshold: 20 successThreshold: 1 readinessProbe: enabled: false initialDelaySeconds: 300 periodSeconds: 30 timeoutSeconds: 5 failureThreshold: 6 successThreshold: 1 dovecot: logLevel: DEBUG nodeSelector: hardware: rpi4 oletools: logLevel: DEBUG nodeSelector: hardware: rpi4 postfix: logLevel: DEBUG nodeSelector: hardware: rpi4 overrides: postfix.cf: | mynetworks = 127.0.0.0/8 [::1]/128 10.42.0.0/16 10.43.0.0/16 192.168.22.0/24 recipient_canonical_maps = regexp:/overrides/recipient_canonical, ${podop}recipientmap recipient_canonical_classes = envelope_recipient,header_recipient smtpd_delay_reject = yes smtpd_helo_required = yes smtpd_helo_restrictions = reject_invalid_helo_hostname, reject_non_fqdn_helo_hostname, reject_unknown_helo_hostname smtpd_sasl_auth_enable = yes smtpd_sasl_type = dovecot smtpd_sasl_path = private/auth smtpd_sasl_security_options = noanonymous smtpd_sasl_tls_security_options = noanonymous smtpd_client_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_pipelining, reject_unknown_client_hostname smtpd_recipient_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination, reject_non_fqdn_recipient, reject_unknown_recipient_domain smtpd_relay_restrictions = permit_sasl_authenticated, reject_unauth_destination smtpd_sender_restrictions = reject_non_fqdn_sender, reject_unknown_sender_domain, reject_sender_login_mismatch, reject_authenticated_sender_login_mismatch smtpd_tls_auth_only = yes smtpd_forbid_unauth_pipelining = yes smtpd_client_connection_count_limit = 20 smtpd_client_connection_rate_limit = 30 smtpd_client_message_rate_limit = 100 smtpd_client_recipient_rate_limit = 200 smtpd_recipient_limit = 100 recipient_canonical: | /^double-bounce@mail\.bstein\.dev$/ double-bounce@bstein.dev podAnnotations: bstein.dev/restarted-at: "2026-01-20T04:20:00Z" redis: enabled: true architecture: standalone logLevel: DEBUG image: repository: bitnamilegacy/redis tag: 8.0.3-debian-12-r3 master: nodeSelector: hardware: rpi4 persistence: enabled: true accessModes: [ReadWriteMany] size: 8Gi storageClass: astreae rspamd: logLevel: DEBUG nodeSelector: hardware: rpi4 persistence: accessModes: [ReadWriteOnce] size: 8Gi storageClass: astreae tika: logLevel: DEBUG nodeSelector: hardware: rpi4 global: logLevel: DEBUG storageClass: astreae webmail: enabled: false nodeSelector: hardware: rpi4 ingress: enabled: false ingressClassName: traefik tls: true existingSecret: mailu-certificates annotations: traefik.ingress.kubernetes.io/router.entrypoints: websecure traefik.ingress.kubernetes.io/service.serversscheme: https traefik.ingress.kubernetes.io/service.serverstransport: mailu-transport@kubernetescrd extraRules: - host: mail.bstein.dev http: paths: - path: / pathType: Prefix backend: service: name: mailu-front port: number: 443 service: ports: smtp: port: 25 targetPort: 25 smtps: port: 465 targetPort: 465 submission: port: 587 targetPort: 587 postRenderers: - kustomize: patches: - target: kind: Deployment name: mailu-admin patch: |- apiVersion: apps/v1 kind: Deployment metadata: name: mailu-admin spec: template: metadata: annotations: vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/role: "mailu-mailserver" vault.hashicorp.com/agent-inject-secret-mailu-env.sh: "kv/data/atlas/mailu/mailu-secret" vault.hashicorp.com/agent-inject-template-mailu-env.sh: | {{ with secret "kv/data/atlas/mailu/mailu-secret" }} export SECRET_KEY="{{ index .Data.data "secret-key" }}" {{ end }} {{ with secret "kv/data/atlas/mailu/mailu-db-secret" }} export DB_PW="{{ .Data.data.password }}" export ROUNDCUBE_DB_PW="{{ .Data.data.password }}" {{ end }} {{ with secret "kv/data/atlas/mailu/mailu-initial-account-secret" }} export INITIAL_ADMIN_PW="{{ .Data.data.password }}" {{ end }} {{ with secret "kv/data/atlas/shared/postmark-relay" }} {{- $access := index .Data.data "accesskey" -}} {{- $secret := index .Data.data "secretkey" -}} {{- if and $access $secret }} export RELAYUSER="{{ $access }}" export RELAYPASSWORD="{{ $secret }}" {{- else }} export RELAYUSER="{{ index .Data.data "apikey" }}" export RELAYPASSWORD="{{ index .Data.data "apikey" }}" {{- end }} {{ end }} spec: serviceAccountName: mailu-vault-sync automountServiceAccountToken: true containers: - name: admin command: - /entrypoint.sh args: - python3 - /start.py env: - name: SECRET_KEY $patch: delete - name: INITIAL_ADMIN_PW $patch: delete - name: DB_PW $patch: delete - name: RELAYUSER $patch: delete - name: RELAYPASSWORD $patch: delete - name: VAULT_ENV_FILE value: /vault/secrets/mailu-env.sh volumeMounts: - name: mailu-vault-entrypoint mountPath: /entrypoint.sh subPath: vault-entrypoint.sh volumes: - name: mailu-vault-entrypoint configMap: name: mailu-vault-entrypoint defaultMode: 493 - target: kind: Deployment name: mailu-front patch: |- apiVersion: apps/v1 kind: Deployment metadata: name: mailu-front spec: template: metadata: annotations: vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/role: "mailu-mailserver" vault.hashicorp.com/agent-inject-secret-mailu-env.sh: "kv/data/atlas/mailu/mailu-secret" vault.hashicorp.com/agent-inject-template-mailu-env.sh: | {{ with secret "kv/data/atlas/mailu/mailu-secret" }} export SECRET_KEY="{{ index .Data.data "secret-key" }}" {{ end }} {{ with secret "kv/data/atlas/mailu/mailu-db-secret" }} export DB_PW="{{ .Data.data.password }}" export ROUNDCUBE_DB_PW="{{ .Data.data.password }}" {{ end }} {{ with secret "kv/data/atlas/mailu/mailu-initial-account-secret" }} export INITIAL_ADMIN_PW="{{ .Data.data.password }}" {{ end }} {{ with secret "kv/data/atlas/shared/postmark-relay" }} {{- $access := index .Data.data "accesskey" -}} {{- $secret := index .Data.data "secretkey" -}} {{- if and $access $secret }} export RELAYUSER="{{ $access }}" export RELAYPASSWORD="{{ $secret }}" {{- else }} export RELAYUSER="{{ index .Data.data "apikey" }}" export RELAYPASSWORD="{{ index .Data.data "apikey" }}" {{- end }} {{ end }} spec: serviceAccountName: mailu-vault-sync automountServiceAccountToken: true containers: - name: front command: - /entrypoint.sh args: - python3 - /start.py env: - name: SECRET_KEY $patch: delete - name: INITIAL_ADMIN_PW $patch: delete - name: DB_PW $patch: delete - name: RELAYUSER $patch: delete - name: RELAYPASSWORD $patch: delete - name: VAULT_ENV_FILE value: /vault/secrets/mailu-env.sh volumeMounts: - name: mailu-vault-entrypoint mountPath: /entrypoint.sh subPath: vault-entrypoint.sh volumes: - name: mailu-vault-entrypoint configMap: name: mailu-vault-entrypoint defaultMode: 493 - target: kind: Deployment name: mailu-postfix patch: |- apiVersion: apps/v1 kind: Deployment metadata: name: mailu-postfix spec: strategy: type: Recreate template: metadata: annotations: vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/role: "mailu-mailserver" vault.hashicorp.com/agent-inject-secret-mailu-env.sh: "kv/data/atlas/mailu/mailu-secret" vault.hashicorp.com/agent-inject-template-mailu-env.sh: | {{ with secret "kv/data/atlas/mailu/mailu-secret" }} export SECRET_KEY="{{ index .Data.data "secret-key" }}" {{ end }} {{ with secret "kv/data/atlas/mailu/mailu-db-secret" }} export DB_PW="{{ .Data.data.password }}" export ROUNDCUBE_DB_PW="{{ .Data.data.password }}" {{ end }} {{ with secret "kv/data/atlas/mailu/mailu-initial-account-secret" }} export INITIAL_ADMIN_PW="{{ .Data.data.password }}" {{ end }} {{ with secret "kv/data/atlas/shared/postmark-relay" }} {{- $access := index .Data.data "accesskey" -}} {{- $secret := index .Data.data "secretkey" -}} {{- if and $access $secret }} export RELAYUSER="{{ $access }}" export RELAYPASSWORD="{{ $secret }}" {{- else }} export RELAYUSER="{{ index .Data.data "apikey" }}" export RELAYPASSWORD="{{ index .Data.data "apikey" }}" {{- end }} {{ end }} spec: serviceAccountName: mailu-vault-sync automountServiceAccountToken: true containers: - name: postfix command: - /entrypoint.sh args: - python3 - /start.py env: - name: SECRET_KEY $patch: delete - name: INITIAL_ADMIN_PW $patch: delete - name: DB_PW $patch: delete - name: RELAYUSER $patch: delete - name: RELAYPASSWORD $patch: delete - name: VAULT_ENV_FILE value: /vault/secrets/mailu-env.sh volumeMounts: - name: mailu-vault-entrypoint mountPath: /entrypoint.sh subPath: vault-entrypoint.sh volumes: - name: mailu-vault-entrypoint configMap: name: mailu-vault-entrypoint defaultMode: 493 - target: kind: Deployment name: mailu-dovecot patch: |- apiVersion: apps/v1 kind: Deployment metadata: name: mailu-dovecot spec: template: metadata: annotations: vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/role: "mailu-mailserver" vault.hashicorp.com/agent-inject-secret-mailu-env.sh: "kv/data/atlas/mailu/mailu-secret" vault.hashicorp.com/agent-inject-template-mailu-env.sh: | {{ with secret "kv/data/atlas/mailu/mailu-secret" }} export SECRET_KEY="{{ index .Data.data "secret-key" }}" {{ end }} {{ with secret "kv/data/atlas/mailu/mailu-db-secret" }} export DB_PW="{{ .Data.data.password }}" export ROUNDCUBE_DB_PW="{{ .Data.data.password }}" {{ end }} {{ with secret "kv/data/atlas/mailu/mailu-initial-account-secret" }} export INITIAL_ADMIN_PW="{{ .Data.data.password }}" {{ end }} {{ with secret "kv/data/atlas/shared/postmark-relay" }} {{- $access := index .Data.data "accesskey" -}} {{- $secret := index .Data.data "secretkey" -}} {{- if and $access $secret }} export RELAYUSER="{{ $access }}" export RELAYPASSWORD="{{ $secret }}" {{- else }} export RELAYUSER="{{ index .Data.data "apikey" }}" export RELAYPASSWORD="{{ index .Data.data "apikey" }}" {{- end }} {{ end }} spec: serviceAccountName: mailu-vault-sync automountServiceAccountToken: true containers: - name: dovecot command: - /entrypoint.sh args: - python3 - /start.py env: - name: SECRET_KEY $patch: delete - name: INITIAL_ADMIN_PW $patch: delete - name: DB_PW $patch: delete - name: RELAYUSER $patch: delete - name: RELAYPASSWORD $patch: delete - name: VAULT_ENV_FILE value: /vault/secrets/mailu-env.sh volumeMounts: - name: mailu-vault-entrypoint mountPath: /entrypoint.sh subPath: vault-entrypoint.sh volumes: - name: mailu-vault-entrypoint configMap: name: mailu-vault-entrypoint defaultMode: 493 - target: kind: Deployment name: mailu-rspamd patch: |- apiVersion: apps/v1 kind: Deployment metadata: name: mailu-rspamd spec: template: metadata: annotations: vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/role: "mailu-mailserver" vault.hashicorp.com/agent-inject-secret-mailu-env.sh: "kv/data/atlas/mailu/mailu-secret" vault.hashicorp.com/agent-inject-template-mailu-env.sh: | {{ with secret "kv/data/atlas/mailu/mailu-secret" }} export SECRET_KEY="{{ index .Data.data "secret-key" }}" {{ end }} {{ with secret "kv/data/atlas/mailu/mailu-db-secret" }} export DB_PW="{{ .Data.data.password }}" export ROUNDCUBE_DB_PW="{{ .Data.data.password }}" {{ end }} {{ with secret "kv/data/atlas/mailu/mailu-initial-account-secret" }} export INITIAL_ADMIN_PW="{{ .Data.data.password }}" {{ end }} {{ with secret "kv/data/atlas/shared/postmark-relay" }} {{- $access := index .Data.data "accesskey" -}} {{- $secret := index .Data.data "secretkey" -}} {{- if and $access $secret }} export RELAYUSER="{{ $access }}" export RELAYPASSWORD="{{ $secret }}" {{- else }} export RELAYUSER="{{ index .Data.data "apikey" }}" export RELAYPASSWORD="{{ index .Data.data "apikey" }}" {{- end }} {{ end }} spec: serviceAccountName: mailu-vault-sync automountServiceAccountToken: true containers: - name: rspamd command: - /entrypoint.sh args: - python3 - /start.py env: - name: SECRET_KEY $patch: delete - name: INITIAL_ADMIN_PW $patch: delete - name: DB_PW $patch: delete - name: RELAYUSER $patch: delete - name: RELAYPASSWORD $patch: delete - name: VAULT_ENV_FILE value: /vault/secrets/mailu-env.sh volumeMounts: - name: mailu-vault-entrypoint mountPath: /entrypoint.sh subPath: vault-entrypoint.sh volumes: - name: mailu-vault-entrypoint configMap: name: mailu-vault-entrypoint defaultMode: 493 - target: kind: Deployment name: mailu-oletools patch: |- apiVersion: apps/v1 kind: Deployment metadata: name: mailu-oletools spec: template: metadata: annotations: vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/role: "mailu-mailserver" vault.hashicorp.com/agent-inject-secret-mailu-env.sh: "kv/data/atlas/mailu/mailu-secret" vault.hashicorp.com/agent-inject-template-mailu-env.sh: | {{ with secret "kv/data/atlas/mailu/mailu-secret" }} export SECRET_KEY="{{ index .Data.data "secret-key" }}" {{ end }} {{ with secret "kv/data/atlas/mailu/mailu-db-secret" }} export DB_PW="{{ .Data.data.password }}" export ROUNDCUBE_DB_PW="{{ .Data.data.password }}" {{ end }} {{ with secret "kv/data/atlas/mailu/mailu-initial-account-secret" }} export INITIAL_ADMIN_PW="{{ .Data.data.password }}" {{ end }} {{ with secret "kv/data/atlas/shared/postmark-relay" }} {{- $access := index .Data.data "accesskey" -}} {{- $secret := index .Data.data "secretkey" -}} {{- if and $access $secret }} export RELAYUSER="{{ $access }}" export RELAYPASSWORD="{{ $secret }}" {{- else }} export RELAYUSER="{{ index .Data.data "apikey" }}" export RELAYPASSWORD="{{ index .Data.data "apikey" }}" {{- end }} {{ end }} spec: serviceAccountName: mailu-vault-sync automountServiceAccountToken: true containers: - name: oletools command: - /entrypoint.sh args: - python3 - /start.py env: - name: SECRET_KEY $patch: delete - name: INITIAL_ADMIN_PW $patch: delete - name: DB_PW $patch: delete - name: RELAYUSER $patch: delete - name: RELAYPASSWORD $patch: delete - name: VAULT_ENV_FILE value: /vault/secrets/mailu-env.sh volumeMounts: - name: mailu-vault-entrypoint mountPath: /entrypoint.sh subPath: vault-entrypoint.sh volumes: - name: mailu-vault-entrypoint configMap: name: mailu-vault-entrypoint defaultMode: 493 - target: kind: StatefulSet name: mailu-clamav patch: |- apiVersion: apps/v1 kind: StatefulSet metadata: name: mailu-clamav spec: template: spec: containers: - name: clamav env: - name: SECRET_KEY $patch: delete - name: INITIAL_ADMIN_PW $patch: delete - name: DB_PW $patch: delete - name: RELAYUSER $patch: delete - name: RELAYPASSWORD $patch: delete - target: kind: Deployment name: mailu-tika patch: |- apiVersion: apps/v1 kind: Deployment metadata: name: mailu-tika spec: template: spec: containers: - name: tika env: - name: SECRET_KEY $patch: delete - name: INITIAL_ADMIN_PW $patch: delete - name: DB_PW $patch: delete - name: RELAYUSER $patch: delete - name: RELAYPASSWORD $patch: delete